Skip to content

feat(analyzer): extend declarative lint matcher vocabulary (scope lists, deep values search, each-match) - #1013

Merged
e54-bot merged 2 commits into
mainfrom
gh-970-declarative-matcher
Oct 27, 2026
Merged

e54-bot merged 2 commits into
mainfrom
gh-970-declarative-matcher

Conversation

@e54-bot

@e54-bot e54-bot commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator

What changed

Extends the SPEC-309 declarative lint matcher so the four structural shapes
behind the built-in rules become expressible (#970, owner decisions
2026-10-27 applied: declarative finding format, ongoing-condition-hot-path
stays native, loop-scope values searches body actions only).

  • matcher.scope accepts a list of scopes — one rule ID enumerates all
    listed scopes ([while, for-global-variable, for-player-variable] shares a
    rule). A single scope behaves as before.
  • matcher.values lists value patterns searched at any depth in every
    value tree of the scope: rule conditions + live action arguments for
    rule; live body-action arguments for a loop scope (not the loop's own
    condition; a nested loop's body is searched once per enclosing loop); the
    condition list for if. Disabled/dead statements stay excluded.
  • matcher.report: each-match anchors one finding per matched node at the
    node's own span; default scope is unchanged.
  • A values pattern may carry within (+ in-args: { from: N }), an
    ancestor constraint limited to ancestor argument positions ≥ N — so
    Filtered Array(Sorted Array(x, r), c) is not reported.
  • A call pattern accepts names: [...] as an alternative to name; every
    spelling resolves through the catalog at load time.
  • First-party package rules may declare evidence and severity in
    metadata (Support GitHub-based community lint rule discovery and lifecycle management #616 addendum); both fields are rejected with lint-rule-error
    in community packages and --rule-file rules.
  • declarative.rs split into declarative/{schema,pattern,error}.rs
    (size-guard), SPEC-309 + ADR-0015 amended, and the
    reproducer-bastion-prophet.ws fixture added (verbatim copy from
    workshop-rs import-limits, SHA-256 3a1cb891…, Apache-2.0).

Verification

  • crates/wright-analyzer/tests/yaml_contract.rs gains cases for every
    acceptance criterion: scope lists, each-match per-node anchoring at any
    depth with disabled exclusion, within/in-args binder semantics,
    names resolution + unknown-spelling rejection, first-party evidence/
    severity acceptance vs. external rejection, and span-parity checks of
    nested-binder-lambda/expensive-loop-check/min-wait-loop written in
    the new vocabulary against the native rules on
    reproducer-bastion-prophet.ws (16 findings — see note) and on lint
    fixtures.
  • cargo fmt --all -- --check: clean.
  • cargo clippy --workspace --all-targets --all-features -- -D warnings: clean.
  • cargo test --workspace --all-targets --all-features: passing (see run
    notes below).
  • git diff --check: clean.

Unverified / notes

  • The issue body says the reproducer should yield 13 nested-binder
    findings; the native rule is the acceptance reference and reports 16
    on the current fixture, matching the count workshop-rs
    docs/import-limits.md records for this file. Span parity holds; the test
    asserts parity + 16.
  • expensive-loop-check's native target list is distance, raycast,
    isInLoS — raycast is not a workshop-rs catalog id, so the declarative
    fixture lists the two resolvable spellings (distance, isInLoS), which
    is the effective native set; a YAML names: [raycast] would be a load
    error by design (unknown spelling).

Refs #970

Add the #970 vocabulary so the first-party lint package can express the
built-in rules the SPEC-309 matcher could not:

- `matcher.scope` accepts a list of scopes; each listed scope is
  enumerated under one rule ID, so the three loop kinds share a rule.
- `matcher.values` lists value patterns searched at any depth in every
  value tree of the scope: rule conditions and live action arguments for
  `rule`, live body-action arguments for a loop scope (per the owner
  decision, not the loop's own condition), and the condition list for
  `if`. Disabled and dead statements stay excluded.
- `matcher.report: each-match` anchors one finding per matched node at
  the node's own span; the default `scope` keeps one finding per scope.
- A `values` pattern may carry `within` plus `in-args: { from: N }`, an
  ancestor constraint limited to ancestor argument positions >= N, so a
  binder's array-source argument at index 0 stays outside.
- A call pattern accepts `names: [...]` as an alternative to `name`; every
  spelling resolves through the catalog at load time.
- First-party package rules may declare `evidence` and `severity` in
  metadata; the loader rejects both fields for community packages and
  `--rule-file` rules, as reserved by SPEC-309/ADR-0015.

Acceptance checks live in `crates/wright-analyzer/tests/yaml_contract.rs`:
span parity of the three expressible rules against the native analyses on
`reproducer-bastion-prophet.ws` (16 nested-binder findings — the issue
body's "13" predates the fixture's final count recorded in workshop-rs
`docs/import-limits.md`) and on lint fixtures, plus the scope-list,
each-match, within/in-args, names, and first-party-metadata contract
cases. SPEC-309 and ADR-0015 are amended in the same change.
Each-match findings under a non-`rule` scope now address the scope's
anchor action (the loop/if), matching the native loop rules instead of
the statement that owns the matched value. A `rule` scope has no anchor,
so it keeps the owning-statement semantics the native
`nested-binder-lambda` rule uses.

The native-vs-declarative parity tests now key on (span, action) so the
addressed node is asserted, not just the span.

Refs #970
@e54-bot

e54-bot commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

Review finding addressed:

Finding 1 — each-match findings under a non-rule scope addressed the owning statement instead of the scope anchor, diverging from the native loop rules on the machine-readable action field.

Fixed in 849418e: the each-match arm in run_canonical now resolves action as anchor_action.or(owner_action), so a loop scope reports the loop (matching native expensive-loop-check) while a rule scope keeps the owning statement (matching native nested-binder-lambda). The span-parity tests now key on (span, action) so the addressed node is asserted.

Verification: cargo test --workspace --all-targets --all-features (65 suites, 0 failures), cargo clippy --workspace --all-targets --all-features -- -D warnings clean, cargo fmt --all -- --check clean, git diff --check clean.

Ready for follow-up review.

@Teakowa Teakowa left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM after independent review.

@e54-bot
e54-bot merged commit ad0437c into main Oct 27, 2026
36 checks passed
@e54-bot
e54-bot deleted the gh-970-declarative-matcher branch October 27, 2026 13:57
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Make Workshop Great Again Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

2 participants