For AI agents: this page as Markdown · llms.txt · JSON Schema

Rerail code-rule DSL reference

Every construct of rerail.policy/v1: the rule keys, relations, combinators and predicates the native engine compiles, with their JSON shape, semantics, capture behaviour, three-valued behaviour, soundness under syntax errors, language support and limits, each mapped to the rule documents committed in the rule packs and the bundled code rules. For complete, tested rules that hold a codebase's design in place, start with the architecture rule examples.

Sources of truth. Accepted JSON: src/engine/code-rule/code-rule-schema.ts (zod) and src/engine/code-rule/code-rule.ts (types). Semantics: native/src/code_rule_dsl/. Every example below is either quoted from a code-rule document (its path and JSON pointer are printed above it; Ruff and ESLint ports come from conformance suites that are not part of this repository) or written for this page; each one that is not marked fragment compiles with the engine at this commit (scripts/generate/docs/code-rule-dsl-reference.mjs examples).

1. Overview #

What a code-rule document holds, which languages it reads, and how the engine evaluates it.

Documents, clauses, violations #

A code-rule document is one JSON object:

The catalog stores one document per row of ~/.rerail/rerail.sqlite; each top-level field is a column, so everything else lives in clauses. A clause has an id unique in its document and exactly one form:

FormWhat it holdsWhat it reports
forbida query: ast, regex, file, or select_whenone violation per witness (per binding with each)
require_eachtwo evidence relations, subject and evidenceeach projected subject without evidence
budgetan evidence relation, group_by keys and count.ltegroups over the budget
llmanything (the schema reads unknown)reserved
defineshared utils, options and confignothing: it has no violation; a document needs a clause beside it

Every clause but a define has a violation: a code, an anchor, a message template, and optionally fix, each, suppress. The document's mode decides what happens to it:

A fix is only a suggestion: Rerail never applies it.

Most rules are forbid clauses over an ast query: a language, a rule object matched at each candidate node, and options (utils, constraints, transform, languageOptions, when, file). A rule object is ast-grep 0.45.3's rule language (the pinned ast-grep-core) plus Rerail's name resolution, values, arguments, sub-grammars, layout, counting and comparison.

Languages come from the registry (languages/*.json, see docs/language-packs.md):

A complete document: one forbid clause over a Python ast query whose rule is a kind and a regex.
example document (whole document)
{
  "format": "rerail.policy/v1",
  "id": "example:model-config-plain-dict",
  "revision": 1,
  "mode": "steer",
  "description": "Use ConfigDict() rather than a plain dict for Pydantic model configuration.",
  "scope": {
    "include": ["**/*.py"],
    "exclude": [
      "**/node_modules/**",
      "**/.venv/**",
      "**/vendor/**",
      "**/__pycache__/**",
      "tmp/**"
    ]
  },
  "clauses": [
    {
      "id": "model-config-plain-dict",
      "forbid": {
        "ast": {
          "language": "python",
          "rule": {
            "kind": "assignment",
            "regex": "(?s)^model_config\\s*(?::[^=]*)?=\\s*(?:\\{|dict\\s*\\()"
          }
        }
      },
      "violation": {
        "code": "MODEL_CONFIG_PLAIN_DICT",
        "anchor": "witness.span",
        "message": "Pydantic model_config must be a ConfigDict(...) rather than a plain dict so the keys are type-checked."
      }
    }
  ]
}
The other common shape: a regex query over the file's text.
example document /clauses/0
{
  "id": "uuid7-wrap",
  "forbid": { "regex": { "pattern": "(?m)\\w+Id\\(uuid7\\(\\)\\)", "source": "file.content" } },
  "violation": {
    "anchor": "witness.span",
    "code": "UUID7_WRAP",
    "message": "Prefer the branded factory API over manual uuid7 wrapping."
  }
}

Language support #

Rerail follows one language standard: a code-rule language names the dialects it compiles for; its family supplies semantics. Structural terms work in every dialect: kind, pattern, regex, relations, field, nthChild, count, layout, matches. A term the family lacks is refused at compile time by the engine and zod, with its path and code-rule language.

Code-rule languageDialects it compiles forFile labels and extensionsFamily
pythonpythonpython: .py, .pyipython
typescripttypescript, tsx, javascripttypescript: .ts, .mts, .cts; tsx: .tsx; javascript: .js, .jsx, .mjs, .cjsecma
javascriptjavascriptjavascript: .js, .jsx, .mjs, .cjsecma
phpphpphp: .php except .blade.phpphp
bladebladeblade: .blade.phpblade
rustrustrust: .rsrust
vuevuevue: .vuevue
sqlsqlsql: .sqlsql
CapabilityPredicatespythonecmaphpbladerustvuesql
Scoperesolves, bindsyesyesyesyesyesyesyes
Valuevalue, compareyesyesyesyesyesyesyes
Argumentsargumentsyesyesyesyesyesyesyes
Callscallsyesnoyesyesyesnono
Format argumentsformatArgsyesnoyesyesyesnono
Suppressionsuppressnoqa, noseceslint, oxlint, biomephpstan, psalm, phpcsnoneclippyeslintsqlfluff

TypeScript rules also run on JavaScript with the JavaScript grammar; kinds unique to another dialect are Never there.

PHP. PHP-only files; .blade.php templates use Blade.

Blade. Every semantic predicate but suppress reads the view: the PHP that illuminate/view 13.34.0's BladeCompiler compiles the template to. It holds echoes, directive arguments, @php blocks, <?php ?> tags and bound component attributes, in the template's control flow, each mapped back to its template bytes.

Vue. resolves, binds, value, compare and arguments read the program, with TypeScript's semantics. The program is the <script> and <script setup> blocks (as @vue/compiler-sfc 3.5.43 compiles them) plus template expressions (in @vue/compiler-dom 3.5.43's scopes), without render-function code. It is TypeScript for lang="ts", TSX for lang="tsx", else JavaScript; coffee and other languages embed nothing.

Script content and template expressions are copied byte for byte, each node mapped to its component bytes; generated syntax maps to the Vue construct it stands for. In source order:

Text, static attributes, comments and v-pre elements write nothing.

SQL. SQLFluff 4.4.0's semantics over one generic dialect for PostgreSQL, SQLite, MySQL and ANSI, parsed by DerekStride's tree-sitter-sql 0.3.11 with Rerail's patches (native/grammars/tree-sitter-sql/PATCHES.md). Where dialects differ, PostgreSQL wins.

Rust. Names resolve lexically, as rustc does, from syntax alone (Rust Reference, "Names"). Macro token trees, macro_rules! bodies, and anything needing types or expansion are not read.

Evaluation pipeline #

For each file in a clause's scope, the engine:

  1. picks the grammar matcher, skipping the file if when, file or the language fail;
  2. narrows nodes to candidates via the rule's anchor plan;
  3. matches the rule object at each candidate: three-valued (true, false, unknown);
  4. applies the soundness gate to files with syntax errors;
  5. drops suppressed violations and renders the rest.
1 · Document $option resolved, then compiled once: patterns, utils, order, soundness class 2 · Gates scope globs language of the file when (versions, options) file (path, module) 3 · Anchors root kinds of the rule text probes (resolves) unrestricted fallback for kindless rules 4 · Match the rule object at each candidate atomic terms → ordered group (by capture dependency) → count, compare, layout, value → resolves relations walk the tree; utils share or frame captures constraints after the rule; transform after both 5 · Three-valued verdict true false unknown unknown: a value, usedness, argument slot or key the engine cannot decide root: true = witness, else none 6 · Soundness gate (syntax errors) clean file: every witness stands file with ERROR / MISSING nodes: sound rule → witnesses outside error regions stand; failure source-partial unsound rule → not evaluated; failure source-incomplete 7 · Suppress opt-in per violation: eslint-disable… # noqa, # nosec read at the anchor's start, before the witness budget 8 · Violations each: one per binding anchor: span, start, end, file.start, capture.NAME message: $VAR, ${VAR} fix: template (+ range) id: hash of witness span

The stateless evaluator (CompiledCodeRuleDsl.evaluate, sessions) and the indexed workspace (CodeRuleDslWorkspace) share every stage; the engine's tests run both paths.

Budgets. Exceeding maxVisitedNodes, the work budget, a deadline or utility depth 256 fails the clause for the file with budget-exceeded, never a false verdict. maxWitnesses, an output cap, fails it with witness-limit.

Rule objects and term order #

A rule object's keys are terms; all must hold (an implicit all). A rule object with no matcher key is refused. Terms run in a fixed order (compile/ast_rule.rs), since a term can bind captures later terms read:

  1. Atomic terms: pattern, kind, regex, capture, parse, nthChild (without ofRule), arguments.
  2. The ordered group: nthChild with ofRule, all, any, not, matches, relations (inside, has, precedes, follows), binding terms (resolves.binding / resolves.write, binds), formatArgs. A term reading a sibling's capture runs after it (a stable topological sort, compile/order.rs); mutual reads are refused. See Captures.
  3. Late terms: count, compare, layout, value; then resolves (but binding/write), which binds and reads nothing; then importTarget, which binds nothing and reads other files.

Comments #

Grammars keep comments in the tree as comment nodes: match by kind, read by regex (delimiters included). Exceptions:

Elsewhere, tell line, block and doc comments apart by their opening, with regex:

LanguageLineBlockDoc
typescript (typescript, tsx, javascript)^//^/\*([^*]|\*/)^/\*\*[^/] (JSDoc)
python^# (the only comment; a docstring is a string)nonenone
php^(#|//) (#[ opens an attribute, never a comment)^/\* and not doc^/\*\*\s (T_DOC_COMMENT; phpDoc parses it)
bladePHP's, under ast.embedded^\{\{-- (Blade), ^<!-- (HTML); PHP's under ast.embeddedPHP's, under ast.embedded
rustkind: "line_comment" (^//), not dockind: "block_comment" (^/\*), not dochas: { kind: "outer_doc_comment_marker" } (///, /**) or has: { kind: "inner_doc_comment_marker" } (//!, /*!); //// and /*** are no doc comments
vueTypeScript's, under ast.embedded^<!-- (the template's HTML); TypeScript's under ast.embeddedTypeScript's, under ast.embedded
sql^--^/\* (nested, as PostgreSQL reads them)none

Position. A comment is a tree-sitter extra: a child of the node whose span holds it, in source order. A comment before a declaration is its preceding sibling, so use sibling relations, not inside or has:

Text. A regex group anchors a violation (capture.NAME) but binds nothing compare reads. Capture the comment (pattern: "$C"), derive text with a transform, and put the compare inside the relation binding the other side (it binds from its first match). Directive comments parse into trees: eslintDirective, phpDoc.

Another language. Comments named comment need no engine change: only a style row above and a case in matcher/comments_tests.rs. Other names (line_comment, block_comment, as in Rust and Java) also go into the predicates' filters (child.kind() != "comment" in matcher/, calls/, literal/). An embedding language (Blade over PHP) keeps each grammar's comments in its own document.

Rerail's own comment. rerail-ignore silences the rules it names, by id, at the node it covers, regardless of the rule or the repository's suppressions setting (Rerail's comment). suppress reads other linters' comments only where a violation opts in and the setting is honour (the default).

Limits. rerail-ignore works where an ast query reports: forbid witnesses, a select_when's select, require_each subject rows, and a budget group's violation at its first row in path order. It never removes facts other violations read: evidence rows, counted budget rows, the exists side, calls seeds. regex witnesses (file.content, file.path) have no tree and stay unsuppressed.

An exported function with no JSDoc directly before its export statement.
{
  "language": "typescript",
  "rule": {
    "kind": "export_statement",
    "has": { "kind": "function_declaration" },
    "not": { "follows": { "stopBy": "neighbor", "kind": "comment", "regex": "^/\\*\\*[^/]" } }
  }
}
A /// <reference types="x" /> directive (a /// comment before the first statement) whose x a static import of the file names. The transform reads x out of the comment and the comparison runs inside the walk over the imports' specifiers.
{
  "id": "reference-types-imported",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "all": [
          { "kind": "comment" },
          { "pattern": "$C" },
          {
            "regex": "^///[ \\t]*<reference[ \\t](?:[^>\\n]*[ \\t])?types[ \\t]*=[ \\t]*[\"'](?<NAME>[^\"'\\n]*)[\"'][^\\n]*?/>"
          },
          {
            "not": {
              "follows": {
                "stopBy": "end",
                "not": { "any": [{ "kind": "comment" }, { "kind": "hash_bang_line" }] }
              }
            }
          },
          {
            "inside": {
              "kind": "program",
              "has": {
                "stopBy": "end",
                "kind": "string_fragment",
                "pattern": "$S",
                "inside": {
                  "kind": "string",
                  "inside": {
                    "any": [{ "kind": "import_statement" }, { "kind": "import_require_clause" }]
                  }
                },
                "compare": { "left": "T", "op": "eq", "right": "S" }
              }
            }
          }
        ]
      },
      "transform": {
        "T": {
          "replace": {
            "source": "$C",
            "replace": "^///[ \\t]*<reference[ \\t](?:[^>\\n]*[ \\t])?types[ \\t]*=[ \\t]*[\"']([^\"'\\n]*)[\"'][\\s\\S]*$",
            "by": "$1"
          }
        }
      }
    }
  },
  "violation": {
    "anchor": "capture.NAME",
    "code": "NO_REFERENCE_IMPORT",
    "message": "No need to reference \"$T\", since it is imported."
  }
}

2. Grammar #

The accepted JSON, production by production. Each production says in one line what it is; open it to see its keys, one per line, each with its type and what it does. key? is optional, a nested object is indented under its key, and an object refuses any key it does not list. A key's name links to the card in section 3 that explains it. A | B are alternatives, Str a non-empty string, NAME a capture name ([A-Z0-9_]+, written without $); "list of T" is a JSON array and "map of K to V" an object used as a map. A note under a production is a rule the schema adds (at least one key, one form, per-language keys).

For tools and AI agents: every production is in this page's HTML, open or closed. The same grammar is published as a JSON Schema, rerail.policy.v1.schema.json, generated from src/engine/code-rule/code-rule-schema.ts (zod), which the engine's host validates rules with; the hosted docs also serve this page as Markdown (code-rule-dsl-reference.md). The script scripts/generate/docs/code-rule-dsl-reference.mjs check checks every data-zod annotation names an object in that file and that its documented keys match.

Documents and clauses

DocumentOne code rule: its identity, mode, scope and clauses
  • format"rerail.policy/v1"Always rerail.policy/v1
  • idStrThe code rule's id, which rerail-ignore comments name
  • revisionInt≥1The document's revision number, starting at 1
  • mode"log" | "steer" | "block"What Rerail does with a violation: log it, steer the agent, or block
  • description?StringWhat the code rule asks for, in plain words
  • scope?ScopeThe files the code rule reads; every file when omitted
  • clauseslist of Clause, at least 1The clauses that report violations or define shared utilities and options

Clause ids are unique; at least one clause besides defines; $option references are resolved first.

ScopeThe repository-relative path globs a code rule or evidence relation reads
  • includelist of Glob, at least 1Globs of the files to read
  • excludelist of GlobGlobs of files to leave out, applied after include
ClauseOne check in a document: an id, exactly one form, and its violation
  • idStrThe clause's id, unique within the document
  • violation?Violation required except on define, refused on defineWhere each violation points and what it says; required except on a define
  • exactly one form
  • forbid?QueryReports every witness of its query
  • require_each?Reports each subject row that no evidence row matches
    • subjectEvidenceThe evidence relation whose rows must each be matched
    • evidenceEvidenceThe evidence relation whose rows satisfy a subject, usually through where
  • budget?Counts evidence rows per group and reports groups past the limit
    • queryEvidenceThe evidence relation whose rows are counted
    • group_by?list of StrProjected alias.field operands that split the rows into groups
    • countThe limit each group's count is held to
      • lteInt≥0The most rows a group may hold before it is reported
      • distinct?list of StrCounts distinct values of these operands instead of rows
  • llm?anyReserved for a future clause form
  • define?DefineHolds utilities, options and config the other clauses share; reports nothing
ViolationWhere a clause's violation points, what it says, and how it may be fixed
  • codeStrA short code naming the violation, such as SELECT_ONE
  • anchorAnchorWhere the violation points: the witness, its start or end, the file, or a capture
  • messageTemplateThe text reported, with $NAME interpolating captures and transform variables
  • fix?Template, or:A suggested replacement for the witness, never applied
    • templateTemplateThe replacement text, with $NAME interpolating captures
    • range"witness.span" | "capture.NAME"What the template replaces: the witness or a capture's range inside it
  • each?NAMEReports one violation per distinct binding of this capture at a witness
  • suppress?SuppressHonours other linters' suppression comments for this violation
  • variables?empty objectReserved; only an empty object is accepted
Anchor"witness.span" | "witness.start" | "witness.end" | "file.start" | "capture.NAME"Where a violation points: the witness, a point at its start or end, the file, or a capture
SuppressWhich other linters' suppression comments drop the violation, one entry per linter
  • eslint?Honours eslint-disable comments in TypeScript and Vue queries
    • ruleslist of RuleName, 1 to 256The ESLint rules a directive may name; one naming no rule also suppresses
  • noqa?Honours Ruff's noqa comments in Python queries
    • codeslist of Code, 1 to 256The rule codes a noqa comment may name to suppress
    • parent?"statement"Also counts a directive where the enclosing statement starts
  • nosec?Honours Bandit's nosec comments in Python queries
    • codeslist of BanditTest, 1 to 256The Bandit tests, by id or name, a nosec comment may name
  • oxlint?Honours oxlint and eslint disable directives in TypeScript queries
    • ruleslist of OxlintRuleName, 1 to 256The Oxlint rule names a directive may name to suppress
  • biome?Honours biome-ignore comments in TypeScript queries
    • ruleslist of BiomeLintRule, 1 to 256The Biome lint rules a biome-ignore comment may name
  • phpstan?Honours @phpstan-ignore comments in PHP queries
    • identifierslist of PhpstanIdentifier, 1 to 256The PHPStan error identifiers a comment may name
  • psalm?Honours @psalm-suppress doc comments in PHP queries
    • issueslist of PsalmIssue, 1 to 256The Psalm issue types a tag may name, or their parent types
    • suppressAll?BoolLets @psalm-suppress all count as a suppression
  • phpcs?Honours phpcs:ignore and phpcs:disable comments in PHP queries
    • sniffslist of PhpcsCode, 1 to 256The sniff codes a directive may name, or their category or standard
    • version?3 | 4Reads directives as PHP_CodeSniffer 3 or 4 does; 4 when omitted
  • clippy?Honours lint-level attributes such as allow and expect in Rust queries
    • lintslist of LintName, 1 to 256The lint names an allow or expect attribute may name
    • msrv?RustVersionThe Rust version the lint needs; an older clippy::msrv in force drops it
  • sqlfluff?Honours SQLFluff's noqa comments in SQL queries
    • ruleslist of SqlfluffReference, 1 to 256The SQLFluff rule references a noqa entry may name or glob

At least one.

DefineA clause that reports nothing and holds shared utilities, options and config
  • language?"python" | "typescript" | "javascript" | "php" | "blade" | "rust" | "sql" | "vue"The language whose queries see the shared utilities; required with utils
  • utils?map of UtilId to UtilityNamed rules every query of the define's language can call with matches
  • options?map of OptionName to OptionDeclTyped options declared once for the whole document
  • config?map of OptionName to JsonValues set for declared options; an unset option takes its default

At least one of utils, options, config; language is required with utils.

OptionDeclOne typed option: its type, allowed values and default
  • type"boolean" | "string" | "number" | "integer" | "array"The kind of value the option holds
  • items?ScalarTypeThe type of each item of an array option; string when omitted
  • enum?list of Json, at least 1The only values, or array items, the option accepts
  • nullable?BoolAlso accepts null as the option's value
  • defaultJsonThe value used when config leaves the option unset
  • description?StringWhat the option controls, in plain words
OptionRefStands for an option's value and is replaced by it before validation
  • "$option"OptionNameThe name of the option whose value replaces the reference
  • cases?map of Text to JsonPicks a replacement by the option's value, keyed by its text
  • default?JsonThe replacement when no case matches the option's value
  • escape?"regex"Escapes the value as regex text
  • map?StringA template applied to each array item, {} standing for the item
  • join?StringJoins array items into one string with this separator
  • template?StringText to embed the value in, {} standing for the value
  • spread?trueSplices an array value into the array holding the reference

Anywhere but format, id, revision, options and config.

UtilityA named rule that matches calls in place, optionally taking captures as parameters
  • an AstRule, or
  • paramslist of NAME, at least 1The capture names a call must pass; the utility runs in its own frame
  • ruleAstRuleThe rule the utility matches, reading only its parameters and own captures
UtilIdStr without ( ) , =A utility's name, which matches uses to call it

Queries

QueryWhat a forbid clause matches: an ast, regex or file query, or a select_when
  • exactly one source
  • regexRegexQueryRuns a Rust regex over the file's text or its path
  • astAstQueryMatches a rule object on a language's syntax tree
  • fileempty objectMatches every file in scope, with no span
  • select_whenReports select's witnesses when exists has a witness in the workspace
    • selectQueryThe query whose witnesses are reported
    • existsQueryThe query that must have at least one witness anywhere
RegexQueryA Rust regex over the file's text or path; named groups become captures
  • source"file.content" | "file.path"Whether the regex reads the file's content or its path
  • patternStrThe Rust regex; every match is a witness

A Rust regex; named groups are captures.

EvidenceA relation of rows from an AST query, projected to named fields for cross-file clauses
  • evidenceWraps an AST query whose matches become rows
    • asStrThe alias rows are known by in where and group_by operands
    • paths?ScopePath globs limiting the files this relation's query reads
    • queryAstQueryThe AST query whose matches are the rows
    • projectmap of Str to ProjectionSourceThe fields each row carries, taken from captures or file metadata
    • where?PredicateA predicate over projected alias.field operands that rows must pass
ProjectionSource"capture.NAME" | "file.path" | "file.directory" | "file.name" | "file.stem" | "file.module" | "file.package"Where a projected field's value comes from: a capture or the file's metadata
PredicateA test over projected alias.field operands that filters evidence rows
  • exactly one of
  • alllist of Predicate, at least 1Every predicate holds
  • anylist of Predicate, at least 1At least one predicate holds
  • notPredicateThe predicate does not hold
  • eqtwo Json valuesThe two operands are equal
  • netwo Json valuesThe two operands differ
AstQueryA rule object matched on a language's syntax tree, with the options around it
  • language"python" | "typescript" | "javascript" | "php" | "blade" | "rust" | "sql" | "vue"The language whose files the query reads and parses
  • embedded?ViaRuns the query on the program a Blade or Vue file embeds
  • ruleAstRuleThe rule object matched on every candidate node
  • utils?map of UtilId to UtilityNamed rules local to this query, called with matches
  • constraints?map of NAME to AstRuleRules each named capture's node must also match after the rule matched
  • languageOptions?LanguageOptionsConfiguration the file cannot show, such as globals or the parser's settings
  • when?WhenThe configurations the query applies to, gated on option values
  • file?FileConditions on the file the query reads
  • transform?map of NAME to TransformationVariables computed from captures for messages, fixes and comparisons
Via"compiled-view" (blade) | "script-block" (vue)Which embedded program an ast query reads: a Blade compiled view or a Vue script block

A via of the language's registry embeds; the query runs on that embedded program.

Rule objects and relations

AstRuleA rule object: terms that must all hold at a node, matched as ast-grep plus Rerail's terms
  • atomic
  • pattern?Str, or:A code snippet whose shape the node must have; $NAME binds a node
    • contextStrA whole snippet to parse, for fragments that do not parse alone
    • selectorKindThe kind of the sub-node of context that is matched
  • kind?KindThe node's tree-sitter kind
  • regex?StrA Rust regex the node's source text must match
  • nthChild?NthChildThe node's 1-based position among its parent's named children
  • capture?NAME inside parse onlyInside parse only: binds the parsed node under a name for the host rule
  • composite
  • all?list of AstRule, at least 1Every item matches, in written order
  • any?list of AstRule, at least 1At least one item matches
  • not?AstRuleThe rule does not match
  • matches?UtilId, or a map of UtilId to a map of NAME to "$NAME"Calls a named utility, which matches as if written in place
  • relational
  • inside?RelationAn ancestor matches the relation's rule
  • has?RelationA descendant matches the relation's rule
  • precedes?RelationA later sibling matches the relation's rule
  • follows?RelationAn earlier sibling matches the relation's rule
  • Rerail
  • compare?CompareCompares two captures: their texts, values, names, structures or positions
  • count?CountHow many nodes one relation reaches that match its rule, within bounds
  • resolves?ResolvesWhat the node's name resolves to: a global, an import, a binding, or nothing
  • value?ValueTestTests what the node evaluates to, judged from its own source
  • arguments?ArgumentsBinds a call's arguments to a signature's named slots
  • binds?BindsThe node is a binding's name node and the binding passes the filters
  • parse?ParseParses a literal's decoded value with a sub-grammar and matches a rule on it
  • formatArgs?FormatArgsA format call's unused arguments or unfilled fields
  • layout?LayoutTests the node's lines, first byte, and lines to a neighbour
  • host?AstRule inside parse, or over an embedded programA rule matched on the host document's node that this node stands in
  • embedded?AstRule over a host document whose language embeds one program; not inside parseA rule matched on the embedded program a host file's node holds
  • calls?CallsThe call reaches, through the call graph, a definition the inner rule matches
  • importTarget?ImportTarget javascript and typescript; not inside parseTests the import or require target a module specifier names

At least one key.

CallsA call-graph term: the callee reaches a definition the inner rule matches
  • through?"sync" | "any"Which functions the walk passes: only synchronous ones, or any
  • chain?NAMEBinds a capture to the definitions on the way, for messages
  • and every AstRule key but calls: the inner rule, over function definitions
RelationA rule matched on related nodes, with how far the walk goes and which field it enters
  • stopBy?"neighbor" | "end" | AstRuleHow far the walk goes: the nearest node, every node, or up to a stop rule
  • field?Str inside and has onlyRestricts has and inside to one grammar field
  • and every AstRule key: pattern?, kind?, regex?, nthChild?, all?, any?, not?, matches?, inside?, has?, precedes?, follows?, compare?, count?, resolves?, value?, arguments?, binds?, parse?, capture?, formatArgs?, layout?, host?, embedded?, calls?, importTarget? Any AstRule key, applied to the related node
NthChildA node's position among its parent's named children, optionally counted over a rule
  • Int≥0, AnB, or
  • positionInt≥0 | AnBThe 1-based position, as a number or CSS An+B
  • ofRule?AstRuleCounts only the siblings this rule matches
  • reverse?BoolCounts from the last child
AnBCSS An+B, as ast-grep's parse_an_b reads it: "2n+1", "-n+3"; "odd" is refusedA CSS An+B formula for nthChild positions, such as 2n+1
CountHow many nodes one relation reaches that match its rule, held between min and max
  • exactly one relation, without field
  • inside?RelationCounts matching ancestors
  • has?RelationCounts matching descendants, descending below each match
  • precedes?RelationCounts matching later siblings
  • follows?RelationCounts matching earlier siblings
  • bounds
  • min?Int≥0The fewest matches allowed; 0 when omitted
  • max?Int≥0The most matches allowed
  • as?NAMEBinds the count plus offset as text, for the message only
  • offset?IntA number added to the count that as binds

min ≥ 1 or a max; min ≤ max; offset needs as; as binds offset + the exact count as text, for the message only.

Comparing, values and arguments

CompareCompares two captures by text, value, name, structure, identity or list position
  • leftNAMEThe capture on the left of the comparison
  • op"lt" | "le" | "eq" | "ne" | "gt" | "ge"The operator: less, less or equal, equal, not equal, greater, greater or equal
  • rightNAMEThe capture on the right of the comparison
  • collation?"ordinal" | "fold-lower" | "fold-upper" | "lowercase-first" | "natural" | "natural-fold-lower"How two strings order: ordinal by default, case-folded or natural
  • text?"source" | "unquoted" | "value" | "key" | "member" | "name" | "reference" | "reference-no-static-keys" | "tokens" | "structure" | "structure-hash" | "structure-parts" | "node" | "index"What is compared, the source text by default; the modes table lists every mode
  • commute?list of Str, at least 1 text tokens onlyBinary operators whose operands may compare in either order, with tokens

Equality texts (tokens, structure*, node, reference*) take eq and ne and the ordinal collation only; index takes ordinal.

ValueTestPredicates over what an expression evaluates to, judged from its own source
  • eq?Str | Num | Bool | nullThe value equals this, in the language's sense of equality
  • type?TypeName, or a list of 1 to 1024The value's type name is this one or one listed
  • constant?BoolWhether the whole value is known; alone, asks whether it is known
  • startsWith?Str, or a list of 1 to 1024The decoded string starts with this text or one listed
  • regex?StrThe decoded string matches this regex, backreferences allowed
  • bitsAny?Int≥1The integer shares at least one bit with this mask
  • gt?NumThe number is greater than this
  • ge?NumThe number is greater than or equal to this
  • lt?NumThe number is less than this
  • le?NumThe number is less than or equal to this
  • truthy?BoolWhether the value is truthy, as the language's bool or ToBoolean reads it
  • follow?BoolReads a name's value through its binding, up to 16 names deep
  • name?Bool true only with eq, startsWith or regexMakes eq, startsWith and regex test what the node names instead of its value

At least one predicate besides follow and name; type names are per language.

ArgumentsBinds a call's arguments to the named slots of a signature, as the language binds them
  • signaturelist of up to 256: Ident, "/" or "*"The parameter slots in order; / ends positional-only slots, * starts keyword-only
  • bind?map of NAME to SlotBinds the argument passed to each slot as a capture; an unpassed slot fails
  • absent?list of Slot, at least 1Slots that must not be passed
  • mode?"language" | "syntactic" | "estree"How arguments fill slots: the language's own rules, syntactic, or ESTree positions

bind or absent; slots are unique and in the signature; "/" before "*"; a bound slot is not absent.

Names and bindings

ResolvesWhether a reference resolves to a global, an import, a local binding, or nothing
  • global?list of 1 to 1024 Ident, or trueA reference to a listed (or any) global or builtin that nothing local shadows
  • import?list of 1 to 1024 Dotted, RustPath or PhpName, or trueA name or attribute chain whose qualified name, through imports, is listed
  • imported?list of 1 to 1024 Dotted, RustPath or PhpNameAn import site whose imported qualified name is listed
  • prefix?trueA listed name also matches every name under it
  • relative?"never" | "text"Whether a relative import is named by its text without the dots
  • seenBefore?trueThe node comes after an absolute import of a listed module
  • directives?"eslint"Reads ESLint /* global */ comments as declaring or removing globals
  • declared?Boolfalse asks only that nothing shadows the name, declared as a global or not
  • scriptDeclarations?trueIn a script, a listed name bound at the top level counts as the global
  • this?"global" | "globalScope"A this whose value is the global object, in ESLint no-eval or no-alert's sense
  • sloppyFunctions?trueAlso a this owned by an ordinary function that is not strict
  • binding?ResolvesBindingA reference to a file-local binding that passes the filters and sub-rules
  • write?"last"Where ESLint's no-unused-vars reports a variable: its last write
  • unresolved?true | "star"A reference with no binding, builtin or declared global; star: a star import may bind it
  • beforeLocal?trueA read in a function before the function's own later binding of the name

The resolves cards say which keys combine.

RustPathIdent, then :: Ident any number of timesA Rust path from a crate, segments joined by ::, as import lists it
PhpNamean optional \, then Label, then \ Label any number of timesA PHP class, function or constant name, namespaces joined by \

Label: a letter, _ or a non-ASCII character, then those or digits; a name is at most 256 UTF-8 bytes. Dotted in python, PhpName in php and blade, RustPath in rust (import only).

ResolvesBindingFilters, sub-rules and lookup options for the binding a reference resolves to
  • filters, as in Binds: form?, scope?, used?, referenced?, builtin?, directives?, nestedUse?, exported?, latest?, outer?, bindings? The binds filters, applied to the binding the reference reads
  • sub-rules: site?, value?, annotation?, occurrence? AstRule Rules over the binding construct, its value, annotation or an occurrence
  • lookup: stopAtGlobal?, only?, destructuredValue? Bool Which binding is read: its scope's global, the only one, or a destructured value
BindsMatches a binding's name node where a linter reports it, if the binding passes the filters
  • form?Form, or a list of Form, at least 1The binding construct: assignment, import, parameter, definition and so on
  • scope?ScopeKind, or a list of ScopeKind, at least 1The kind of scope that holds the binding
  • used?BoolWhether some read resolves to the binding
  • referenced?BoolWhether some reference, writes included, resolves to the binding
  • builtin?BoolWhether it is a configured global's variable in a script
  • directives?"eslint"Counts ESLint /* exported */ comments as exports
  • nestedUse?BoolWhether the binding is read from another scope
  • exported?BoolWhether the binding is explicitly re-exported or exported
  • latest?BoolWhether this is the live binding of the name
  • outer?BoolWhether the binding was made under global or nonlocal
  • bindings?How many bindings of the name its scope holds
    • within?NAMECounts only the bindings inside this capture
    • min?Int≥0The fewest bindings of the name the scope may hold
    • max?Int≥0The most bindings of the name the scope may hold
  • site?AstRuleA rule over the construct that binds
  • value?AstRuleA rule over the value the binding carries
  • annotation?AstRuleA rule over the binding's annotation, without its colon
  • occurrence?AstRuleA rule over some definition name or reference of the variable
  • redefines?RedefinesThe binding shadows or redeclares an earlier one that passes these terms
Formassignment | annotated | annotation | augmented | named | for | with | except | import | definition | parameter | global | match | typeParameter | typeAlias | implicit | deletion | field | method | accessor | relation | cte | targetThe binding constructs form can name, per language
ScopeKindpython: module | class | function | lambda | comprehension | typeParameters · typescript: global | module | function | class | block | namespace | typeParameters | functionName · sql: queryThe scope kinds scope can name, per language
RedefinesTerms the earlier binding, the one a binding shadows or redeclares, must pass
  • filters, as in Binds: form?, scope?, used?, referenced?, builtin?, directives?, nestedUse?, exported?, latest?, outer?, bindings? The binds filters, applied to the shadowed binding
  • site?AstRuleA rule over the construct that made the shadowed binding
  • value?AstRuleA rule over the value the shadowed binding carries
  • annotation?AstRuleA rule over the shadowed binding's annotation
  • sameScope?BoolWhether the shadowed binding is in the same scope
  • sameImport?BoolWhether both bindings import the same qualified name
  • every?BoolEvery other definition must pass, not just some earlier one

Sub-grammars, layout and imports

ParseParses a literal's value with a sub-grammar and matches a rule on the tree
  • grammar"percentFormat" | "braceFormat" | "annotation" | "eslintDirective" | "printfFormat" | "scanfFormat" | "phpDoc" | "rustFormat"The sub-grammar that parses the literal or comment
  • and a rule over the parsed tree, with the AstRule keys: pattern?, kind?, regex?, nthChild?, all?, any?, not?, matches?, inside?, has?, precedes?, follows?, compare?, count?, resolves?, value?, arguments?, binds?, parse?, capture?, formatArgs?, layout?, host?, embedded?, calls?, importTarget? A rule matched on the parsed tree's root; each grammar admits some keys

Each grammar admits only some keys and kinds (see parse); none admits embedded or importTarget.

FormatArgsA format call's arguments no field reads, or fields no argument fills
  • style"brace" | "percent" | "printf" | "scanf" | "rust"The format family: brace or percent, printf or scanf, or rust
  • stringNAMEThe capture holding the format string
  • argsNAMEThe capture holding the arguments passed after the format
  • unused?AstRuleA rule matched on the arguments no field consumes
  • missing?AstRuleA rule matched on the fields or placeholders no argument fills

unused or missing. brace and percent: python; printf and scanf: php and blade; rust: rust.

LayoutTests the node's line span, its start offset, and the lines between it and a neighbour
  • span?LineCountThe line breaks inside the node, 0 on one line
  • start?LineCountThe byte offset of the node's first byte, 0 at the file's start
  • before?GapThe gap from the previous neighbour's end to the node's start
  • after?GapThe gap from the node's end to the next neighbour's start

At least one.

LineCountA count of lines, exact or within bounds
  • eqInt≥0The count is exactly this
  • or
  • min?Int≥0The count is at least this
  • max?Int≥0The count is at most this
GapThe lines between the node and a neighbour token, sibling or capture
  • to"token" | "sibling" | "capture.NAME"The neighbour: the nearest token, the nearest named sibling, or a capture
  • comments?BoolA comment counts as the neighbour instead of being passed over
  • edge?BoolWith no neighbour, the file's start or end stands in
  • breaks?LineCountThe line breaks between the two
  • blank?LineCountThe lines between the two that no token or comment touches

breaks or blank; comments and edge not with a capture.

ImportTargetThe import or require target a module specifier names, and how it resolves
  • via"import" | "require"Which visitor the rule stands for when no ancestor decides the style
  • moduleType?ModuleType, or a list of ModuleType, at least 1The specifier's module type is this one or one listed
  • builtin?BoolWhether the specifier is a Node built-in module
  • resolved?BoolWhether the target has a file path
  • failed?BoolWhether resolution failed
  • extensionSpelled?BoolWhether the specifier spells the extension file-extension-in-import expects
  • published?BoolWhether npm publishes the file the target names
  • devDependency?BoolWhether the module is listed only in devDependencies
  • declared?BoolWhether the importing package declares the module as a dependency
  • aliased?BoolWhether the specifier starts with one of the tsconfig.json paths keys
ModuleType"unknown" | "relative" | "absolute" | "node" | "npm" | "http" | "data"The module types a specifier can have, from its spelling
TransformationOne transform variable: a single operation over a source's text
replaceReplaces every match of a regex in the text
  • sourceSrcThe capture, variable or regex group the text comes from
  • default?StringThe text an unbound source reads as before the operation
  • value?BoolReads the capture's value as a string instead of its source text
  • replaceStrThe regex whose every match is replaced
  • byStringThe replacement text, expanding $1 and ${name}
substringKeeps the characters between two positions
  • sourceSrcThe capture, variable or regex group the text comes from
  • default?StringThe text an unbound source reads as before the operation
  • value?BoolReads the capture's value as a string instead of its source text
  • startChar?IntThe first character kept; negative counts from the end
  • endChar?IntThe character the substring stops before; negative counts from the end
convertConverts the text to a case, splitting it into words
  • sourceSrcThe capture, variable or regex group the text comes from
  • default?StringThe text an unbound source reads as before the operation
  • value?BoolReads the capture's value as a string instead of its source text
  • toCaseCaseThe case to convert the text to
  • separatedBy?list of SeparatorWhere to split words, by default at every separator
lineThe 1-based line of the capture's start or end
  • sourceSrcThe capture, variable or regex group the text comes from
  • default?StringThe text an unbound source reads as before the operation
  • edge?"start" | "end"Which edge's line: the capture's start by default, or its end
linesThe number of lines the capture spans
  • sourceSrcThe capture, variable or regex group the text comes from
  • default?StringThe text an unbound source reads as before the operation
countThe number of named nodes a $$$ capture holds
  • sourceSrcThe capture, variable or regex group the text comes from
  • default?StringThe text an unbound source reads as before the operation
toPrecisionThe number formatted as Number.prototype.toPrecision does
  • sourceSrcThe capture, variable or regex group the text comes from
  • default?StringThe text an unbound source reads as before the operation
  • value?BoolReads the capture's value as a string instead of its source text
  • digits1 to 100, or "$NAME"The significant digits, or a capture or variable holding them

Exactly one of these keys, holding the keys under it.

Src"$NAME" | "$$$NAME"A capture a transform reads: a single one, or a $$$ list read first node to last
CaselowerCase | upperCase | capitalize | camelCase | snakeCase | kebabCase | pascalCaseThe cases convert can produce
SeparatorcaseChange | dash | dot | slash | space | underscoreThe word boundaries convert can split at

Query options

LanguageOptionsConfiguration the file cannot show, after ESLint's languageOptions and Ruff's settings
  • typescript
  • ecmaVersion?"latest" | 3 | 5 | 6 to 17 | 2015 to 2026The ECMAScript version whose builtin globals apply
  • sourceType?"script" | "module" | "commonjs"Reads every file as a script, a module or CommonJS
  • moduleSystem?"file" | "package-type"How a file's module system is decided: by the file, or by package.json type
  • globals?map of Ident to "readonly" | "writable" | "off" | an alias, up to 16384Extra global names, each readonly, writable or off
  • parserOptions?ESLint parser options
  • jsxPragma?Ident | nullThe variable a JSX element reads, React by default; null for none
  • jsxFragmentName?Ident | nullThe variable a JSX fragment reads
  • lib?list of LibNametypescript-eslint lib names whose globals are declared
  • python
  • pythonVersion?"3.7" to "3.15"The Python version whose builtins and magic globals apply
  • typingModules?list of up to 256 DottedExtra modules treated as typing, as Ruff's lint.typing-modules
  • extendGenerics?list of up to 256 DottedExtra generic base classes, as Ruff's lint.pyflakes.extend-generics
WhenThe configurations a query applies to; otherwise it matches no file
  • pythonVersion?Range of PyVersionThe configured Python version lies in this inclusive range
  • ecmaVersion?Range of EcmaVersionThe configured ECMAScript version lies in this inclusive range
  • sourceType?SourceType, or a list of SourceType, at least 1The source type, configured or the file's own, is this one or one listed
  • ecmaFeatures?The query's ecmaFeatures flags equal these
  • option?map of OptionName to ConditionEach named document option's value meets its condition
RangeAn inclusive version range with at least one bound
  • min?TThe lowest version included
  • max?TThe highest version included

At least one; min not after max.

ConditionA test on an option's value: a value, a list of values, or in, includes or not
  • Scalar, a list of Scalar, or exactly one of
  • inlist of JsonThe value equals one of these
  • includesScalarThe array option holds this item
  • notConditionThe condition does not hold
FileConditions on the file the query reads; otherwise the query skips the file
  • path?TextCondTests the path the host sends
  • declaration?Bool tsWhether the file is a TypeScript declaration file
  • module?TextCondTests the Python module path, its names joined by dots
  • moduleName?TextCondTests the last name of the Python module path
  • package?TextCond pythonPython: tests the top package name; TypeScript: tests the nearest package.json
  • inPackage?Bool pythonWhether the host sent a Python module path
  • package?Package tsPython: tests the top package name; TypeScript: tests the nearest package.json
  • jsxRuntime?list of "react" | "other" | "unknown" tsThe file's JSX runtime is one listed
  • byteOrderMark?BoolWhether the file's text opens with a byte order mark
TextCondA regex test on a text
  • regex?StrThe text matches this regex
  • notRegex?StrThe text does not match this regex

At least one.

PackageConditions on the file's nearest package.json
  • bin?BoolWhether the file is one of the package's bin targets
  • nodeVersion?Tests the package's configured Node version range
    • exactly one of
    • subsetOfSemverRangeThe configured Node range is a subset of this npm range
    • notSubsetOfSemverRangeThe configured Node range is not a subset of this npm range
  • dependsOn?list of StrThe package lists one of these names among its dependencies
  • path?TextCondTests the file's path relative to the package's folder
  • serverGraph?BoolWhether the file is in the Next.js App Router server module graph
  • private?BoolWhether the package is private
  • published?BoolWhether npm publishes the file

At least one.

3. Constructs #

One card per construct. Each gives the JSON shape; the semantics; how it composes with other terms; what it binds or reads (captures); how it behaves when a verdict is unknown; whether a rule using it is evaluated in a file with syntax errors (sound: witnesses outside error regions are reported and the file gets a source-partial failure; source-incomplete: the clause is not evaluated on the file); which languages take it; its limits; and one to three example rules with their source. Section 4 explains the verdict model and the syntax-error classification in full.

Document and queries

format · id · revision · mode · description · scope · clauses #

The document's envelope: identity, mode, file scope and clauses.

In the grammar: Document, Scope, Evidence

  • mode: log records violations without guidance; steer sends them to the agent; block is reserved and acts like steer for now.
  • scope: repository-relative globs (globset). include is non-empty; exclude applies after. Without a scope the code rule reads every file (include: ["**"]) its queries can read: see which files each query reads.
  • Clause ids are unique. At least one clause is not a define.
  • $option references resolve before validation, except in format, id, revision and the defines' options/config (options).
  • A repeated object key in raw JSON or YAML fails with "duplicate key" and its JSON pointer.
Details: notes, shape, syntax errors, languages, limits
Notes
The catalog stores the resolved document. CompiledCodeRuleDsl.inScope(path) uses evaluation's matcher, so a host passes only the files the rule reads.
Shape
{ "format": "rerail.policy/v1", "id": Str, "revision": Int≥1, "mode": "log" | "steer" | "block", "description"?: String, "scope"?: { "include": [Glob]⁺, "exclude": [Glob] }, "clauses": [Clause]⁺ }
Syntax errors
n/a: the envelope reads no source.
Languages
any; clauses of both languages and regex clauses may mix.
Limits
An older Rerail sharing ~/.rerail/rerail.sqlite can't read a document with a newer key (codeRuleSchema is strict).
Example example:select-one: a scope that includes the API package and excludes tests and migrations.
example document (whole document)
{
  "format": "rerail.policy/v1",
  "id": "example:select-one",
  "revision": 1,
  "mode": "steer",
  "description": "Prefer lazy-row entry points for primary-key lookups in consumer code.",
  "scope": { "include": ["api/app/**/*.py"], "exclude": ["**/tests/**", "**/migrations/**"] },
  "clauses": [
    {
      "id": "select-one",
      "forbid": { "regex": { "pattern": "(?m)txn\\.select_one\\(", "source": "file.content" } },
      "violation": {
        "anchor": "witness.span",
        "code": "SELECT_ONE",
        "message": "Prefer lazy-row entry points for primary-key lookups in consumer code."
      }
    }
  ]
}

forbid · require_each · budget · llm #

A reporting clause holds exactly one form and a violation.

In the grammar: Document, Clause, Evidence

  • forbid reports every witness of its query. Its violation may take any anchor, a fix (ast or file.content regex), and each/suppress (ast only).
  • require_each reports each projected subject row lacking an evidence row.
  • budget counts projected evidence rows per group_by and reports groups past count.lte.
  • llm is reserved.
  • require_each, budget and llm violations take only anchor: "witness.span" and no fix.
Details: notes, shape, syntax errors, languages, limits
Notes
A select_when violation reads its select side. An evidence relation wraps an AST query, aliases its rows with as, and projects non-empty fields from captures or file metadata. Its optional where compares alias.field operands.
Shape
{ "id": Str, "violation": Violation, "forbid": Query } { "id": Str, "violation": Violation, "require_each": { "subject": Evidence, "evidence": Evidence } } { "id": Str, "violation": Violation, "budget": { "query": Evidence, "group_by"?: [Str], "count": { "lte": Int≥0, "distinct"?: [Str] } } } { "id": Str, "violation": Violation, "llm": any }
Syntax errors
as the evidence query.
Languages
those of the wrapped AST query.
Limits
Evidence is AST-only; project needs at least one field.
A require_each clause: every TypeScript program in scope is a subject and evidence, projected by its path. It demonstrates the structural evidence wrapper; a useful relation normally gives the two sides different path scopes or AST rules.
{
  "id": "program-has-evidence",
  "require_each": {
    "subject": {
      "evidence": {
        "as": "subject",
        "query": { "ast": { "language": "typescript", "rule": { "kind": "program" } } },
        "project": { "path": "file.path" }
      }
    },
    "evidence": {
      "evidence": {
        "as": "proof",
        "query": { "ast": { "language": "typescript", "rule": { "kind": "program" } } },
        "project": { "path": "file.path" },
        "where": { "eq": ["proof.path", "subject.path"] }
      }
    }
  },
  "violation": {
    "code": "MISSING_EVIDENCE",
    "anchor": "witness.span",
    "message": "Program has no evidence."
  }
}

ast · regex · file · select_when #

A query has exactly one source. Cross-file clause forms use Evidence instead.

In the grammar: Query, RegexQuery, AstQuery

  • ast matches a rule object on the file's syntax tree or, with embedded, on the program embedded through that via.
  • regex runs a Rust regex over file.content or file.path. Each match is a witness; named groups are its captures.
  • file yields one witness per scope file it reads (see Files), whatever its text. { "forbid": { "file": {} } } says those files shouldn't exist; exclude lists exceptions.
  • select_when reports select's witnesses if exists matches anywhere in the workspace. The violation reads select's captures.
  • file.path regex and file witnesses have no span: no fix, no capture.NAME anchor. file also takes no each.
Details: notes, shape, captures, syntax errors, files, limits
Notes
  • Named groups are (?<NAME>…) or (?P<NAME>…). Messages, anchors and file.content fix templates read them.
  • A file.path match reports the path with no line, once per path; messages read the first match.
  • exists is global by design.
  • ast options: utils, constraints, transform, languageOptions, when, file.
Shape
{ "ast": { "language", "embedded"?, "rule", "utils"?, "constraints"?, "languageOptions"?, "when"?, "file"?, "transform"? } } { "regex": { "source": "file.content" | "file.path", "pattern": Str } } { "file": {} } { "select_when": { "select": Query, "exists": Query } }
Captures
regex: named groups. ast: metavariables and other binding terms.
Syntax errors
regex: always sound (only AST leaves mark a file partial). ast: per rule, see each construct.
Files
ast: files of its language: python, typescript, javascript, php, blade, rust, vue, or sql. regex and file: every text file in scope:
  • source files of those or planned languages (read as text until parsed);
  • files with a text suffix (.json, .yaml, .yml, .toml, .md, .mdx, .rst, .txt, .sh, .bash, .zsh, .html, .css, .scss) or name (.gitignore, .npmignore);
  • other files without a NUL byte (Justfile, LICENSE, NOTICE, .gitattributes, Cargo.lock, …), only if a regex or file query (at any depth) has them in scope. Never secret-like names (SENSITIVE_FILE_NAMES, SENSITIVE_FOLDER_NAMES in src/engine/path-boundary.ts: .env, .npmrc, id_rsa, *.pem, *.tfstate, a path through .ssh/, …).
No query sees:
  • binary files (with a NUL byte): of the third kind above, passed over (judged by the first 8000 bytes if too large); otherwise reported as not checked;
  • text files over 32 MB (audit) or 1 MB (session), reported as not checked;
  • folders .git, .hg, .svn, .rerail, .venv, __pycache__, node_modules, dist, build, .next, .turbo, coverage, target, vendor, and what Git ignores while the repository follows .gitignore.
rerail-ignore can't silence a file.path or file match: it has no line.
Limits
maxAstPatternBytes and maxRegexPatternBytes cap pattern sizes: 16 KiB each in the bundled capabilities.
A file.content regex anchored at the start of the file: a byte-order mark.
{
  "id": "bom",
  "forbid": { "regex": { "source": "file.content", "pattern": "\\A\\x{FEFF}" } },
  "violation": {
    "anchor": "witness.span",
    "code": "BOM",
    "message": "The file starts with a byte-order mark."
  }
}
3 more examples
A select_when over two file.path regexes: a .tsx file whose name is not kebab-case.
{
  "id": "tsx-kebab",
  "forbid": {
    "select_when": {
      "select": {
        "regex": { "source": "file.path", "pattern": "\\A(?:.*/)?[^/]*[A-Z_][^/]*$" }
      },
      "exists": { "regex": { "source": "file.path", "pattern": "(?i)tsx$" } }
    }
  },
  "violation": {
    "anchor": "witness.start",
    "code": "FILE_NAME_CASING",
    "message": "File name must be kebab-case"
  }
}
A file query: a pack.json anywhere but at the top of a pack folder. The scope names the misplaced files; the exclude is the exception; each violation names the file, with no line.
example document (whole document)
{
  "format": "rerail.policy/v1",
  "id": "example:pack-json-placement",
  "revision": 1,
  "mode": "steer",
  "description": "Keep each pack's pack.json at the top of its pack folder.",
  "scope": { "include": ["**/pack.json"], "exclude": ["packs/*/pack.json"] },
  "clauses": [
    {
      "id": "misplaced",
      "forbid": { "file": {} },
      "violation": {
        "anchor": "witness.span",
        "code": "MISPLACED_PACK_JSON",
        "message": "Move this pack.json to the top of its pack folder, packs/<name>/pack.json."
      }
    }
  ]
}
ESLint's no-irregular-whitespace: a regex query whose named group WS is the violation's anchor.
eslint/eslint/no-irregular-whitespace/skip-comments.json /checks/3 · ESLint conformance port
{
  "id": "before-first-token-1-terminator",
  "forbid": {
    "regex": {
      "source": "file.content",
      "pattern": "\\A[\\t\\n\\r ]*(?P<WS>[\\x{2028}\\x{2029}])"
    }
  },
  "violation": {
    "anchor": "capture.WS",
    "code": "NO_IRREGULAR_WHITESPACE",
    "message": "Irregular whitespace not allowed."
  }
}

Atomic terms

kind #

The node's tree-sitter kind: a named, concrete kind of the query's grammar.

In the grammar: AstRule

  • Holds where the node has the named kind. Kinds compare by symbol id, so grammar aliases resolve.
  • A kind the grammar lacks is a grammar gap: the rule compiles for each dialect that has it (TypeScript and TSX are two), failing only if none does.
  • Abstract kinds (supertypes: expression, statement, declaration, primary_expression, pattern) are refused; list their concrete kinds under any.
Details: shape, composition, captures, unknown, syntax errors, languages, limits
Shape
"kind": Str
Composition
Atomic. Beside pattern, it narrows the pattern's root. The root's kinds decide which nodes are candidates.
Captures
none.
Unknown
never unknown.
Syntax errors
sound
Languages
python typescript; inside parse, the sub-grammar's kinds.
Limits
Named kinds only. Reach a token such as == with a kindless rule and a regex.
ESLint's no-ternary: the whole rule is one kind.
eslint/eslint/no-ternary.json /checks/0 · ESLint conformance port
{
  "id": "ternary",
  "forbid": { "ast": { "language": "typescript", "rule": { "kind": "ternary_expression" } } },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_TERNARY",
    "message": "Ternary operator used."
  }
}

pattern #

A snippet in the query's language the node must match. $NAME binds one node, $$$NAME a run of nodes; $_ matches anything unbound.

In the grammar: AstRule

  • ast-grep 0.45.3 matching (pinned ast-grep-core, default smart strictness): ignoring trivia and comments, the trees must match.
  • A repeated metavariable (f($A, $A)) requires equal text.
  • Object form: parses context whole and matches its sub-node of kind selector. For fragments that do not parse alone: a pair, a generic type, a class member.
  • Python: $ before an upper-case letter or _ becomes Python's expando; a leaf that is no valid metavariable ($At) is refused. TypeScript keeps $At as an identifier, like upstream.
Details: shape, composition, captures, unknown, syntax errors, languages, limits
Shape
"pattern": Str "pattern": { "context": Str, "selector": Kind }
Composition
The first atomic term; later terms read its bindings.
Captures
Binds $NAME (one node) and $$$NAME (a node list; messages interpolate first node to last, comments included). A name bound earlier (enclosing rule, earlier sibling, utility caller) must match the same text. See Captures.
Unknown
never unknown.
Syntax errors
sound
Languages
python typescript; inside parse, only an annotation tree.
Limits
At most maxAstPatternBytes. A pattern parsing to an ERROR is refused for that dialect.
A pattern with a metavariable: any member of console, named in the message.
{
  "id": "console-access",
  "forbid": { "ast": { "language": "typescript", "rule": { "pattern": "console.$M" } } },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_CONSOLE",
    "message": "Calls to 'console.$M' are not allowed."
  }
}
1 more example
Contextual patterns for fragments (a longhand property whose key and value are the same name), and a fix template that reuses $A.
{
  "id": "longhand-property",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "any": [
          { "pattern": { "context": "({ $A: $A })", "selector": "pair" } },
          { "pattern": { "context": "({ $A: $A } = x)", "selector": "pair_pattern" } }
        ]
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "OBJECT_SHORTHAND",
    "message": "Expected property shorthand in object literal.",
    "fix": "$A"
  }
}

regex #

A Rust regex (unanchored) that the node's source text must match.

In the grammar: AstRule

  • Matches the node's whole source text; for a parsed tree's node, its decoded text.
  • Named groups of a regex on the witness itself (not under a relation or negation) feed the violation: capture.NAME anchors, $NAME in messages, fix.range, each, transform sources.
Details: shape, composition, captures, unknown, syntax errors, languages, limits
Shape
"regex": Str
Composition
Atomic. Usually beside kind, or alone on a kindless rule to reach an anonymous token ({ "regex": "^==$" }).
Captures
Named groups, as above; no metavariables.
Unknown
never unknown.
Syntax errors
sound
Languages
python typescript
Limits
regex crate syntax: no look-around or backreferences (value.regex has backreferences).
Ruff's E742: a named group NAME read by the message.
ruff/E742.json /checks/0 · Ruff conformance port
{
  "id": "ambiguous-class-name",
  "forbid": {
    "ast": {
      "language": "python",
      "rule": {
        "kind": "identifier",
        "regex": "^(?<NAME>[lIO])$",
        "inside": { "stopBy": "neighbor", "kind": "class_definition" },
        "follows": { "stopBy": "neighbor", "regex": "^(?:class|def)$" }
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "E742",
    "message": "Ambiguous class name: `$NAME`"
  }
}

value #

Predicates on the node's value, computed from its own source: literals, plus operators, conditionals and builtin constructors folded over them. Python follows CPython; TypeScript follows eslint-utils' getStaticValue.

In the grammar: AstRule, ValueTest

  • eq: the language's equality: Python == (1 == 1.0 == True), JavaScript Object.is. type: the value's type name.
  • constant: true if the whole value is known, false if not (a list with an unknown element is not).
  • startsWith, regex test a decoded string; bitsAny, gt/ge/lt/le a number; truthy truthiness.
  • name: true: eq, startsWith and regex test what the node names, not its value.
  • follow: true: a name takes its binding's value (see resolves.binding.value), through at most 16 names per decode.
Details: notes, shape, composition, captures, unknown, syntax errors, languages, limits
Notes
  • eq: a JSON integer is a Python int.
  • type: Python type(v).__name__ (int, float, complex, str, bytes, bool, NoneType, ellipsis, tuple, list, set, dict). TypeScript typeof, plus null, array, and regexp for a regex literal. A bool is not an int.
  • constant alone asks whether the value is known.
  • startsWith, regex: Python str, not bytes. regex allows backreferences (fancy-regex, at most 1,000,000 backtracking steps on 64 KiB).
  • bitsAny: an integer sharing a bit with the mask. gt/ge/lt/le: a Python bool counts. truthy: Python bool(v), JavaScript ToBoolean.
  • name reads any node with a name, property names and bindings included: ESTree's name (TypeScript: identifiers with \u escapes decoded, private names without #), a string literal's value, PHP's symbol name. compare's name text is the same. Nameless nodes fail. Only those three predicates may sit beside it; Python and Rust refuse it.
  • Example: ESLint's id-length counts Identifier.name in graphemes (\u{61} is one). "value": { "name": true, "regex": "^(?:\\P{M}\\p{M}*){1,2}$" } holds on names of one or two.
  • follow: a followed Python list, set or dict keeps only its type; a followed TypeScript array or object a reference mutates is unknown. Builtins fold through bindings (const f = String; f(1)).
  • follow in PHP, as PHPStan reads it: a constant this file declares once takes its declaration's value. That covers const, top-level define, and constants of classes, interfaces, enums or traits declared here. Class constants resolve via self, parent, a class name, or static if the class or constant is final, inheriting only from ancestors declared here. Non-public constants read where PHP may refuse them are unknown.
  • Python decodes every literal form (bases, separators, all string prefixes and escapes including \N{…}, implicit concatenation, field-less f-strings). It folds operators (ints to 4,096 bits), and/or, conditionals and nine builtin constructors.
  • TypeScript decodes numbers, BigInts (128 bits), strings, templates, arrays, regex literals and JSX attribute strings (raw, HTML entities decoded). It folds what getStaticValue folds, including allow-listed builtin calls (String, Math.max, 'a'.toUpperCase(), parseInt…) and member reads ('abc'.length).
  • Builtin names (undefined, NaN, list()) are values only if no file-local binding shadows them and the file has no syntax errors.
Shape
"value": { "eq"?: Str|Num|Bool|null, "type"?: Name | [Name], "constant"?: Bool, "startsWith"?: Str | [Str], "regex"?: Str, "bitsAny"?: Int, "gt"?|"ge"?|"lt"?|"le"?: Num, "truthy"?: Bool, "follow"?: Bool, "name"?: Bool }
Composition
Late: after relations, count and compare; before resolves. Often in constraints on a capture. Only expressions have values: on a statement, type or property name every predicate is false, not unknown.
Captures
binds and reads none.
Unknown
An unknown value (a name without follow, a call outside the allowlists, a string past 64 KiB, nesting past 256) satisfies and fails no predicate, so not: { value } fails too. The main source of unknown verdicts.
Syntax errors
sound (builtin names read as unknown in a broken file) · with follow source-incomplete
Languages
python typescript; inside parse, the grammar's values (follow refused).
Limits
Python: no % formatting, complex division or frozenset. TypeScript: no new, Math's transcendental functions, Object.keys, Date, Symbol or callback methods. Array and object ToPrimitive is not folded.
ESLint's no-template-curly-in-string: value.regex over the decoded string, so escapes read as the runtime reads them.
eslint/eslint/no-template-curly-in-string.json /checks/0 · ESLint conformance port
{
  "id": "template-curly-in-string",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": { "kind": "string", "value": { "regex": "\\$\\{[^}]+\\}" } }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_TEMPLATE_CURLY_IN_STRING",
    "message": "Unexpected template string expression."
  }
}
2 more examples
Ruff's ASYNC115: value with type and eq, in a utility the document's define shares, constraining the argument arguments binds in syntactic mode.
ruff/ASYNC115.json /checks/0 + /checks/1 · Ruff conformance port
[
  {
    "id": "shared",
    "define": {
      "language": "python",
      "utils": {
        "number": {
          "any": [
            {
              "any": [{ "kind": "integer" }, { "kind": "float" }],
              "value": { "type": "int", "eq": 0 }
            },
            {
              "kind": "parenthesized_expression",
              "has": { "stopBy": "neighbor", "matches": "number" }
            }
          ]
        }
      }
    }
  },
  {
    "id": "trio-sleep",
    "forbid": {
      "ast": {
        "language": "python",
        "utils": {
          "target": {
            "any": [{ "kind": "identifier" }, { "kind": "attribute" }],
            "resolves": { "import": ["trio.sleep"] }
          },
          "callee-of-target": {
            "any": [
              { "matches": "target" },
              {
                "kind": "parenthesized_expression",
                "has": { "stopBy": "neighbor", "matches": "callee-of-target" }
              }
            ]
          }
        },
        "rule": {
          "kind": "call",
          "has": {
            "stopBy": "neighbor",
            "matches": "callee-of-target",
            "precedes": {
              "stopBy": "neighbor",
              "any": [{ "kind": "argument_list" }, { "kind": "generator_expression" }]
            }
          },
          "arguments": {
            "signature": ["seconds"],
            "bind": { "S": "seconds" },
            "mode": "syntactic"
          },
          "all": [
            {
              "has": {
                "stopBy": "neighbor",
                "kind": "argument_list",
                "count": {
                  "has": {
                    "stopBy": "neighbor",
                    "not": { "any": [{ "regex": "^[(),\\[\\]{}]$" }, { "kind": "comment" }] }
                  },
                  "min": 1,
                  "max": 1
                }
              }
            }
          ]
        },
        "constraints": { "S": { "matches": "number" } }
      }
    },
    "violation": {
      "anchor": "witness.span",
      "code": "ASYNC115",
      "message": "Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`"
    }
  }
]
typescript-eslint's class-literal-property-style, its unknown-key utility: constant: false with follow holds where a computed key does not fold.
eslint/typescript-eslint/class-literal-property-style.json /checks/0/forbid/ast/utils/unknown-key · ESLint conformance port
{
  "has": {
    "stopBy": "neighbor",
    "kind": "computed_property_name",
    "has": {
      "stopBy": "neighbor",
      "not": { "kind": "comment" },
      "value": { "constant": false, "follow": true }
    }
  }
}

nthChild #

The node's 1-based position among its parent's named children, as a number or CSS's An+B.

In the grammar: AstRule, NthChild

  • Counts the parent's named children, comments included; anonymous tokens and array holes are not children.
  • Matches like ast-grep 0.45.3's nth_child.rs: n / step >= 0 && n % step == 0 in i32.
  • ofRule counts only siblings it matches; reverse counts from the last. A root has no position.
Details: shape, composition, captures, unknown, syntax errors, languages, limits
Shape
"nthChild": Int | "An+B" | { "position": Int | "An+B", "ofRule"?: AstRule, "reverse"?: Bool }
Composition
Without ofRule: atomic, after regex. With it: leads the ordered binding terms, so ofRule can compare what a sibling relation binds.
Captures
ofRule tests each sibling against a copy of the caller's bindings, so siblings never reject each other. The node's own ofRule bindings are kept.
Unknown
An unknown ofRule verdict on any sibling leaves the position unknown.
Syntax errors
source-incomplete: an error region can hide or add a sibling.
Languages
python typescript; refused inside parse.
Limits
Counts as tree-sitter parses, including JSX child and decorator lists.
Written for this page: every other identifier counted from the last, among the identifier siblings only.
{
  "kind": "identifier",
  "nthChild": { "position": "2n+1", "ofRule": { "kind": "identifier" }, "reverse": true }
}
1 more example
Ruff's SIM114, utility tail (a parameterized one): the last non-comment child of a compound statement (position: 1, reverse, ofRule), recursively, returning the innermost last statement as LAST. A fragment: a utility of its query.
ruff/SIM114.json /checks/0/forbid/ast/utils/tail · Ruff conformance port
{
  "params": ["LAST"],
  "rule": {
    "any": [
      {
        "not": {
          "any": [
            { "kind": "block" },
            { "kind": "if_statement" },
            { "kind": "elif_clause" },
            { "kind": "else_clause" },
            { "kind": "for_statement" },
            { "kind": "while_statement" },
            { "kind": "try_statement" },
            { "kind": "except_clause" },
            { "kind": "finally_clause" },
            { "kind": "with_statement" },
            { "kind": "function_definition" },
            { "kind": "class_definition" },
            { "kind": "decorated_definition" },
            { "kind": "match_statement" },
            { "kind": "case_clause" }
          ]
        },
        "pattern": "$LAST"
      },
      {
        "any": [
          { "kind": "block" },
          { "kind": "if_statement" },
          { "kind": "elif_clause" },
          { "kind": "else_clause" },
          { "kind": "for_statement" },
          { "kind": "while_statement" },
          { "kind": "try_statement" },
          { "kind": "except_clause" },
          { "kind": "finally_clause" },
          { "kind": "with_statement" },
          { "kind": "function_definition" },
          { "kind": "class_definition" },
          { "kind": "decorated_definition" },
          { "kind": "match_statement" },
          { "kind": "case_clause" }
        ],
        "has": {
          "stopBy": "neighbor",
          "not": { "kind": "comment" },
          "nthChild": {
            "position": 1,
            "reverse": true,
            "ofRule": { "not": { "kind": "comment" } }
          },
          "matches": { "tail": { "LAST": "$LAST" } }
        }
      }
    ]
  }
}

layout #

The node's lines, its first byte, and the lines between it and a neighbour.

In the grammar: AstRule, Layout, LineCount, Gap

  • span: line breaks inside the node (0 on one line). start: its first byte's offset (0 at file start).
  • A gap runs from neighbour to node (before) or node to neighbour (after), end to start.
  • to picks the neighbour: "token" (nearest leaf with width), "sibling" (nearest named sibling), or "capture.NAME" (an earlier single-node capture, wholly on that side).
  • breaks: line breaks between the two (0 on one line). blank: lines between them no token or comment touches, as ESLint's sort-keys counts.
  • comments: comments count as neighbours (default: skipped). edge: with no neighbour, the file's start or end stands in (default: the gap fails).
Details: notes, shape, composition, captures, unknown, syntax errors, languages, limits
Notes
Line terminators follow the ported linter: ESLint's (\r\n, \r, \n, U+2028, U+2029) for TypeScript, Ruff's (\r\n, \r, \n) for Python. "token" is ESLint's getTokenBefore/getTokenAfter.
Shape
"layout": { "span"?: LineCount, "start"?: LineCount, "before"?: Gap, "after"?: Gap } LineCount = { "eq": N } | { "min"?: N, "max"?: N } Gap = { "to": "token" | "sibling" | "capture.NAME", "comments"?: Bool, "edge"?: Bool, "breaks"?: LineCount, "blank"?: LineCount }
Composition
Late, beside compare; every listed key must hold.
Captures
Binds nothing. A gap to capture.NAME runs after the term binding it; a capture unbound there is refused (as for compare).
Unknown
never unknown.
Syntax errors
span, start, positive gaps sound · negated gaps source-incomplete (a recovered tree cannot prove a neighbour absent)
Languages
python typescript; refused inside parse (false on a parsed node).
Limits
Lines, not columns: indent and spacing rules are out of reach. With bare \r line endings, tree-sitter-python's comment token swallows the code after it.
ESLint's no-unexpected-multiline: a line break between a tag and its template (before, to: "token"), anchored at a regex group.
eslint/eslint/no-unexpected-multiline.json /checks/2 · ESLint conformance port
{
  "id": "tagged-template",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "kind": "template_string",
        "regex": "^(?P<PAREN>`)",
        "all": [{ "layout": { "before": { "to": "token", "breaks": { "min": 1 } } } }],
        "inside": { "stopBy": "neighbor", "kind": "call_expression" }
      }
    }
  },
  "violation": {
    "anchor": "capture.PAREN",
    "code": "NO_UNEXPECTED_MULTILINE",
    "message": "Unexpected newline between template tag and template literal."
  }
}
2 more examples
ESLint's curly (multi-line), its utilities: isCollapsedOneLiner and isOneLiner as span and before counts.
eslint/eslint/curly/multi-line.json /checks/0/forbid/ast/utils · ESLint conformance port
{
  "using-declaration": {
    "kind": "expression_statement",
    "has": {
      "stopBy": "neighbor",
      "any": [
        { "kind": "assignment_expression", "has": { "stopBy": "neighbor", "regex": "^using$" } },
        {
          "kind": "await_expression",
          "has": {
            "stopBy": "neighbor",
            "kind": "assignment_expression",
            "has": { "stopBy": "neighbor", "regex": "^using$" }
          }
        }
      ]
    }
  },
  "statement": { "all": [{ "not": { "kind": "comment" } }, { "not": { "regex": "^[{}]$" } }] },
  "unsafe-if": {
    "any": [
      { "kind": "if_statement", "not": { "has": { "kind": "else_clause" } } },
      {
        "kind": "if_statement",
        "has": { "kind": "else_clause", "has": { "matches": "unsafe-if" } }
      },
      {
        "any": [
          { "kind": "for_statement" },
          { "kind": "for_in_statement" },
          { "kind": "while_statement" },
          { "kind": "labeled_statement" },
          { "kind": "with_statement" }
        ],
        "has": { "matches": "unsafe-if" }
      }
    ]
  },
  "before-else": {
    "any": [
      { "precedes": { "kind": "else_clause" } },
      {
        "inside": {
          "any": [
            { "kind": "for_statement" },
            { "kind": "for_in_statement" },
            { "kind": "while_statement" },
            { "kind": "labeled_statement" },
            { "kind": "with_statement" }
          ],
          "stopBy": "neighbor",
          "matches": "before-else"
        }
      },
      {
        "inside": {
          "kind": "else_clause",
          "stopBy": "neighbor",
          "inside": { "kind": "if_statement", "stopBy": "neighbor", "matches": "before-else" }
        }
      }
    ]
  },
  "one-statement-block": {
    "kind": "statement_block",
    "has": { "stopBy": "neighbor", "matches": "statement" },
    "not": {
      "has": {
        "stopBy": "neighbor",
        "all": [
          { "matches": "statement" },
          { "precedes": { "stopBy": "end", "matches": "statement" } }
        ]
      }
    }
  },
  "braces-necessary": {
    "all": [
      { "matches": "one-statement-block" },
      {
        "any": [
          {
            "has": {
              "stopBy": "neighbor",
              "any": [
                {
                  "any": [
                    { "kind": "lexical_declaration" },
                    { "kind": "function_declaration" },
                    { "kind": "generator_function_declaration" },
                    { "kind": "class_declaration" }
                  ]
                },
                { "matches": "using-declaration" }
              ]
            }
          },
          {
            "all": [
              { "has": { "stopBy": "neighbor", "matches": "unsafe-if" } },
              { "matches": "before-else" }
            ]
          }
        ]
      }
    ]
  },
  "requires-block": {
    "all": [
      { "kind": "statement_block" },
      {
        "any": [
          { "not": { "matches": "one-statement-block" } },
          { "matches": "braces-necessary" }
        ]
      }
    ]
  },
  "if-body": {
    "all": [
      { "inside": { "kind": "if_statement", "stopBy": "neighbor" } },
      {
        "follows": {
          "stopBy": { "not": { "kind": "comment" } },
          "all": [{ "not": { "kind": "comment" } }, { "kind": "parenthesized_expression" }]
        }
      }
    ]
  },
  "one-liner": {
    "any": [
      { "layout": { "span": { "eq": 0 } } },
      {
        "all": [
          {
            "has": {
              "regex": "^;$",
              "not": { "precedes": { "stopBy": "end", "not": { "kind": "comment" } } }
            }
          },
          {
            "not": {
              "has": {
                "all": [
                  { "not": { "kind": "comment" } },
                  { "not": { "regex": "^;$" } },
                  { "layout": { "span": { "min": 1 } } }
                ]
              }
            }
          },
          {
            "not": {
              "has": {
                "all": [
                  { "not": { "kind": "comment" } },
                  { "not": { "regex": "^;$" } },
                  { "follows": { "stopBy": "end", "not": { "kind": "comment" } } },
                  { "layout": { "before": { "to": "token", "breaks": { "min": 1 } } } }
                ]
              }
            }
          }
        ]
      }
    ]
  },
  "collapsed": {
    "any": [
      { "kind": "empty_statement" },
      {
        "all": [
          { "layout": { "before": { "to": "token", "breaks": { "eq": 0 } } } },
          { "matches": "one-liner" }
        ]
      }
    ]
  }
}

Before and after: sort-keys allowLineSeparatedGroups

Before, without layout, the variant compared keys across a blank line (ESLint's valid/99 fired; the port could not read trivia). After, the property rule adds one negated gap: a blank line before the previous named sibling starts a group.

After (the term added to the property rule).
{
  "kind": "pair",
  "not": { "layout": { "before": { "to": "sibling", "blank": { "min": 1 } } } }
}

calls #

At a call: holds where the callee's definition, in any workspace file, reaches a definition the inner rule matches, along the static call graph.

In the grammar: AstRule, Calls

  • The inner rule (all keys but through and chain) is a seed query, run on reachable definitions with the query's utilities, never on the callee at query time.
  • Holds where the callee is a seed or transitively calls one.
  • through: "sync" passes only synchronous functions, callee included; "any" (default) passes all. PHP has no async functions, so both pass all.
  • chain binds the definitions on the way (a.fetch -> b.pause) for messages: a shortest chain, first callee in file-path order on ties.
  • A callee the graph cannot name makes the term unknown, so not: { calls } never claims an unproven absence.
{ "kind": "call",
  "not": { "inside": { "kind": "await" } },
  "inside": { "kind": "function_definition", "regex": "^async ", "stopBy": { "kind": "function_definition" } },
  "calls": { "through": "sync", "chain": "CHAIN",
    "has": { "stopBy": { "kind": "function_definition" }, "kind": "call",
      "has": { "field": "function", "resolves": { "import": ["time.sleep"] } } } } }
Details: notes, shape, composition, captures, unknown, syntax errors, languages, limits
Notes
  • Seeds: Python function_definition; PHP function_definition, method_declaration, anonymous_function, arrow_function.
  • Python callees resolve statically through the file's bindings: f(), mod.f(), a.b.C.m(), aliases (run = helper). Imports, re-exports, relative imports and name assignments are followed across modules; files are found by package module and checkout path (namespace packages).
  • C() calls the __new__ and __init__ its method resolution order gives; C.m() its m (C3 over workspace classes, so inherited methods are found).
  • self.m(), cls.m(), cls() are late-bound: the method plus every override in workspace subclasses.
  • A function defined twice is the binding live at the call (the later once its module ran); definitions on both branches both count.
  • PHP call nodes: function_call_expression, member_call_expression, nullsafe_member_call_expression, scoped_call_expression, object_creation_expression, where dispatch is static. Names compare case-insensitively.
  • Functions use name resolution. An unqualified call, in a namespace, to a function neither the file declares nor PHP defines is the workspace's namespaced function if declared, else the global one. PHP's own functions are outside the workspace.
  • self::m(), parent::m(), C::m(), new C (constructor) use PHP's lookup: own method, trait alias (as), used trait's (minus insteadof exclusions), parent's, interface's (abstract yields to a later concrete one).
  • $this->m(), static::m(), new static are late-bound (method plus workspace overrides), unless the class is final (enum, anonymous class) or the method final or private.
  • Followed if a variable holds it on every path to $f(): a closure, arrow function, first-class callable (f(...), $this->m(...), C::m(...)), string or array callable, or new C (__invoke). Also constant callbacks of call_user_func and call_user_func_array. f(...) itself calls nothing.
  • PHP chains read App\Svc::fetch -> App\Repo::pause; a closure is {closure}.
Shape
"calls": { "through"?: "sync" | "any", "chain"?: NAME, …Rule }
Composition
Late, at the call node. Not in evidence or select_when queries; not nested (no calls in a seed, even via a utility).
Captures
Binds only its chain, where the call reaches.
Unknown
Where the chain meets a callee no static name gives (an unbound name a star import may bind, a relative import outside a package, a module member only a star import could bind), a file with syntax errors, or a failed seed query.
  • Python unknown: calls of a parameter, subscript, call result, lambda or super() method; methods of a local, parameter or call result (obj.m() needs type inference); names bound on several paths to non-definitions; values no name denotes (a module's or class's assigned call result); attributes no workspace class in the receiver's method resolution order defines.
  • Python decorated: unknown unless the decorator is staticmethod, classmethod, abc.abstractmethod, functools.wraps(f) or returns its argument. The definition still reaches through the wrapper.
  • Python, no edge: builtins, literal methods.
  • PHP unknown: dynamic callees ($f() of a parameter or a variable paths assign differently, $o->$m(), new $class, f()(), a non-constant call_user_func callback). Also $this, self, static, parent in a trait (they mean the using class) or in a closure passed to a call (may be rebound).
  • PHP, no edge: $o->m() unless the receiver is $this, a new expression, or a variable every path assigns one.
Syntax errors
source-incomplete in the calling file (name resolution); errors in a callee's file make its definitions unknown.
Languages
python php blade (embedded PHP calls into workspace PHP; directive calls have no callee).
Limits
Outside the graph. Python: callbacks from builtins and other modules (sorted(key=f), map); overrides in non-workspace classes. A class whose base no name denotes may derive from anything, so its late-bound methods are unknown. PHP: callbacks from other functions (array_map, framework hooks), __call/__callStatic, typed receivers (function f(C $c) { $c->m(); }), overrides in non-workspace classes.

importTarget #

At a node whose value is a module specifier: the import or require target it names. Read like eslint-plugin-n 18.4.1's ImportTarget at default options, resolved by enhanced-resolve 5.26.

In the grammar: AstRule, ImportTarget

  • Other keys of the rule pick the target nodes (an import or export source, an import() or require() argument); the term does not find them.
  • The specifier is the node's value without follow, up to its first !. No known value, no target.
  • via sets the style where no ancestor does: "import" for visitImport's targets, "require" for visitRequire's.
  • Every listed fact must hold. A fact that does not apply holds for neither true nor false.
  • Files: held files over files on disk below the workspace root; nothing above it. Hosts set the root with CodeRuleDslWorkspace.setRoot.
{ "kind": "string",
  "inside": { "field": "source", "any": [{ "kind": "import_statement" }, { "kind": "export_statement" }] },
  "importTarget": { "via": "import", "builtin": false, "failed": true } }
Details: notes, shape, composition, captures, unknown, syntax errors, languages, limits
Notes
  • Specifier: the plugin's getStringIfConstant (no scope), converted by String().
  • Style: from the first deciding ancestor, per the plugin's getModuleStyle. A call to the identifier require (through parentheses) gives require; an import declaration import (type for import type); export type … from type. Else via: "import" covers import/export sources and import(); "require" covers require() and require.resolve() arguments.
  • moduleType: the type given, or one of those listed. relative (., .., or starting ./, ../, .\, ..\), else absolute (starting / or \), else node (a built-in), data (data:), http (http://, https://), npm (a letter, digit, _, ~ or - first, after an optional @scope/), else unknown. node and npm targets have a module name: the first segment (two if scoped), minus node:.
  • builtin: a Node built-in per Node 26.5.0's module.isBuiltin (names with or without node:, plus node:-only ones).
  • resolved: the target has a file path (the plugin's filePath): the resolved file, else a relative or absolute target's own path, or a data: or http(s): import's specifier. Modules the fallback or an alias maps to false have none.
  • failed: resolution failed (resolveError). A relative path to no file is both resolved and failed.
  • extensionSpelled (relative, absolute): file-extension-in-import at its default always. The specifier must spell the named file's extension: a folder's index file; if nothing is there, a same-named file beside it, try extensions first. TypeScript maps to the compiled extension: .ts and none to .js, .mts to .mjs, .cts to .cjs, .tsx to .js under a React jsx mode above, else .jsx; none with allowImportingTsExtensions.
  • published (no module name): negated check-publish isPrivateFile. npm publishes the target file, per the importing package, read like file.package.published (files, main, .npmignore, .gitignore). Files outside the package folder are unpublished. A target without a file path is read at the workspace root (upstream's working directory). The package folder itself gives neither value.
  • devDependency (module name): check-publish isDevPackage, without allowModules: the importing package lists the module in devDependencies, not in dependencies, peerDependencies or optionalDependencies.
  • declared (module name): in check-extraneous's dependency set. That is the importing package's name and its dependencies, devDependencies, peerDependencies, optionalDependencies. Add the same from the nearest package.json above whose workspaces (array, or object packages) or pnpm-workspace.yaml packages claim the folder (globs; ! excludes). type imports also count @types packages (@types/scope__name for @scope/name).
  • aliased: a TypeScript file whose specifier starts with a tsconfig.json paths key, minus trailing /, \, * (hasTSAlias).
  • Importing package: the nearest package.json parsing to an object, at or above the file, never above the workspace root.
  • Resolution: enhanced-resolve 5.26's synchronous resolver, plugins in order, with the plugin's options:
    • Conditions node, require; plus import for the import style, import and types for type.
    • Main field main and main file index, only for the require style or node/npm targets.
    • Extensions .js, .json, .node, .mjs, .cjs. With allowImportingTsExtensions in a TypeScript file's tsconfig.json: .js, .ts, .mjs, .mts, .cjs, .cts, .json, .node.
    • The plugin's Node built-in table as a false fallback.
    • TypeScript files (.ts, .tsx, .mts, .cts): tsconfig.json paths as aliases (keys and targets minus trailing /, \, *; targets relative to the config's folder).
    • Also the extension map, reversed, as extensionAlias: .js to .ts and .tsx if jsx is react, react-jsx, react-jsxdev or react-native; else .js to .ts, .jsx to .tsx. Always .cjs to .cts, .mjs to .mts. None with allowImportingTsExtensions.
    • Otherwise defaults: package.json description files, node_modules folders, exports and imports fields, symlinks resolved.
    • import-style data: and http(s): targets are not resolved.
  • tsconfig.json: get-tsconfig 4.14.3, as eslint-plugin-n reads it: the nearest at or above the file, JSON with comments. Its compilerOptions override those it extends (string or array, later over earlier), at most 8 bases deep. A base is a path (.json appended if it names no file) or a package in a node_modules folder at or above: its tsconfig field's file, the given subpath, else its tsconfig.json.
  • Root: an absolute folder; the enforcer sets its repository's. Without one, and in a snapshot's evaluation (CompiledCodeRuleDsl.evaluate), only held files resolve, so a bare specifier needs its package installed below the root or held.
Shape
"importTarget": { "via": "import" | "require", "moduleType"?: ModuleType | [ModuleType]⁺, "builtin"?: Bool, "resolved"?: Bool, "failed"?: Bool, "extensionSpelled"?: Bool, "published"?: Bool, "devDependency"?: Bool, "declared"?: Bool, "aliased"?: Bool } ModuleType = "unknown" | "relative" | "absolute" | "node" | "npm" | "http" | "data"
Composition
Late, after resolves; every listed key must hold. Not applicable, so neither true nor false: extensionSpelled of a package, published of a named module, devDependency or declared of a path, and the last three without an importing package.json.
  • Results depend on other files, so the workspace never stores a result of a clause that reads it.
  • It re-evaluates the file at the next apply that defers nothing, once held files change in what resolution reads (a file added or removed; a manifest, project config or ignore file edited) or the root changes.
Captures
binds and reads none.
Unknown
Never unknown: no known value, or a fact that does not apply, is false, so not: { importTarget } holds there.
Syntax errors
positive sound · negated source-incomplete (a recovered tree may misread the style)
Languages
typescript javascript; refused in other languages and inside parse.
Limits
  • Default plugin options only: no tryExtensions, resolvePaths, typescriptExtensionMap, allowModules or convertPath.
  • No jsconfig.json or baseUrl (upstream reads neither).
  • A resolution past 20,000 steps fails.
  • Disk files are read only if their real path is inside the root's (outward symlinks are not followed), up to 1 MiB, as UTF-8.
  • Each apply reads disk anew, but files re-evaluate only when held files or the root change; a changed installed package is read at the importer's next evaluation.

Relations

A relation holds when a node it walks to matches the relation's rule. The figure shows which nodes each relation tests from the witness X:

has"has": { … }namebodyprogramfunctionnamebodytypestmtstmtXstmtstmtchildchildchildchildren; end: every descendantinside"inside": { … }namebodyprogramfunctionnamebodytypestmtstmtXstmtstmtchildchildchildparent; end: every ancestorprecedes"precedes": { … }namebodyprogramfunctionnamebodytypestmtstmtXstmtstmtchildchildchildnext sibling; end: every later onefollows"follows": { … }namebodyprogramfunctionnamebodytypestmtstmtXstmtstmtchildchildchildprevious sibling; end: every earlier onefield"has": { "field": "body" }namebodyprogramfunctionnamebodytypestmtstmtXstmtstmtchildchildchildthe body child; end: its subtreestopBy rule"inside": { "stopBy": { "kind": "function" } }namebodyprogramfunctionstopnamebodytypestmtstmtXstmtstmtchildchildchildwalk ends at the stop node, which is testedwitnesstested with neighbor (default)also tested with endnot testedpast the stop node

has · inside · precedes · follows #

has: a descendant matches; inside: an ancestor; precedes: a later sibling; follows: an earlier sibling.

In the grammar: AstRule, Relation

  • The relation's object is a full rule object, other relations included, plus stopBy and field.
  • Walk order: has in document order, inside outwards from the parent, precedes forwards, follows backwards from the nearest sibling.
  • The first node that matches decides.
Details: notes, shape, composition, captures, unknown, syntax errors, languages, limits
Notes
ast-grep 0.45.3's relational rules.
Shape
"has" | "inside" | "precedes" | "follows": { "stopBy"?: "neighbor" | "end" | AstRule, "field"?: Str, ...AstRule keys }
Composition
Ordered group, after atomic terms, sorted by capture dependency among siblings. Relations nest: inside: { kind: "array", inside: { … } }.
Captures
The first matching candidate binds its metavariables for the caller. A count over a relation keeps nothing its rule binds.
Unknown
A later match decides past unknown candidates. If the rule binds captures, the first unknown candidate that no other term makes false ends the search unknown, since it might have been the one to bind. An unknown stop verdict leaves the answer open. For existence only, use count: { …, min: 1 }, which searches on.
Syntax errors
has sound, negated too (it reads inside the witness) · inside, precedes, follows positive sound (the node is in the recovered tree) · negated, or with a stop rule source-incomplete (claims absence over nodes the parser may not have read)
Languages
python typescript; inside parse too (inside stops at the parsed root).
Limits
With a zero-width node in the file, the sibling check is quadratic in one container's siblings. An outward relation's first binding can change near an error region.
ESLint's no-plusplus: has an anonymous token.
eslint/eslint/no-plusplus.json /checks/1 · ESLint conformance port
{
  "id": "update-decrement",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": { "kind": "update_expression", "has": { "regex": "^--$" } }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_PLUSPLUS",
    "message": "Unary operator '--' used."
  }
}
2 more examples
ESLint's default-case-last: precedes with stopBy: "end", any later case.
eslint/eslint/default-case-last.json /checks/0 · ESLint conformance port
{
  "id": "not-last",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "kind": "switch_default",
        "precedes": { "kind": "switch_case", "stopBy": "end" }
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "DEFAULT_CASE_LAST",
    "message": "Default clause should be the last clause."
  }
}
ESLint's no-with: a kindless token rule inside a statement.
eslint/eslint/no-with.json /checks/0 · ESLint conformance port
{
  "id": "with-statement",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": { "regex": "^with$", "inside": { "kind": "with_statement" } }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_WITH",
    "message": "Unexpected use of 'with' statement."
  }
}

stopBy #

How far a relation walks.

In the grammar: Relation

  • "neighbor" (default): the nearest node only: parent, children or adjacent sibling.
  • "end": every node on the axis.
  • A rule: walk to the first node the stop rule matches, and test that node too.
Details: notes, shape, captures, unknown, syntax errors, languages, limits
Notes
For has, a pre-order walk that does not descend below a node the stop rule matches.
Shape
"stopBy": "neighbor" | "end" | AstRule
Captures
A stop rule has its own environment: it neither reads nor binds the caller's metavariables.
Unknown
An unknown stop verdict ends the walk with the answer open.
Syntax errors
On inside, precedes or follows source-incomplete (claims the nodes walked past do not stop it) · on has, as sound as the stop rule negated
Languages
both.
Limits
With stopBy: "end" or a stop rule, an ERROR sibling can change which node an outward relation binds first (review finding 3, still open).
ESLint's no-new: the stop rule not parenthesized_expression reads through any number of parentheses.
eslint/eslint/no-new.json /checks/0 · ESLint conformance port
{
  "id": "new-statement",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "kind": "expression_statement",
        "has": {
          "kind": "new_expression",
          "stopBy": { "not": { "kind": "parenthesized_expression" } }
        }
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_NEW",
    "message": "Do not use 'new' for side effects."
  }
}
1 more example
ESLint's no-sparse-arrays: follows the previous token, stepping over comments.
eslint/eslint/no-sparse-arrays.json /checks/0 · ESLint conformance port
{
  "id": "hole-comma",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "regex": "^,$",
        "inside": { "kind": "array" },
        "follows": { "regex": "^[\\[,]$", "stopBy": { "not": { "kind": "comment" } } }
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_SPARSE_ARRAYS",
    "message": "Unexpected comma in middle of array."
  }
}

field #

Restricts has and inside to a grammar field.

In the grammar: Relation

  • inside tests only ancestors entered through the field; others are skipped, not stopped at.
  • So { "inside": { "kind": "if_statement", "field": "condition", "stopBy": "end" } } holds for a name at any depth in the condition.
  • has starts at the child in that field (none: no match). neighbor tests that child, end its subtree.
  • A stop rule on has tests the child, then only its children unless the stop rule matches it.
  • A field name the grammar lacks is a grammar gap.
Details: notes, shape, composition, captures, syntax errors, languages
Notes
ast-grep 0.45.3's relational_rule.rs, line for line, including upstream's one-level stop-rule quirk on has.
Shape
"has" | "inside": { "field": Str, ... }
Composition
Only on has and inside. Refused on precedes/follows (upstream's FieldNotSupported), in a count's relation, in stop rules, and inside parse.
Captures
as the relation it narrows.
Syntax errors
classified as the relation it narrows.
Languages
both. Field names are tree-sitter's, not ESTree's or Ruff's: Python's if body is consequence.
ESLint's no-empty-function (allow: ["asyncFunctions"]): inside through the body field, and a value field in a negated inside.
eslint/eslint/no-empty-function/allow-asyncFunctions.json /checks/4 · ESLint conformance port
{
  "id": "arrow-function",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "kind": "statement_block",
        "regex": "^\\{\\s*\\}",
        "inside": {
          "field": "body",
          "all": [
            { "kind": "arrow_function" },
            {
              "not": {
                "inside": {
                  "field": "value",
                  "stopBy": { "not": { "kind": "parenthesized_expression" } },
                  "any": [{ "kind": "pair" }, { "kind": "public_field_definition" }]
                }
              }
            },
            {
              "any": [
                {
                  "has": {
                    "regex": "^async$",
                    "not": {
                      "any": [{ "kind": "identifier" }, { "kind": "property_identifier" }]
                    },
                    "pattern": "$$AS"
                  }
                },
                { "regex": "^" }
              ]
            }
          ]
        }
      },
      "transform": {
        "ASW": { "replace": { "source": "$AS", "replace": "^.+$", "by": "async " } }
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_EMPTY_FUNCTION",
    "message": "Unexpected empty $ASWarrow function."
  }
}
2 more examples

Before and after: no-empty-function

Before: the body told by which kind is unique among the children.
{
  "kind": "statement_block",
  "regex": "^\\{\\s*\\}",
  "inside": { "all": [{ "kind": "method_definition" }, "…"] }
}
After: told by field.
{
  "kind": "statement_block",
  "regex": "^\\{\\s*\\}",
  "inside": {
    "field": "body",
    "all": [
      { "kind": "method_definition" },
      { "has": { "field": "name", "kind": "property_identifier" } }
    ]
  }
}

all · any · not #

all: every item matches, in written order; any: some item matches; not: the rule does not match.

In the grammar: AstRule

  • all threads bindings through its items in written order. An item reading a capture only a later item binds is refused ("move all/2 before all/0"), unless bound before the all runs.
  • any: the first item that matches decides, and its bindings stand.
  • not: matches where its rule does not; nothing it binds leaves it.
Details: shape, composition, captures, unknown, syntax errors, languages
Shape
"all": [AstRule]⁺ "any": [AstRule]⁺ "not": AstRule
Composition
Ordered group. An any runs after a sibling that binds a name any branch reads, even if another branch binds it. A not runs after the sibling that binds any capture its rule would bind (directly or through a utility), and tests the bound value.
Captures
all and any bind what their matching items bind. not binds nothing outward; a compare inside it may read what the same not binds.
Unknown
not of unknown is unknown. any: a true branch decides, else an unknown branch makes it unknown. all: a false item decides. Past an unknown item, items using a capture it would have bound stay open, as do their readers. No false item: unknown. A rule object's keys are an implicit all with the same rules.
Syntax errors
as their items; not over an outward relation source-incomplete
Languages
both; inside parse too.
Example example:use-normal-imports: any of two patterns.
example document /clauses/0
{
  "violation": {
    "anchor": "witness.span",
    "code": "USE_NORMAL_IMPORTS",
    "message": "Use normal imports rather than import_module."
  },
  "forbid": {
    "ast": {
      "language": "python",
      "rule": {
        "any": [
          { "pattern": "importlib.import_module($MODULE)" },
          { "pattern": "import_module($MODULE)" }
        ]
      }
    }
  },
  "id": "use-normal-imports"
}
1 more example
ESLint's no-labels (allowSwitch): all with a negated has.
eslint/eslint/no-labels/allow-switch.json /checks/0 · ESLint conformance port
{
  "id": "labeled-statement",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "all": [
          { "kind": "labeled_statement" },
          {
            "not": { "has": { "stopBy": "neighbor", "any": [{ "kind": "switch_statement" }] } }
          }
        ]
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_LABELS",
    "message": "Unexpected labeled statement."
  }
}

Reuse and configuration

matches · utils #

Named rules: a query's utils maps ids to rules; matches names one, which matches at the node as if written in place.

In the grammar: AstQuery, AstRule

  • Utilities may name each other and recurse through a relation (the parentheses idiom below).
  • Ids may not contain (, ), , or =.
  • matches looks in the query's utils, then in a define's shared utilities for its language. An id may not be in both.
  • Utilities no rule reaches are still compiled and checked.
Details: notes, shape, composition, captures, unknown, syntax errors, languages, limits
Notes
ast-grep's local utilities. ast-grep reserves (, ), , and = for a call syntax.
Shape
"utils": { UtilId: AstRule | { "params": [NAME]⁺, "rule": AstRule } } (on the ast query, or in a define) "matches": UtilId | { UtilId: { PARAM: "$CAPTURE", … } }
Composition
Ordered group. A matches binds what its utility binds, transitively, so a sibling reading those captures runs after it. Utilities that meet one not yet compiled compile against its draft.
Captures
A plain utility shares its caller's environment both ways. Its compare may read any caller's binding, under not too; it is refused only if no call binds the name. A parameterized call runs in a frame (params).
Unknown
as the utility's rule.
Syntax errors
as the utility's rule.
Languages
both; a define's utilities compile for its language. matches is refused inside a parsed tree, except an annotation's.
Limits
Recursion depth 256 or 1 MiB of stack, and one maxVisitedNodes step per call; past any of them, the clause fails for the file (budget-exceeded). Skipped nodes spend no step: in a two-valued clause whose relation rule bounds its kinds, the walk skips kinds no term admits; a has to stopBy: end (field has too) also skips nodes whose text lacks a literal every match holds. Other candidates are tested, each call spending a step (as for a utility that is a lone regex, which bounds no kind). A verdict at a node is cached per file for callers agreeing on the captures it reads (for a parameterized call, on its arguments' bindings). A cached call spends the first call's steps, minus those that filled a per-file cache (scope table, predicate facts, counted siblings).
Ruff's SIM208: a recursive utility that reads through parentheses.
ruff/SIM208.json /checks/0 · Ruff conformance port
{
  "id": "double-negation",
  "forbid": {
    "ast": {
      "language": "python",
      "utils": {
        "negation": {
          "any": [
            { "kind": "not_operator" },
            {
              "kind": "parenthesized_expression",
              "has": { "stopBy": "neighbor", "matches": "negation" }
            }
          ]
        }
      },
      "rule": { "kind": "not_operator", "has": { "stopBy": "neighbor", "matches": "negation" } }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "SIM208",
    "message": "Use the operand instead of `not (not ...)`"
  }
}
1 more example
ESLint's no-eval (direct call): the utility binds $P, which the violation anchors at.
eslint/eslint/no-eval.json /checks/0 · ESLint conformance port
{
  "id": "direct-call",
  "forbid": {
    "ast": {
      "language": "typescript",
      "languageOptions": {},
      "rule": { "kind": "call_expression", "has": { "matches": "direct-callee" } },
      "utils": {
        "direct-callee": {
          "any": [
            { "kind": "identifier", "regex": "^eval$", "pattern": "$P" },
            { "kind": "parenthesized_expression", "has": { "matches": "direct-callee" } }
          ]
        }
      }
    }
  },
  "violation": { "anchor": "capture.P", "code": "NO_EVAL", "message": "`eval` can be harmful." }
}

define #

A clause that reports nothing and holds what the document's other clauses share: utilities, typed options and their configuration.

In the grammar: Clause, Define

  • Shared utils are visible to every ast query of the define's language, as if listed there.
  • Each compiles with the calling query's languageOptions, Python options and transform, so resolves reads each caller's configuration.
  • Each id may be shared once per language; shared utilities may name only shared utilities.
  • options and config: see options.
Details: notes, shape, composition, captures, languages, limits
Notes
A query compiles only the shared utilities it reaches.
Shape
{ "id": Str, "define": { "language"?: "python" | "typescript", "utils"?: Map, "options"?: Map, "config"?: Map } }
Composition
Needs at least one of utils, options, config; utils needs language. No violation; a document needs a clause besides its defines.
Captures
as utilities.
Languages
both (one language per define).
Limits
One document only; sharing across documents is unscheduled. No compiled table is shared between queries.
The worked example of ESLint's no-unused-vars as one document: a define of six shared TypeScript utilities its clauses call (matches: "u0"…).
fixtures/ecma/compile/examples/eslint-no-unused-vars.json /clauses/1 · the worked example as built; this repository keeps a cut-down copy
{
  "id": "shared",
  "define": {
    "language": "typescript",
    "utils": {
      "name": {
        "any": [
          { "kind": "identifier" },
          { "kind": "type_identifier" },
          { "kind": "property_identifier" },
          { "kind": "shorthand_property_identifier_pattern" },
          { "kind": "shorthand_property_identifier" },
          { "kind": "undefined" }
        ]
      },
      "u0": {
        "resolves": {
          "binding": {
            "occurrence": {
              "inside": {
                "any": [
                  { "kind": "assignment_expression" },
                  { "kind": "augmented_assignment_expression" },
                  { "kind": "update_expression" },
                  { "kind": "for_in_statement" },
                  { "has": { "regex": "^=$" }, "kind": "variable_declarator" },
                  { "kind": "assignment_pattern" },
                  { "kind": "object_assignment_pattern" },
                  { "has": { "regex": "^=$" }, "kind": "required_parameter" },
                  { "has": { "regex": "^=$" }, "kind": "optional_parameter" }
                ],
                "stopBy": {
                  "not": {
                    "any": [
                      { "kind": "array_pattern" },
                      { "kind": "object_pattern" },
                      { "kind": "pair_pattern" },
                      { "kind": "object_assignment_pattern" },
                      { "kind": "assignment_pattern" },
                      { "kind": "rest_pattern" },
                      { "kind": "parenthesized_expression" }
                    ]
                  }
                }
              }
            }
          },
          "write": "last"
        }
      },
      "u1": {
        "precedes": {
          "any": [
            { "kind": "pair_pattern" },
            { "kind": "shorthand_property_identifier_pattern" },
            { "kind": "object_assignment_pattern" },
            { "kind": "rest_pattern" }
          ]
        }
      },
      "u2": {
        "inside": {
          "inside": {
            "has": { "kind": "rest_pattern", "not": { "matches": "u1" } },
            "kind": "object_pattern"
          },
          "kind": "pair_pattern"
        }
      },
      "u3": {
        "inside": {
          "has": { "kind": "rest_pattern", "not": { "matches": "u1" } },
          "kind": "object_pattern"
        }
      },
      "u4": {
        "resolves": {
          "binding": { "occurrence": { "inside": { "kind": "array_pattern" } } },
          "write": "last"
        }
      },
      "u5": {
        "not": { "inside": { "any": [{ "kind": "enum_body" }, { "kind": "enum_assignment" }] } }
      }
    }
  }
}
1 more example
Ruff's PIE796 (m12c): its two clauses (.py and stub readings) share the enum utilities through one define.
ruff/PIE796.json /checks/0 · Ruff conformance port
{
  "id": "shared",
  "define": {
    "language": "python",
    "utils": {
      "auto-call": {
        "any": [
          {
            "kind": "call",
            "has": {
              "stopBy": "neighbor",
              "not": { "follows": { "stopBy": "neighbor", "regex": "(?s)." } },
              "matches": "enum-auto"
            }
          },
          {
            "kind": "parenthesized_expression",
            "has": { "stopBy": "neighbor", "matches": "auto-call" }
          }
        ]
      },
      "enum-auto": {
        "any": [{ "kind": "identifier" }, { "kind": "attribute" }],
        "resolves": { "import": ["enum.auto"] }
      },
      "enum-base": {
        "any": [{ "kind": "identifier" }, { "kind": "attribute" }],
        "resolves": { "import": ["enum.Enum"] }
      }
    }
  }
}

params #

A utility that takes captures as parameters and runs in its own frame, so it can recurse over a pair of nodes to any depth.

In the grammar: Utility

  • The frame holds only the parameters, each bound to the caller's capture (a node, a node list, a parsed tree's exported text and host range) or unbound.
  • Bindings to a parameter the caller left unbound return under the caller's name. Other captures stay in the frame, fresh at each level.
  • Pass every parameter. Calling a parameterized utility by bare id, or a plain one with an object, is refused.
  • Each distinct call (utility and mapping) is planned once: at most 1,024 per query and grammar.
Details: shape, composition, captures, unknown, syntax errors, languages, limits
Shape
"utils": { "pair": { "params": ["X", "Y"], "rule": AstRule } } "matches": { "pair": { "X": "$R", "Y": "$L" } }
Composition
A call binds the caller's names for the parameters its utility binds and reads those for the parameters it reads, so a sibling comparing a returned name runs after it.
Captures
Comparisons inside read only the frame; reading a caller's other capture is refused. Violations see returned captures, not frame captures ($YP below in a message is an error). The query's transform variables are unreadable in a frame.
Unknown
A call's verdict depends on every capture it passes; unknown inside is unknown to the caller, under not too.
Syntax errors
classified as its utility.
Languages
both.
Limits
Only captures ($NAME) are passed, no rules or text. A $$$ capture bound to no nodes reads as unbound in the frame.
ESLint's no-self-assign over array destructuring to any depth, from native/src/code_rule_dsl/compile/params_tests.rs (self_assign): pair walks up one array per level and returns the mirror on the left, pinned by index and node. About 1 KB, where the ESLint suite's port spells out three levels of arrays and objects in 206,909 bytes over three documents.
{
  "language": "typescript",
  "rule": {
    "kind": "identifier",
    "pattern": "$R",
    "matches": { "pair": { "X": "$R", "Y": "$L" } },
    "compare": { "left": "L", "op": "eq", "right": "R", "text": "name" }
  },
  "utils": {
    "pair": {
      "params": ["X", "Y"],
      "rule": {
        "pattern": "$X",
        "any": [
          {
            "inside": {
              "field": "right",
              "kind": "assignment_expression",
              "has": { "field": "left", "pattern": "$Y" }
            }
          },
          {
            "all": [
              {
                "inside": { "kind": "array", "matches": { "pair": { "X": "$XP", "Y": "$YP" } } }
              },
              {
                "inside": {
                  "stopBy": "end",
                  "kind": "assignment_expression",
                  "has": {
                    "field": "left",
                    "has": {
                      "stopBy": "end",
                      "pattern": "$Y",
                      "compare": { "left": "Y", "op": "eq", "right": "X", "text": "index" },
                      "inside": {
                        "pattern": "$YPC",
                        "compare": { "left": "YPC", "op": "eq", "right": "YP", "text": "node" }
                      }
                    }
                  }
                }
              }
            ]
          }
        ]
      }
    }
  }
}
1 more example
Ruff's SIM114 (m12c): in-range takes the arm's test T and innermost last statement LAST and reads both through layout gaps; comments-0 calls it once per arm ({ "in-range": { "T": "$T", "LAST": "$LAST" } }). A fragment: two utilities of the query.
ruff/SIM114.json /checks/0/forbid/ast/utils/in-range + /checks/0/forbid/ast/utils/comments-0 · Ruff conformance port
[
  {
    "params": ["T", "LAST"],
    "rule": {
      "kind": "comment",
      "all": [
        { "layout": { "before": { "to": "capture.T", "breaks": { "min": 1 } } } },
        { "not": { "layout": { "before": { "to": "capture.LAST", "breaks": { "min": 1 } } } } }
      ]
    }
  },
  {
    "params": ["T", "LAST"],
    "rule": {
      "all": [
        {
          "not": {
            "has": {
              "stopBy": "end",
              "matches": { "in-range": { "T": "$T", "LAST": "$LAST" } }
            }
          }
        }
      ]
    }
  }
]

options · config · $option · when.option #

Typed options with defaults, references to their values, and a gate on option values. A port that differs by configuration becomes one document; each variant sets its options.

In the grammar: Define, OptionDecl, OptionRef, When, Condition

  • Names: a letter or _, then letters, digits, _, -, .. Declare each once per document; an unset option takes its default.
  • config sets declared options: a variant's patch, written by applyCodeRuleConfig(document, options) in code-rule-options.ts.
  • $option may appear anywhere but format, id, revision and the defines' options and config. It resolves before validation.
  • A clauses item may be a reference: one clause or, spread, several. "cases": { "false": [] } drops them.
  • when.option: unless every named option meets its condition, the query matches no file. It is still compiled.
Details: notes, shape, composition, syntax errors, languages, limits
Notes
Names follow the linters' option names as written. Resolution is depth first: the value or, with cases (and default), the selected case; strings select by text, other values by JSON text (true, null, []). Then, in order: escape: "regex", map (a template per item, {} the item), join, template ({} the value). spread: true splices an array into the array holding it. A reference may stand for any value the schema accepts: a regex, a scope list, a rule, a message. A resolved document holds none. References in dropped clauses need hold only where kept. Defines are read before resolution: a reference never makes one, and a configuration leaving only defines is refused. when.option: {} always holds: the neutral gate for cases.
Shape
"options": { Name: { "type": "boolean"|"string"|"number"|"integer"|"array", "items"?, "enum"?, "nullable"?, "default", "description"? } } "config": { Name: value } { "$option": Name, "cases"?: { Text: value }, "default"?: value, "escape"?: "regex", "map"?: Str, "join"?: Str, "template"?: Str, "spread"?: true } "when": { "option": { Name: value | [scalar] | { "in": [...] } | { "includes": scalar } | { "not": Condition } } }
Composition
The engine (compile/config.rs) and zod (code-rule-options.ts) resolve identically; the catalog stores the resolved document with its config. A repository sets a Rerail rule's options with the config rule setting (repository_settings_set, whole repository only): merged per option over the defaults, checkout before repository, then the document as written is resolved again. Undeclared options and disallowed values are refused when written.
Syntax errors
n/a (resolved before compilation).
Languages
any.
Limits
References must resolve validly in every configuration, since gated queries still compile (guard a null with cases). Clauses a reference drops are not compiled. Numbers are JavaScript numbers in the engine and zod: compared by value (1 equals 1.0; integer admits 3.0). cases keys them by String(n): 1.0 selects "1", 0.00001 "0.00001", 1.5e-7 "1.5e-7", -0 "0". A "1.0" key is never selected (not refused). map, join and template print numbers the same way. Integral numbers in enum, default or config must be safe integers (|n| ≤ 9007199254740991): 9007199254740993, 253 or 1e21 is refused, as a parsed JavaScript document may have rounded it. $option is a reserved key.
Ruff's F841 since the suite adopted options (m12c): Ruff's dummy-variable-rgx is an option with Ruff's default, read by { "$option": "dummy-variable-rgx" } in the rule; a variant is a config patch.
ruff/F841.json /checks/0 + /checks/1 · Ruff conformance port
[
  {
    "id": "options",
    "define": {
      "options": {
        "dummy-variable-rgx": {
          "type": "string",
          "default": "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$",
          "description": "Names of variables that may be unused."
        }
      }
    }
  },
  {
    "id": "unused-variable",
    "forbid": {
      "ast": {
        "language": "python",
        "utils": {
          "display": {
            "any": [
              { "kind": "tuple" },
              { "kind": "expression_list" },
              { "kind": "list" },
              { "kind": "set" },
              {
                "kind": "parenthesized_expression",
                "has": { "stopBy": "neighbor", "matches": "display" }
              }
            ]
          },
          "chain-all-tuple": {
            "kind": "assignment",
            "has": {
              "stopBy": "neighbor",
              "any": [
                { "kind": "pattern_list" },
                { "kind": "list_pattern" },
                { "kind": "tuple_pattern", "has": { "stopBy": "neighbor", "regex": "^,$" } }
              ]
            },
            "any": [
              {
                "has": {
                  "stopBy": "neighbor",
                  "kind": "assignment",
                  "matches": "chain-all-tuple"
                }
              },
              { "not": { "has": { "stopBy": "neighbor", "kind": "assignment" } } }
            ]
          },
          "chain-display-value": {
            "kind": "assignment",
            "any": [
              {
                "has": {
                  "stopBy": "neighbor",
                  "kind": "assignment",
                  "matches": "chain-display-value"
                }
              },
              {
                "not": { "has": { "stopBy": "neighbor", "kind": "assignment" } },
                "has": { "stopBy": "neighbor", "matches": "display" }
              }
            ]
          },
          "chain-top": {
            "kind": "assignment",
            "not": { "inside": { "stopBy": "neighbor", "kind": "assignment" } }
          },
          "unpacked": {
            "any": [
              {
                "inside": {
                  "stopBy": { "kind": "assignment" },
                  "any": [
                    { "kind": "pattern_list" },
                    { "kind": "list_pattern" },
                    { "kind": "tuple_pattern", "has": { "stopBy": "neighbor", "regex": "^,$" } }
                  ]
                },
                "all": [
                  {
                    "inside": {
                      "stopBy": "end",
                      "matches": "chain-top",
                      "any": [
                        { "not": { "matches": "chain-display-value" } },
                        { "not": { "matches": "chain-all-tuple" } }
                      ]
                    }
                  }
                ]
              },
              {
                "binds": { "form": "named" },
                "inside": {
                  "stopBy": "end",
                  "matches": "chain-top",
                  "all": [
                    { "matches": "chain-all-tuple" },
                    { "not": { "matches": "chain-display-value" } }
                  ]
                }
              },
              {
                "binds": { "form": "with" },
                "inside": { "stopBy": "end", "kind": "tuple", "matches": "with-target-top" }
              }
            ]
          },
          "with-target-top": {
            "inside": {
              "stopBy": "neighbor",
              "any": [
                { "kind": "as_pattern_target" },
                { "kind": "parenthesized_expression", "matches": "with-target-top" }
              ]
            }
          }
        },
        "rule": {
          "kind": "identifier",
          "pattern": "$N",
          "any": [
            {
              "binds": {
                "form": [
                  "assignment",
                  "annotated",
                  "augmented",
                  "named",
                  "with",
                  "match",
                  "typeAlias"
                ],
                "scope": ["function", "lambda"],
                "latest": true,
                "outer": false,
                "used": false
              },
              "not": {
                "any": [
                  {
                    "regex": "^(?:__tracebackhide__|__traceback_info__|__traceback_supplement__|__debuggerskip__)$"
                  },
                  { "matches": "unpacked" }
                ]
              }
            },
            { "binds": { "form": "except", "used": false } }
          ],
          "not": { "regex": { "$option": "dummy-variable-rgx" } }
        }
      }
    },
    "violation": {
      "anchor": "witness.span",
      "code": "F841",
      "message": "Local variable `$N` is assigned to but never used"
    }
  }
]
2 more examples
The worked example of Ruff's A005 (184,058 bytes over 16 documents to 10,115): a define of three options, and one per-version clause whose notRegex is built from allowed-modules with cases, escape, join and template.
fixtures/python/compile/examples/ruff-A005.json /clauses/0 + /clauses/2 · the worked example as built; this repository keeps a cut-down copy
[
  {
    "id": "options",
    "define": {
      "options": {
        "strict-checking": { "type": "boolean", "default": false },
        "src": { "type": "array", "default": ["src"] },
        "allowed-modules": { "type": "array", "default": [] }
      }
    }
  },
  {
    "id": "stdlib-module-shadowing-3.7-3.7",
    "forbid": {
      "ast": {
        "language": "python",
        "when": { "pythonVersion": { "max": "3.7" } },
        "file": {
          "module": {
            "regex": "(?:^|\\.)(?:macpath)(?:\\.__init__)?$",
            "notRegex": {
              "$option": "allowed-modules",
              "cases": { "[]": "[^\\s\\S]" },
              "default": {
                "$option": "allowed-modules",
                "escape": "regex",
                "join": "|",
                "template": "(?:^|\\.)(?:{})(?:\\.__init__)?$"
              }
            }
          },
          "moduleName": { "notRegex": "\\." }
        },
        "rule": { "kind": "module" }
      }
    },
    "violation": {
      "anchor": "witness.span",
      "code": "A005",
      "message": "Module shadows a Python standard-library module"
    }
  }
]
Written for this page: a nullable pattern option that is no filter until set (cases with spread), a clause gated by when.option, and a config setting the variant.
[
  {
    "id": "options",
    "define": {
      "options": {
        "ignorePattern": { "type": "string", "nullable": true, "default": null },
        "mode": { "type": "string", "enum": ["all", "none"], "default": "all" }
      },
      "config": { "ignorePattern": "^_" }
    }
  },
  {
    "id": "unused-name",
    "forbid": {
      "ast": {
        "language": "python",
        "when": { "option": { "mode": { "not": "none" } } },
        "rule": {
          "kind": "identifier",
          "all": [
            { "binds": { "form": "assignment", "scope": "function", "used": false } },
            {
              "$option": "ignorePattern",
              "cases": { "null": { "kind": "identifier" } },
              "default": { "not": { "regex": { "$option": "ignorePattern" } } }
            }
          ]
        }
      }
    },
    "violation": { "code": "UNUSED", "anchor": "witness.span", "message": "unused" }
  }
]

constraints #

Per-capture rules checked after the query's rule matched.

In the grammar: AstQuery

  • The node a metavariable captured must also match the constraint's rule.
  • Single-node captures only.
  • An unbound metavariable is not checked: it passes.
Details: notes, shape, composition, captures, unknown, syntax errors, languages
Notes
ast-grep's constraints.
Shape
"constraints": { NAME: AstRule } (on the ast query)
Composition
After the rule matched, before transform. Typically value, resolves or a matches.
Captures
The rule matches at the captured node; its bindings join the witness.
Unknown
The first failing constraint's verdict stands (unknown before false is unknown).
Syntax errors
as its rule. source-incomplete for a capture an outward relation binds under any: an error region can leave it unbound, which passes.
Languages
both.
ESLint's no-unneeded-ternary: a repeated $B constrained to a boolean literal through a utility, and $T constrained by a negation.
eslint/eslint/no-unneeded-ternary/no-default-assignment.json /checks/1 · ESLint conformance port
{
  "id": "same-values",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": { "kind": "ternary_expression", "pattern": "$T ? $B : $B" },
      "utils": { "bool-lit": { "any": [{ "kind": "true" }, { "kind": "false" }] } },
      "constraints": {
        "B": { "matches": "bool-lit" },
        "T": { "not": { "kind": "identifier" } }
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_UNNEEDED_TERNARY",
    "message": "Unnecessary use of boolean literals in conditional expression."
  }
}

Comparing and counting

compare #

Compares two captures by text, value, name, structure, identity or list position.

In the grammar: AstRule, Compare

  • Both sides must be bound: a single-node capture, a parsed tree's exported capture, or a transform variable. An unbound side fails.
  • A transform variable compares as text: source or unquoted only.
  • text picks what is compared (default source); the table below lists every mode.
  • Equality modes (tokens, structure, structure-hash, structure-parts, node, reference, reference-no-static-keys) take only eq/ne, under the ordinal collation.
  • index compares numbers, under the ordinal collation.
Details: shape, composition, captures, unknown, syntax errors, languages
Shape
"compare": { "left": NAME, "op": "lt"|"le"|"eq"|"ne"|"gt"|"ge", "right": NAME, "text"?: Mode, "collation"?: Collation, "commute"?: [OperatorText]⁺ }
Composition
A late term: runs after its rule object's binding terms and reads the bindings so far. Inside a relation's rule, the walk continues to the first node whose captures compare. Inside not or count, it may read what that not/count binds.
Captures
Reads left and right; binds nothing. Comparing a name never bound where it runs is refused at compile time ("`$N` is never bound where this comparison runs").
Unknown
value: either value unknown, or an order would convert an object, array or regex. member: a key does not fold (PHP: a computed name's value is unknown or PHP refuses it). key/name: an order against a nameless side. Structural modes: a capture that does not decode (PHP: an interpolated heredoc). A transform of an unknown value without default.
Syntax errors
sound (reads only captured nodes) · negated, over a capture an outward relation binds under any source-incomplete
Languages
both; modes per language as in the table.
textLangComparesOps
sourceboththe captures' source text (default)all
unquotedboththe text without one pair of enclosing quotes; escapes stayall
valuebothwhat the expressions evaluate to, as value reads them. Python: == and order. JavaScript: Object.is for eq/ne, relational operators for orders (null <= 0, '1' < 2, NaN never). Collation applies where both are stringsall
keyts phpTypeScript: ESLint core's getStaticPropertyName of a key, or of the pair, method, field, signature or member access holding it; a computed key's expression alone is named as its property. PHP: the name of the property, method, class constant, array key or named argument a node is or holds ($o->a, $o?->a(), C::$a, C::A, ['a' => 1], $a['a'], f(a: 1), a member declaration). A computed name is its constant value as PHP converts it ($o->{'a' . 'b'} is ab; [1.5 => x] and $a['1'] are 1); a dynamic one ($o->$x) is nameless. A nameless key equals nothing, not even itself; an order against it is unknownall
memberts phpTypeScript: typescript-eslint's getStaticMemberAccessValue: a non-computed identifier's or private name's name (#a is a), else String(v) of the folded key, following bindings. PHP: key, following a computed name's variables to their binding ($o->$k after $k = 'a' is a). Unknown where the key does not foldall
namets phpTypeScript: the node's own ESTree name: an identifier's name with escapes decoded, or a string literal's value ('ab' equals ab). PHP: the symbol's lookup name, as written, not resolved: functions, classes, methods and namespaces folded to ASCII lower case (Foo() names function foo); variables without $; properties and constants as written, a constant's namespace folded; a string literal's value. Other nodes are nameless. A parsed tree's capture: its textall
referencets phpTypeScript: ESLint's isSameReference: ignoring parentheses and optional chains, the same this/super, identifier or private name, literals by equalLiteralValue, member accesses of the same reference (a.b is a['b']); never calls, templates or a!. PHP: ignoring parentheses, ?-> as ->: the same variable, constant, literal (===), or access of the same object, class (self is not static) or array by the same converted key ($o->a is $o->{'a'}, $a[1] is $a['1']); never calls, new or $a[]eq ne
reference-no-static-keysts phpreference with disableStaticComputedKey (operator-assignment): a.b and a['b'] differ. PHP: a written name differs from a computed one; literal keys compare by === ($a[1] is not $a['1'])eq ne
tokensbothleaf tokens, ignoring comments and white space (ESLint's equalTokens). With commute, listed binary operators compare in either ordereq ne
structurepy phpPython: Ruff's ComparableExpr: syntax trees, ignoring parentheses, comments and trivia; literals by type and value (1 == 0x1, 'a' == "a", 1 != 1.0 != True). PHP: nikic/PHP-Parser's trees, attributes stripped: literals by type and value; ignores keyword case and synonyms PHP-Parser does not tell apart (array() and [], <> and !=, (integer) and (int), exit and die, new C and new C(), a single statement and a block, : ... endif; and {...}, "$a" and "{$a}", modifiers in any order)eq ne
structure-hashpy phpPython: Ruff's HashableExpr: numbers folded by value (1 == 1.0 == True), constant tuples element by element. PHP: a constant expression by the array key PHP coerces it to (1 == '1' == 1.5 == true, null == ''), anything else as structureeq ne
structure-partspyRuff's ComparableLiteral: implicitly concatenated strings part by part ("a" "b" != "ab" ""). PHP refuses it: no implicit concatenationeq ne
nodebothwhether both captures are the same node (range and kind), not just the same texteq ne
indexbotheach capture's position in its list (ESTree's element index: commas before it; holes count, comments do not). A node outside a list has none and compares falseall
ESLint's sort-vars: the default source text with gt, comparing a declarator's name with an earlier one's inside a follows.
eslint/eslint/sort-vars.json /checks/0 · ESLint conformance port
{
  "id": "sort-vars",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "kind": "variable_declarator",
        "all": [
          {
            "has": {
              "stopBy": "neighbor",
              "kind": "identifier",
              "pattern": "$NAME",
              "not": { "follows": { "stopBy": "neighbor", "regex": "^=$" } }
            }
          },
          {
            "follows": {
              "stopBy": "end",
              "kind": "variable_declarator",
              "has": {
                "stopBy": "neighbor",
                "kind": "identifier",
                "pattern": "$PREV",
                "not": { "follows": { "stopBy": "neighbor", "regex": "^=$" } },
                "compare": {
                  "left": "PREV",
                  "op": "gt",
                  "right": "NAME",
                  "collation": "ordinal"
                }
              }
            }
          }
        ]
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "SORT_VARS",
    "message": "Variables within the same declaration block should be sorted alphabetically."
  }
}
9 more examples
ESLint's no-self-assign (props: true), object members: key pairs the properties, reference compares the members, each: "L" reports every pair.
eslint/eslint/no-self-assign/props-true.json /checks/8 · ESLint conformance port
{
  "id": "obj-member",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "any": [{ "kind": "member_expression" }, { "kind": "subscript_expression" }],
        "pattern": "$R",
        "all": [{ "pattern": "$X1" }],
        "inside": {
          "field": "value",
          "stopBy": { "not": { "kind": "parenthesized_expression" } },
          "kind": "pair",
          "pattern": "$PR1",
          "not": { "precedes": { "kind": "spread_element", "stopBy": "end" } },
          "inside": {
            "kind": "object",
            "inside": {
              "field": "right",
              "stopBy": { "not": { "kind": "parenthesized_expression" } },
              "kind": "assignment_expression",
              "has": {
                "field": "left",
                "kind": "object_pattern",
                "has": {
                  "kind": "pair_pattern",
                  "pattern": "$PL1",
                  "all": [
                    { "compare": { "left": "PL1", "op": "eq", "right": "PR1", "text": "key" } }
                  ],
                  "has": {
                    "field": "value",
                    "any": [{ "kind": "member_expression" }, { "kind": "subscript_expression" }],
                    "pattern": "$L",
                    "compare": { "left": "L", "op": "eq", "right": "R", "text": "reference" }
                  }
                }
              }
            }
          }
        }
      },
      "transform": { "RN": { "replace": { "source": "$R", "replace": "\\s+", "by": "" } } }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_SELF_ASSIGN",
    "message": "'$RN' is assigned to itself.",
    "each": "L"
  }
}
ESLint's no-self-compare: tokens over two operands found through parentheses by utilities.
eslint/eslint/no-self-compare.json /checks/0 · ESLint conformance port
{
  "id": "self-compare",
  "forbid": {
    "ast": {
      "language": "typescript",
      "utils": {
        "left": {
          "any": [
            { "not": { "kind": "parenthesized_expression" }, "pattern": "$L" },
            {
              "kind": "parenthesized_expression",
              "has": { "stopBy": "neighbor", "not": { "kind": "comment" }, "matches": "left" }
            }
          ]
        },
        "right": {
          "any": [
            { "not": { "kind": "parenthesized_expression" }, "pattern": "$R" },
            {
              "kind": "parenthesized_expression",
              "has": { "stopBy": "neighbor", "not": { "kind": "comment" }, "matches": "right" }
            }
          ]
        }
      },
      "rule": {
        "kind": "binary_expression",
        "has": {
          "stopBy": "neighbor",
          "regex": "^(===|==|!==|!=|>|<|>=|<=)$",
          "follows": { "stopBy": "end", "not": { "kind": "comment" }, "matches": "left" },
          "precedes": { "stopBy": "end", "not": { "kind": "comment" }, "matches": "right" }
        },
        "compare": { "left": "L", "op": "eq", "right": "R", "text": "tokens" }
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_SELF_COMPARE",
    "message": "Comparing to itself is potentially pointless."
  }
}
Ruff's B033: structure-hash finds a set element equal, as Python hashes it, to a later one.
ruff/B033.json /checks/0 · Ruff conformance port
{
  "id": "duplicate-value",
  "forbid": {
    "ast": {
      "language": "python",
      "utils": {
        "literal": {
          "any": [
            { "kind": "string", "regex": "^[rRuUbB]{0,2}['\"]" },
            {
              "kind": "concatenated_string",
              "not": {
                "has": {
                  "stopBy": "neighbor",
                  "kind": "string",
                  "not": { "regex": "^[rRuUbB]{0,2}['\"]" }
                }
              }
            },
            { "kind": "integer" },
            { "kind": "float" },
            { "kind": "true" },
            { "kind": "false" },
            { "kind": "none" },
            { "kind": "ellipsis" }
          ]
        },
        "literal-p": {
          "any": [
            { "matches": "literal" },
            {
              "kind": "parenthesized_expression",
              "has": { "stopBy": "neighbor", "matches": "literal-p" }
            }
          ]
        },
        "dup-slot": {
          "any": [
            {
              "inside": { "stopBy": "neighbor", "kind": "set" },
              "count": {
                "follows": {
                  "stopBy": "end",
                  "matches": "literal-p",
                  "pattern": "$F",
                  "compare": { "left": "F", "op": "eq", "right": "V", "text": "structure-hash" }
                },
                "min": 1
              }
            },
            {
              "inside": {
                "stopBy": "neighbor",
                "kind": "parenthesized_expression",
                "matches": "dup-slot"
              }
            }
          ]
        }
      },
      "rule": { "matches": "literal", "pattern": "$V", "all": [{ "matches": "dup-slot" }] }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "B033",
    "message": "Sets should not contain duplicate item `$V`"
  }
}
ESLint's no-self-assign over arrays: index pairs elements by position, and bounds the spread before the element.
eslint/eslint/no-self-assign/props-false.json /checks/1 · ESLint conformance port
{
  "id": "arr-id",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "kind": "identifier",
        "pattern": "$R",
        "all": [{ "pattern": "$X1" }],
        "inside": {
          "stopBy": { "not": { "kind": "parenthesized_expression" } },
          "kind": "array",
          "not": {
            "has": {
              "kind": "spread_element",
              "pattern": "$SP1",
              "all": [
                { "compare": { "left": "SP1", "op": "lt", "right": "X1", "text": "index" } }
              ]
            }
          },
          "inside": {
            "field": "right",
            "stopBy": { "not": { "kind": "parenthesized_expression" } },
            "kind": "assignment_expression",
            "has": {
              "field": "left",
              "kind": "array_pattern",
              "has": {
                "kind": "identifier",
                "pattern": "$L",
                "compare": { "left": "L", "op": "eq", "right": "R", "text": "name" },
                "all": [
                  { "compare": { "left": "L", "op": "eq", "right": "R", "text": "index" } }
                ]
              }
            }
          }
        }
      },
      "transform": { "RN": { "replace": { "source": "$R", "replace": "\\s+", "by": "" } } }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_SELF_ASSIGN",
    "message": "'$RN' is assigned to itself.",
    "each": "L"
  }
}
ESLint's no-useless-rename: name equates 'a' and a.
eslint/eslint/no-useless-rename/ignore-destructuring.json /checks/1 · ESLint conformance port
{
  "id": "import-commented",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "all": [
          { "kind": "import_specifier" },
          {
            "has": {
              "stopBy": "neighbor",
              "pattern": "$K",
              "any": [{ "kind": "identifier" }, { "kind": "string" }],
              "precedes": { "regex": "^as$", "stopBy": "end" }
            }
          },
          {
            "has": {
              "stopBy": "neighbor",
              "pattern": "$V",
              "any": [{ "kind": "identifier" }, { "kind": "string" }],
              "follows": { "regex": "^as$", "stopBy": "end" }
            }
          },
          { "compare": { "left": "K", "op": "eq", "right": "V", "text": "name" } },
          { "has": { "kind": "comment", "stopBy": "neighbor" } }
        ]
      },
      "transform": { "VT": { "substring": { "source": "$V", "value": true } } }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_USELESS_RENAME",
    "message": "Import $VT unnecessarily renamed."
  }
}
ESLint's operator-assignment: reference-no-static-keys, positive and negated.
eslint/eslint/operator-assignment/always.json /checks/2 · ESLint conformance port
{
  "id": "replaceable-commutative",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "kind": "assignment_expression",
        "all": [
          {
            "has": {
              "stopBy": "neighbor",
              "not": { "kind": "comment" },
              "pattern": "$L",
              "precedes": { "stopBy": "end", "regex": "^=$" }
            }
          },
          {
            "has": {
              "stopBy": { "not": { "kind": "parenthesized_expression" } },
              "kind": "binary_expression",
              "all": [
                {
                  "has": {
                    "stopBy": "neighbor",
                    "not": { "kind": "comment" },
                    "pattern": "$B",
                    "precedes": {
                      "stopBy": "end",
                      "regex": "^(?:\\*|&|\\^|\\|)$",
                      "pattern": "$$OP"
                    }
                  }
                },
                {
                  "has": {
                    "stopBy": "neighbor",
                    "not": { "kind": "comment" },
                    "pattern": "$BR",
                    "follows": { "stopBy": "end", "regex": "^(?:\\*|&|\\^|\\|)$" }
                  }
                }
              ]
            }
          },
          {
            "not": {
              "compare": {
                "left": "L",
                "op": "eq",
                "right": "B",
                "text": "reference-no-static-keys"
              }
            }
          },
          {
            "compare": {
              "left": "L",
              "op": "eq",
              "right": "BR",
              "text": "reference-no-static-keys"
            }
          }
        ]
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "OPERATOR_ASSIGNMENT",
    "message": "Assignment (=) can be replaced with operator assignment ($OP=)."
  }
}

Before and after: Ruff's structural ports

PortModeSizeTP/FP/FNEval
PIE796structure38.9 → 19.1 KB45/1/21 → 66/0/00.57 → 0.42 ms
SIM116structure-parts35.2 → 11.3 KB14/0/1 → 15/0/08.99 → 2.71 ms
F601structure-hash11.5 → 4.3 KB89/1/6 → 95/0/00.83 → 0.75 ms

PIE796's five typed value groups and its tokens fallback became one comparison; so did SIM116's five groups and F601's value/tokens split. The committed PIE796 counts earlier members whose value $U (returned by the parameterized utility member) equals $V:

Ruff's PIE796, the counted comparison (a fragment: it calls a utility of its query).
ruff/PIE796.json /checks/1/forbid/select_when/select/ast/rule/all/1 · Ruff conformance port
{
  "count": {
    "follows": {
      "stopBy": "end",
      "matches": { "member": { "X": "$U" } },
      "compare": { "left": "U", "op": "eq", "right": "V", "text": "structure" }
    },
    "min": 1
  }
}

Before and after: no-dupe-else-if (commute)

Before, in the suite's covers utility.
{ "pattern": "$X", "compare": { "left": "X", "op": "eq", "right": "C", "text": "tokens" } }
After (committed): b || a now covers a || b.
eslint/eslint/no-dupe-else-if.json /checks/0/forbid/ast/utils/covers/any/0 · ESLint conformance port
{
  "pattern": "$X",
  "compare": {
    "left": "X",
    "op": "eq",
    "right": "C",
    "text": "tokens",
    "commute": ["||", "&&"]
  }
}

collation #

How two strings order under compare.

In the grammar: Compare

  • ordinal (default): UTF-16 code units, as JavaScript's <.
  • fold-lower/fold-upper: after toLowerCase()/toUpperCase().
  • lowercase-first: after swapping the case of ASCII letters.
  • natural: the natural-compare package's order, digit runs by number (ESLint sort-keys natural).
  • natural-fold-lower: natural after toLowerCase().
Details: shape, composition, languages
Shape
"collation": "ordinal" | "fold-lower" | "fold-upper" | "lowercase-first" | "natural" | "natural-fold-lower"
Composition
Applies to text modes, and to value where both values are strings. Equality modes and index accept only ordinal.
Languages
both.
Case-insensitive ordering of named imports: fold-lower, in utilities that bind both sides.
{
  "id": "named-imports",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "kind": "import_specifier",
        "all": [
          { "matches": "unsorted" },
          {
            "not": {
              "follows": {
                "stopBy": "end",
                "kind": "import_specifier",
                "matches": "unsorted-before"
              }
            }
          }
        ]
      },
      "utils": {
        "unsorted": {
          "pattern": "$A",
          "precedes": { "stopBy": "end", "kind": "import_specifier", "pattern": "$B" },
          "compare": { "left": "B", "op": "lt", "right": "A", "collation": "fold-lower" }
        },
        "unsorted-before": {
          "pattern": "$C",
          "precedes": { "stopBy": "end", "kind": "import_specifier", "pattern": "$D" },
          "compare": { "left": "D", "op": "lt", "right": "C", "collation": "fold-lower" }
        }
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "ORDERED_IMPORTS",
    "message": "Named imports must be alphabetized ($B before $A)."
  }
}

count #

Counts the nodes one relation reaches that match its rule.

In the grammar: AstRule, Count

  • Exactly one relation, plus min (default 0) and/or max; needs min ≥ 1 or a max.
  • The relation walks as usual, but has keeps descending below a match, so every matching descendant counts.
  • No field in a count.
Details: shape, composition, captures, unknown, syntax errors, languages, limits
Shape
"count": { "inside" | "has" | "precedes" | "follows": Relation, "min"?: N, "max"?: N }
Composition
A late term, after the ordered group. For existence without binding, count: { …, "min": 1 } searches past unknown candidates where a binding relation would stop.
Captures
The counted rule reads the caller's metavariables; nothing it binds is kept. A compare inside it may read what the counted rule binds.
Unknown
The true count lies between the matches and the matches plus unknown candidates. The verdict is unknown when that range straddles a bound.
Syntax errors
over has sound · over inside, precedes, follows source-incomplete (a count is an absence claim too)
Languages
both; inside parse too.
Limits
A message cannot show a relation's match count. The count transform counts a $$$ capture's nodes.
ESLint's max-classes-per-file: a has count on the program, reported once, at the start of its first top-level node that is no comment.
eslint/eslint/max-classes-per-file/ignore-expressions-max-1.json /checks/0 · ESLint conformance port
{
  "id": "too-many-classes",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "not": { "kind": "comment" },
        "all": [
          {
            "inside": {
              "kind": "program",
              "count": {
                "has": {
                  "stopBy": "end",
                  "any": [
                    { "kind": "class_declaration" },
                    { "kind": "abstract_class_declaration" }
                  ]
                },
                "min": 2
              },
              "stopBy": "neighbor"
            }
          },
          { "not": { "follows": { "stopBy": "end", "not": { "kind": "comment" } } } }
        ]
      }
    }
  },
  "violation": {
    "anchor": "witness.start",
    "code": "MAX_CLASSES_PER_FILE",
    "message": "File has too many classes. Maximum allowed is 1."
  }
}
1 more example
ESLint's no-duplicate-case: a follows count whose rule compares with tokens.
eslint/eslint/no-duplicate-case.json /checks/0 · ESLint conformance port
{
  "id": "duplicate-case",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "kind": "switch_case",
        "all": [
          {
            "has": {
              "stopBy": "neighbor",
              "not": { "kind": "comment" },
              "matches": "t",
              "precedes": { "regex": "^:$", "stopBy": "end" }
            }
          },
          {
            "count": {
              "follows": {
                "stopBy": "end",
                "kind": "switch_case",
                "all": [
                  {
                    "has": {
                      "stopBy": "neighbor",
                      "not": { "kind": "comment" },
                      "matches": "u",
                      "precedes": { "regex": "^:$", "stopBy": "end" }
                    }
                  }
                ],
                "compare": { "left": "U", "op": "eq", "right": "T", "text": "tokens" }
              },
              "min": 1
            }
          }
        ]
      },
      "utils": {
        "t": {
          "any": [
            { "not": { "kind": "parenthesized_expression" }, "pattern": "$T" },
            {
              "kind": "parenthesized_expression",
              "has": {
                "stopBy": "neighbor",
                "all": [{ "not": { "kind": "comment" } }, { "not": { "regex": "^[()]$" } }],
                "matches": "t"
              }
            }
          ]
        },
        "u": {
          "any": [
            { "not": { "kind": "parenthesized_expression" }, "pattern": "$U" },
            {
              "kind": "parenthesized_expression",
              "has": {
                "stopBy": "neighbor",
                "all": [{ "not": { "kind": "comment" } }, { "not": { "regex": "^[()]$" } }],
                "matches": "u"
              }
            }
          ]
        }
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_DUPLICATE_CASE",
    "message": "Duplicate case label."
  }
}

Names and bindings

Two keys read the file's scope table, built on first use once per file revision and reading (Python version and typing settings, TypeScript block scoping, stub files). resolves asks what a reference resolves to; binds asks about a binding's name node.

Python follows Ruff 0.16.9's semantic model; TypeScript, eslint-scope and typescript-eslint 8.70.1's scope manager. Resolution is lexical and per file: no flow, types or other files.

Python (Ruff's model) module class C function f 1import subprocess as sp 2x = 1 3class C: 4 x = 2 5 def f(self): 6 sp.run(x, y) 7print(x) 8y = 3 1 2 3 4 1 sp.run → subprocess.run: resolves.import (qualified through the import) 2 x → line 2: a class scope is invisible to nested scopes 3 y → line 8: function bodies are read after the module 4 print → builtin: resolves.global; x → line 2 (module and class bodies see only bindings made earlier) binds forms: 1 import · 2, 4, 8 assignment · 3, 5 definition · self parameter binds scope: 1, 2, 3, 7, 8 module · 4, 5 class · self function TypeScript (eslint-scope) module function g block 1var a = 1; 2function g(c) { 3 if (c) { 4 let b = a; 5 } 6 var a = 2; 7 return b; 8} 9alert(a); 1 2 3 1 a → line 6: var hoists to g's scope; lookup ignores order 2 b → nothing: let is block-scoped; resolves.unresolved holds (ESLint's no-undef) 3 alert → a global: resolves.global; a → line 1 binds scope: 1, g module (global in a script) · c, 6 function · 4 block values and types are separate namespaces; declare global shadows nothing ES3/ES5 (languageOptions.ecmaVersion): blocks are no scopes

Resolution reads the binding the scope table picks. Python: module and class bodies see only earlier bindings (an assignment's value first, a comprehension's element after its clauses); function and lambda bodies resolve after the whole module; global/nonlocal redirect; star imports never shadow; an unconditional del unbinds by order; string type definitions are forward references. TypeScript: var hoists to the nearest function, module, namespace or static block; let, const, classes and function declarations bind in their block (from ES2015); a JSX tag is a reference when it starts upper case.

resolves #

What a reference resolves to within its file: a global, builtin, imported name, file-local binding, or nothing. The cards below group the keys.

In the grammar: AstRule

  • global, import, imported and this combine; any may match. seenBefore goes only beside imported.
  • binding (with write), unresolved (with directives) and beforeLocal stand alone. Put other terms in their own resolves under all/any.
  • not resolves also holds on binding sites and non-reference names. For "a shadowed reference", anchor on a reference context (the callee of $F($$$)).
  • Parentheses are transparent: (input) references input; (time).sleep is the chain time.sleep.
Details: shape, composition, captures, unknown, syntax errors, languages
Shape
"resolves": { "global"?, "import"?, "imported"?, "prefix"?, "relative"?, "seenBefore"?, "directives"?, "declared"?, "scriptDeclarations"?, "this"?, "sloppyFunctions"?, "binding"?, "write"?, "unresolved"?, "beforeLocal"? }
Composition
binding/write run in the ordered group (their sub-rules may bind); the rest runs last in its rule object. Anchor plans select reference kinds and probe text for listed names; a miss never builds the table.
Captures
None, except binding's sub-rules, which share the caller's environment.
Unknown
Never. The scope table's build is charged to maxVisitedNodes once per clause and file that builds or reads it (not for another clause's utility verdict). Running out fails the clause (budget-exceeded, never false).
Syntax errors
source-incomplete (an error region can hide a binding), except a positive imported-only term sound
Languages
per key, below.
A callee resolved through the file's imports, in a constraint (any import form or alias of subprocess.run or os.path.join; a local function of the same name does not match).
{
  "language": "python",
  "rule": { "pattern": "$F($$$)" },
  "constraints": { "F": { "resolves": { "import": ["subprocess.run", "os.path.join"] } } }
}

global · declared · directives · scriptDeclarations #

A reference to a global or builtin name that no visible file-local binding shadows.

In the grammar: Resolves

  • global: [names]: a listed, unshadowed reference. With languageOptions, it must also be a declared global or builtin.
  • global: true: any unshadowed declared global or builtin. Refuses declared.
  • declared: false (needs global and languageOptions): only require that nothing shadows the name (ESLint's no-alert).
  • directives: "eslint" (TypeScript): also read /* global a, b: off */ comments, as ESLint's parseStringConfig does. off in any comment wins.
  • scriptDeclarations: true (TypeScript, needs global and languageOptions): in a script, a listed top-level binding is the global itself, so its references match.
Details: notes, shape, languages, limits
Notes
  • Declared globals: TypeScript's ecmaVersion builtins, CommonJS, globals, lib; Python's builtins and magic globals of pythonVersion (Ruff's tables). Without languageOptions: ESLint's latest module globals, Ruff's 3.10 builtins.
  • directives declare or remove names with languageOptions. Without, they only remove a listed name (off).
  • scriptDeclarations follows eslint-scope's global scope; parameters and block bindings still shadow.
Shape
"global": [Ident]¹⁻¹⁰²⁴ | true, "declared"?: Bool, "directives"?: "eslint", "scriptDeclarations"?: true
Languages
python typescript: global, declared. typescript only: directives, scriptDeclarations. In an eslintDirective parse, global alone reads an item's text as a declared global.
Limits
In a Python scope declaring a name global, it never matches global (Ruff's has_builtin_binding). readonly and writable are not told apart.
typescript-eslint's no-unsafe-function-type: a type reference to the global Function, not a local type of that name.
eslint/typescript-eslint/no-unsafe-function-type.json /checks/0 · ESLint conformance port
{
  "id": "function-type",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": { "kind": "type_identifier", "resolves": { "global": ["Function"] } }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_UNSAFE_FUNCTION_TYPE",
    "message": "The `Function` type accepts any function-like value.\nPrefer explicitly defining any function parameters and return type."
  }
}
2 more examples
ESLint's no-alert: declared: false for shadowing only, directives for /* global */, and this: "globalScope", in one clause.
eslint/eslint/no-alert.json /checks/0 · ESLint conformance port
{
  "id": "alert",
  "forbid": {
    "ast": {
      "language": "typescript",
      "languageOptions": {},
      "rule": { "kind": "call_expression", "has": { "matches": "callee" } },
      "utils": {
        "global-obj": {
          "any": [
            { "kind": "identifier", "resolves": { "global": ["window"], "declared": false } },
            {
              "kind": "identifier",
              "resolves": { "global": ["globalThis"], "directives": "eslint" }
            },
            { "kind": "this", "resolves": { "this": "globalScope" } },
            { "kind": "parenthesized_expression", "has": { "matches": "global-obj" } }
          ]
        },
        "callee": {
          "any": [
            {
              "kind": "identifier",
              "pattern": "$NAME",
              "resolves": { "global": ["alert", "confirm", "prompt"], "declared": false }
            },
            {
              "kind": "member_expression",
              "all": [
                {
                  "has": {
                    "kind": "property_identifier",
                    "regex": "^(alert|confirm|prompt)$",
                    "pattern": "$NAME"
                  }
                },
                { "has": { "matches": "global-obj" } }
              ]
            },
            {
              "kind": "subscript_expression",
              "all": [
                {
                  "has": {
                    "any": [
                      { "kind": "string" },
                      {
                        "kind": "template_string",
                        "not": { "has": { "kind": "template_substitution" } }
                      }
                    ],
                    "has": {
                      "kind": "string_fragment",
                      "regex": "^(alert|confirm|prompt)$",
                      "pattern": "$NAME"
                    }
                  }
                },
                { "has": { "matches": "global-obj" } }
              ]
            },
            { "kind": "parenthesized_expression", "has": { "matches": "callee" } }
          ]
        }
      }
    }
  },
  "violation": { "anchor": "witness.span", "code": "NO_ALERT", "message": "Unexpected $NAME." }
}
ESLint's no-eval, references to eval: scriptDeclarations makes var eval in a script the global itself.
eslint/eslint/no-eval.json /checks/1 · ESLint conformance port
{
  "id": "eval-reference",
  "forbid": {
    "ast": {
      "language": "typescript",
      "languageOptions": {},
      "rule": {
        "all": [
          { "any": [{ "kind": "identifier" }, { "kind": "shorthand_property_identifier" }] },
          {
            "resolves": {
              "global": ["eval"],
              "directives": "eslint",
              "scriptDeclarations": true
            }
          },
          { "pattern": "$G" },
          {
            "not": {
              "inside": { "kind": "call_expression", "has": { "matches": "direct-callee" } }
            }
          },
          {
            "not": {
              "inside": {
                "kind": "parenthesized_expression",
                "inside": { "kind": "call_expression", "has": { "matches": "direct-callee" } }
              }
            }
          }
        ]
      },
      "utils": {
        "direct-callee": {
          "any": [
            { "kind": "identifier", "regex": "^eval$", "pattern": "$P" },
            { "kind": "parenthesized_expression", "has": { "matches": "direct-callee" } }
          ]
        }
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_EVAL",
    "message": "`eval` can be harmful."
  }
}

import · prefix #

A name or attribute chain whose qualified name, through the file's imports and definitions, is listed.

In the grammar: Resolves

  • import a.b binds a to a; import m as x, x to m; from m import x as y, y to m.x.
  • The chain's head resolves to an import or a def/class; its qualified name plus the rest of the chain must be listed. Star imports never resolve.
  • import: true is Ruff's resolve_qualified_name(..).is_some(): an import, a qualified definition or an unshadowed builtin.
  • prefix: true: a listed name also matches names under it (pickle matches pickle.loads, not pickles), for import and imported. Refused with import: true.
Details: notes, shape, languages, limits
Notes
Definitions are qualified by the module path (the host's module, else the file's stem); relative imports resolve only against one. Under languageOptions.typingModules, a listed typing.<member> also matches that member of a typing module.
Shape
"import": [Dotted | RustPath | PhpName]¹⁻¹⁰²⁴ | true, "prefix"?: true
Languages
  • python: dotted names; \ or :: refused.
  • php, blade: class, function and constant names (PhpName, leading \ optional), resolved as nikic/PHP-Parser's NameResolver (the language standard). Compared without the leading backslash; classes and functions case-insensitively. Dotted names and Rust paths refused.
  • rust: crate paths (std::env::var, crate::m::f). A read's head goes through the file's use and extern crate items; self::/super:: normalise to crate:: (see Rust). prefix matches at a ::. Refused: dotted or PHP names, listed paths starting self::/super::, imported.
  • TypeScript: import resolution out of scope.
Limits
Names resolve at the reference; Ruff resolves some checks from another site (a parameter annotation from the call site, B008 defaults from the body).
Ruff's B022: the callee resolves to contextlib.suppress through any import form or alias.
ruff/B022.json /checks/0 · Ruff conformance port
{
  "id": "useless-contextlib-suppress",
  "forbid": {
    "ast": {
      "language": "python",
      "utils": {
        "suppress": {
          "any": [
            {
              "any": [{ "kind": "identifier" }, { "kind": "attribute" }],
              "resolves": { "import": ["contextlib.suppress"] }
            },
            {
              "kind": "parenthesized_expression",
              "has": { "stopBy": "neighbor", "matches": "suppress" }
            }
          ]
        }
      },
      "rule": {
        "kind": "call",
        "has": {
          "stopBy": "neighbor",
          "kind": "argument_list",
          "not": {
            "has": {
              "stopBy": "neighbor",
              "not": {
                "any": [
                  { "kind": "keyword_argument" },
                  { "kind": "dictionary_splat" },
                  { "kind": "comment" },
                  { "regex": "^[(),]$" }
                ]
              }
            }
          },
          "follows": { "stopBy": "neighbor", "matches": "suppress" }
        }
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "B022",
    "message": "No arguments passed to `contextlib.suppress`. No exceptions will be suppressed and therefore this context manager is redundant"
  }
}
1 more example

Before and after: BLE001's logger candidate

Before, "has a qualified name" was approximated by enumerating the file's import forms. After, import: true is exactly Ruff's test:

The object of $L.exception($$$).
{
  "any": [
    { "resolves": { "import": ["logging", "flask.current_app.logger"] } },
    { "regex": "^_?log", "not": { "resolves": { "import": true } } }
  ]
}

imported · relative · seenBefore #

An import site: a dotted_name an import statement imports, whose qualified name is listed.

In the grammar: Resolves

  • import a.b and import a.b as c import a.b. In from m import x as y, m names m and x names m.x. from __future__ import x imports __future__.x.
  • Reads no bindings and builds no scope table.
  • relative: "text": a relative import is its text without dots (from .pdb import set_trace imports pdb.set_trace). Default "never": relative modules name nothing.
  • seenBefore: true (top-level module names only, alone beside imported): any node Ruff's traversal visits after an absolute import of a listed module (seen_module).
Details: notes, shape, syntax errors, languages, limits
Notes
relative: "text" matches Ruff's T100, F404, F407. Ruff's traversal: the module body in order, then function bodies level by level, then lambda bodies, type parameters and type values.
Shape
"imported": [Dotted | PhpName]¹⁻¹⁰²⁴, "relative"?: "never" | "text", "seenBefore"?: true
Syntax errors
Positive imported-only: sound (a statement broken before the name imports nothing) · negated, beside global/import/this, or with seenBefore: source-incomplete
Languages
python. php and blade support imported (with prefix): a use clause's (or @use's) name, alias aside, matches what it imports (a group's prefix plus the clause's name), compared as import compares PHP names. They refuse relative (use namespace\A; does not parse) and seenBefore (a use only aliases).
Limits
No level-aware reading of relative import sites. seenBefore places deferred annotations, string annotations and a stub's class bases where they are written.
Ruff's F407: __future__ imports, relative ones by text.
ruff/F407.json /checks/0 · Ruff conformance port
{
  "id": "future-feature-not-defined",
  "forbid": {
    "ast": {
      "language": "python",
      "utils": {
        "known-feature": {
          "kind": "dotted_name",
          "regex": "^(nested_scopes|generators|division|absolute_import|with_statement|print_function|unicode_literals|barry_as_FLUFL|generator_stop|annotations)$"
        },
        "future-import": {
          "any": [
            { "kind": "future_import_statement" },
            {
              "kind": "import_from_statement",
              "has": {
                "stopBy": "neighbor",
                "kind": "relative_import",
                "has": {
                  "stopBy": "neighbor",
                  "kind": "dotted_name",
                  "resolves": { "imported": ["__future__"], "relative": "text" }
                }
              }
            }
          ]
        }
      },
      "rule": {
        "any": [
          { "kind": "dotted_name", "not": { "matches": "known-feature" }, "pattern": "$NAME" },
          {
            "kind": "aliased_import",
            "has": {
              "stopBy": "neighbor",
              "kind": "dotted_name",
              "not": { "matches": "known-feature" },
              "pattern": "$NAME"
            }
          },
          { "kind": "wildcard_import", "pattern": "$NAME" }
        ],
        "inside": { "stopBy": "neighbor", "matches": "future-import" }
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "F407",
    "message": "Future feature `$NAME` is not defined"
  }
}
1 more example
Ruff's ASYNC109 (trio and anyio): gated by seenBefore and by when.pythonVersion; the define shares the override utility with the document's other clause.
ruff/ASYNC109.json /checks/0 + /checks/2 · Ruff conformance port
[
  {
    "id": "shared",
    "define": {
      "language": "python",
      "utils": {
        "override": {
          "any": [{ "kind": "identifier" }, { "kind": "attribute" }],
          "resolves": {
            "import": ["typing.override", "_typeshed.override", "typing_extensions.override"]
          }
        }
      }
    }
  },
  {
    "id": "async-function-with-timeout-trio-anyio",
    "forbid": {
      "ast": {
        "language": "python",
        "when": { "pythonVersion": { "max": "3.10" } },
        "rule": {
          "all": [
            {
              "any": [
                {
                  "kind": "identifier",
                  "regex": "^timeout$",
                  "inside": { "stopBy": "neighbor", "kind": "parameters" }
                },
                {
                  "any": [
                    { "kind": "default_parameter" },
                    { "kind": "typed_parameter" },
                    { "kind": "typed_default_parameter" }
                  ],
                  "inside": { "stopBy": "neighbor", "kind": "parameters" },
                  "has": {
                    "stopBy": "neighbor",
                    "kind": "identifier",
                    "regex": "^timeout$",
                    "not": { "follows": { "stopBy": "end", "regex": "^[=:]$" } }
                  }
                }
              ],
              "inside": {
                "stopBy": "neighbor",
                "kind": "parameters",
                "inside": {
                  "stopBy": "neighbor",
                  "kind": "function_definition",
                  "has": { "stopBy": "neighbor", "regex": "^async$" },
                  "not": {
                    "inside": {
                      "stopBy": "neighbor",
                      "kind": "decorated_definition",
                      "has": {
                        "stopBy": "neighbor",
                        "kind": "decorator",
                        "has": { "stopBy": "neighbor", "matches": "override" }
                      }
                    }
                  }
                }
              }
            },
            { "resolves": { "imported": ["trio", "anyio"], "seenBefore": true } }
          ]
        }
      }
    },
    "violation": {
      "anchor": "witness.span",
      "code": "ASYNC109",
      "message": "Async function definition with a `timeout` parameter"
    }
  }
]

this · sloppyFunctions #

The node is a this whose value is the global object.

In the grammar: Resolves

  • "global" (ESLint's no-eval): a top-level this in a script or commonjs file, outside functions (arrows aside), field initializers, static blocks and namespaces.
  • "globalScope" (ESLint's no-alert): a this in a script's eslint-scope global scope. Never in a module or CommonJS file.
  • sloppyFunctions: true (with "global"): also a this owned by a non-strict ordinary function.
Details: notes, shape, languages, limits
Notes
  • "global" under globalReturn: only where the top level is not strict.
  • "globalScope": not enclosed by a function (arrows included), class, static block, namespace, enum, catch clause or with; from ES2015, nor by a block, switch or loop declaring let/const.
  • Strictness is eslint-scope's isStrict: ES5+, modules, impliedStrict, classes, 'use strict' prologues.
Shape
"this": "global" | "globalScope", "sloppyFunctions"?: true (needs languageOptions)
Languages
typescript
Limits
Which calls bind another this (methods by property, bind/call/apply, callbacks with a thisArg) is the port's heuristic (ESLint's isDefaultThisBinding).
no-eval's this term: the this of this.eval, in a script or a sloppy function.
{
  "language": "typescript",
  "languageOptions": { "sourceType": "script" },
  "rule": {
    "kind": "this",
    "resolves": { "this": "global", "sloppyFunctions": true },
    "inside": {
      "kind": "member_expression",
      "stopBy": "neighbor",
      "has": { "kind": "property_identifier", "regex": "^eval$" }
    }
  }
}

binding #

A reference to a file-local binding that passes the filters and sub-rules.

In the grammar: Resolves, ResolvesBinding, Binds

  • Reads the binding the reference resolves to (Python's scope-table pick, TypeScript's lexical declaration); never a global, builtin or Ruff's global placeholder. binding: {}: the name is bound.
  • Sub-rules: site (the binding construct), value (its value), annotation (without :), occurrence (TypeScript: any definition name or reference).
  • Filters are binds's. only: true: the binding shadows no earlier one of its name in its scope (Ruff's only_binding).
  • stopAtGlobal: true (Python): where the scope declares the name global, read that scope's global binding instead of the module's. destructuredValue: false (TypeScript): destructured names have no value.
  • Aliasing chains are a recursive utility: { any: [TARGET, { resolves: { binding: { value: { matches: SELF } } } }] }. A chain that loops back to a node it is matching fails.
Details: notes, shape, composition, captures, unknown, syntax errors, languages, limits
Notes
  • Python reads the latest binding the read sees; function bodies see the whole module; global reads continue in the module.
  • site: Python's assignment, for, with/except pattern, import item, def/class, parameter node, global statement, match pattern…; TypeScript's declarator, parameter, catch clause, import clause or specifier, declaration.
  • value: a plain or annotated assignment's value, a walrus's value, an unpacked name's element where arities agree; TypeScript: a single-definition declarator nothing writes, or the literal element a destructured name takes.
  • only ignores form; TypeScript: the name's one declaration in its scope. stopAtGlobal is Ruff's lookup_symbol; destructuredValue, ESLint's ReferenceTracker.
Shape
"binding": { "site"?, "value"?, "annotation"?, "occurrence"?: AstRule, filters: "form"?, "scope"?, "used"?, "referenced"?, "nestedUse"?, "exported"?, "latest"?, "outer"?, "builtin"?, "directives"?, "bindings"?, lookup: "stopAtGlobal"?: Bool, "only"?: Bool, "destructuredValue"?: Bool }
Composition
Alone in its resolves (with write). Runs in the ordered group before count and compare, which read what its sub-rules bind.
Captures
Sub-rules share the caller's environment, as has does, and may bind.
Unknown
The usage filters are three-valued (see binds).
Syntax errors
source-incomplete
Languages
python typescript php blade, per key:
  • stopAtGlobal: Python. php/blade always read as true; false refused.
  • latest, outer: Python, php, blade. PHP flow: some path carries the binding to its scope's end; it writes the global or enclosing variable via global, a by-reference use or =& $GLOBALS['x'] on every path (unknown on some).
  • destructuredValue, occurrence: TypeScript, php, blade. PHP: the array-literal element a list() target's key selects (also read by value.follow); every $x naming the variable.
  • only (php, blade): the variable's one binding in its scope.
  • builtin, directives: TypeScript.
  • php/blade refuse write; Psalm reports each unused assignment, which binds: { used: false } matches.
Limits
Flow-insensitive: after x = 1 then if c: x = [2], a later read sees [2]. ESLint's ReferenceTracker follows every write; a binding value models only the effectively constant case.
ESLint's no-obj-calls: a global object reached through a constant alias (value), not through a destructured one (destructuredValue: false).
eslint/eslint/no-obj-calls.json /checks/0 · ESLint conformance port
{
  "id": "call",
  "forbid": {
    "ast": {
      "language": "typescript",
      "languageOptions": {},
      "rule": {
        "all": [
          { "any": [{ "kind": "call_expression" }, { "kind": "new_expression" }] },
          { "not": { "has": { "kind": "template_string" } } },
          { "has": { "matches": "callee" } }
        ]
      },
      "utils": {
        "global-obj": {
          "any": [
            {
              "kind": "identifier",
              "resolves": {
                "global": ["globalThis", "window", "self", "global"],
                "directives": "eslint"
              }
            },
            { "kind": "parenthesized_expression", "has": { "matches": "global-obj" } },
            {
              "kind": "identifier",
              "resolves": {
                "binding": { "destructuredValue": false, "value": { "matches": "global-obj" } }
              }
            }
          ]
        },
        "callee": {
          "any": [
            {
              "kind": "identifier",
              "resolves": {
                "global": ["Atomics", "JSON", "Math", "Reflect", "Intl", "Temporal"],
                "directives": "eslint"
              },
              "pattern": "$NAME"
            },
            {
              "kind": "member_expression",
              "all": [
                {
                  "has": {
                    "kind": "property_identifier",
                    "regex": "^(Atomics|JSON|Math|Reflect|Intl|Temporal)$",
                    "pattern": "$NAME"
                  }
                },
                { "has": { "matches": "global-obj" } }
              ]
            },
            {
              "kind": "subscript_expression",
              "all": [
                {
                  "has": {
                    "any": [
                      { "kind": "string" },
                      {
                        "kind": "template_string",
                        "not": { "has": { "kind": "template_substitution" } }
                      }
                    ],
                    "has": {
                      "kind": "string_fragment",
                      "regex": "^(Atomics|JSON|Math|Reflect|Intl|Temporal)$",
                      "pattern": "$NAME"
                    }
                  }
                },
                { "has": { "matches": "global-obj" } }
              ]
            },
            { "kind": "parenthesized_expression", "has": { "matches": "callee" } }
          ]
        }
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_OBJ_CALLS",
    "message": "'$NAME' is not a function."
  }
}
1 more example
Ruff's ASYNC220, utility p-wait-arg: os.P_WAIT directly, through a binding's value, or through a site, read with stopAtGlobal.
ruff/ASYNC220.json /checks/0/forbid/ast/utils/p-wait-arg · Ruff conformance port
{
  "any": [
    { "matches": "p-wait-value" },
    {
      "kind": "identifier",
      "resolves": {
        "binding": { "stopAtGlobal": true, "value": { "matches": "p-wait-value" } }
      }
    },
    {
      "kind": "identifier",
      "pattern": "$PW",
      "resolves": {
        "binding": {
          "stopAtGlobal": true,
          "site": {
            "kind": "assignment",
            "all": [
              {
                "has": {
                  "field": "left",
                  "has": {
                    "stopBy": "neighbor",
                    "kind": "identifier",
                    "pattern": "$UT",
                    "compare": { "left": "UT", "op": "eq", "right": "PW", "text": "tokens" },
                    "not": {
                      "precedes": {
                        "stopBy": "end",
                        "kind": "identifier",
                        "pattern": "$UU",
                        "compare": { "left": "UU", "op": "eq", "right": "PW", "text": "tokens" }
                      }
                    }
                  }
                }
              },
              {
                "has": {
                  "field": "right",
                  "has": { "stopBy": "neighbor", "matches": "p-wait-value", "pattern": "$UV" }
                }
              },
              { "compare": { "left": "UV", "op": "eq", "right": "UT", "text": "index" } }
            ]
          }
        }
      }
    },
    {
      "kind": "parenthesized_expression",
      "has": { "stopBy": "neighbor", "matches": "p-wait-arg" }
    }
  ]
}

Before and after: ports in the Ruff suite

PortNew keySizeRuff fixtures
B008only42.5 → 21.0 KB10/0/0 PASS both
ASYNC220syntactic, stopAtGlobal, node9.5 → 6.6 KB8/0/0 PASS both; probes clean
SIM113unpacking, node19.6 → 14.5 KB7/0/1 → 8/0/0 PASS
ASYNC230unpacking (no JSON change)7.3 KB11/0/1 → 12/0/0 PASS

unresolved #

The node is a reference with no binding: Ruff's F821, ESLint's no-undef.

In the grammar: Resolves

  • No file-local binding (or one an unconditional Python del unbound), builtin or declared global.
  • true: no visible star import may bind the name. "star" (Python): one may (F405).
  • On a string entry of a module __all__, reads the exported name (F822).
Details: notes, shape, composition, syntax errors, languages
Notes
Python counts pythonVersion's builtins and implicit names (__module__, __qualname__, __class__ where a cell holds it, __path__ in a package's __init__). TypeScript counts languageOptions' declared globals and, with directives, /* global */ comments.
Shape
"unresolved": true | "star", "directives"?: "eslint"
Composition
Alone in its resolves, directives aside.
Syntax errors
source-incomplete
Languages
python typescript sql; "star" Python only. sql's true: a SELECT qualifier no relation or target of its query block or an enclosing one names (SQLFluff's RF01). A CTE is named only through a relation selecting from it.
Ruff's F822: an __all__ entry naming nothing.
ruff/F822.json /checks/0 · Ruff conformance port
{
  "id": "undefined-export",
  "forbid": {
    "ast": {
      "language": "python",
      "rule": {
        "any": [{ "kind": "string" }, { "kind": "concatenated_string" }],
        "pattern": "$S",
        "resolves": { "unresolved": true }
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "F822",
    "message": "Undefined name $S in `__all__`"
  }
}
2 more examples
ESLint's no-undef (typeof: true) with /* global */ comments.
eslint/eslint/no-undef/typeof-true.json /checks/0 · ESLint conformance port
{
  "id": "undef",
  "forbid": {
    "ast": {
      "language": "typescript",
      "languageOptions": { "jsxPragma": null },
      "rule": {
        "pattern": "$NAME",
        "any": [
          { "kind": "identifier" },
          { "kind": "type_identifier" },
          { "kind": "property_identifier" },
          { "kind": "shorthand_property_identifier_pattern" },
          { "kind": "shorthand_property_identifier" },
          { "kind": "undefined" }
        ],
        "resolves": { "unresolved": true, "directives": "eslint" }
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_UNDEF",
    "message": "'$NAME' is not defined."
  }
}
Ruff's F405: "star", in code and inside string annotations (parse: annotation), one violation per name (each).
ruff/F405.json /checks/0 · Ruff conformance port
{
  "id": "undefined-local-with-import-star-usage",
  "forbid": {
    "ast": {
      "language": "python",
      "utils": {
        "star-in-string": {
          "any": [{ "kind": "string" }, { "kind": "concatenated_string" }],
          "parse": {
            "grammar": "annotation",
            "has": {
              "stopBy": "end",
              "any": [
                {
                  "kind": "identifier",
                  "resolves": { "unresolved": "star" },
                  "capture": "NAME"
                },
                { "matches": "star-in-string" }
              ]
            }
          }
        }
      },
      "rule": {
        "any": [
          {
            "any": [{ "kind": "identifier" }, { "kind": "string" }],
            "pattern": "$NAME",
            "resolves": { "unresolved": "star" }
          },
          { "matches": "star-in-string" }
        ]
      }
    }
  },
  "violation": {
    "anchor": "capture.NAME",
    "code": "F405",
    "message": "`$NAME` may be undefined, or defined from star imports",
    "each": "NAME"
  }
}

beforeLocal · write #

beforeLocal (Python, F823): a read before the function binds the name. write: "last" (TypeScript): where ESLint's no-unused-vars reports a variable.

In the grammar: Resolves

  • beforeLocal: a function's first load of a name that a later binding in the same function shadows (Ruff's undefined_local).
  • write: "last": the last write reference from the variable's own function scope, else the first definition's name. A binding beside it filters the variable.
Details: notes, shape, syntax errors, languages
Notes
  • beforeLocal skips loads in a nested lambda or comprehension. A builtin counts as shadowed where the target version has it. References in a nested class's bases or a def's annotations are in Ruff's type scope and never count.
  • write includes a declarator's initializer, a for … in/of declaration and each default write at the definition. Holds on a reference or a name node.
Shape
"beforeLocal": true (alone in its resolves) "write": "last", "binding"?: ResolvesBinding
Syntax errors
source-incomplete
Languages
beforeLocal python · write typescript
Ruff's F823, whole.
ruff/F823.json /checks/0 · Ruff conformance port
{
  "id": "undefined-local",
  "forbid": {
    "ast": {
      "language": "python",
      "rule": { "kind": "identifier", "pattern": "$N", "resolves": { "beforeLocal": true } }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "F823",
    "message": "Local variable `$N` referenced before assignment"
  }
}
1 more example
ESLint's no-unused-vars (args: "all", vars: "local"), its parameter term: an unused parameter reported where ESLint reports it.
eslint/eslint/no-unused-vars/args-all-vars-local.json /checks/0 · ESLint conformance port
{
  "id": "unused-args",
  "forbid": {
    "ast": {
      "language": "typescript",
      "languageOptions": { "jsxPragma": null },
      "rule": {
        "pattern": "$NAME",
        "any": [
          { "kind": "identifier" },
          { "kind": "type_identifier" },
          { "kind": "property_identifier" },
          { "kind": "shorthand_property_identifier_pattern" },
          { "kind": "shorthand_property_identifier" },
          { "kind": "undefined" }
        ],
        "all": [
          {
            "all": [
              {
                "resolves": {
                  "write": "last",
                  "binding": {
                    "scope": [
                      "module",
                      "function",
                      "class",
                      "block",
                      "namespace",
                      "typeParameters"
                    ],
                    "directives": "eslint",
                    "form": "parameter",
                    "used": false,
                    "exported": false
                  }
                }
              }
            ]
          }
        ]
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_UNUSED_VARS",
    "message": "'$NAME' is defined but never used."
  }
}

binds #

A binding's name node, where a linter reports it, whose binding passes the filters and sub-rules.

In the grammar: AstRule, ResolvesBinding, Binds, Redefines

  • Name node: Python's identifier, import alias, or unaliased import's dotted_name (os.path); TypeScript's declared identifier, type name, enum member, shorthand pattern or private name (#a).
  • form: the binding construct. scope: the kind of scope holding it. Values in Notes.
  • used: some read resolves to it (or the same-scope binding it shadows). referenced: any reference, writes included. nestedUse: a read from another scope.
  • exported: exported or re-exported. latest/outer (Python): the live binding; made under global/nonlocal. bindings: how many bindings of the name its scope holds.
  • redefines: the binding shadows (Python) or redeclares (TypeScript) one that passes; with sameScope, sameImport, every.
Details: notes, shape, composition, captures, unknown, syntax errors, languages, limits
Notes
  • form: assignment, annotated, annotation (bare x: T), augmented, named, for, with, except, import, definition, parameter, global, match, typeParameter, typeAlias, implicit, deletion, field, method, accessor (the last three for private names); SQL: relation, cte, target.
  • scope: Python's and eslint-scope's names, SQL's query; each language refuses the other's.
  • used follows Ruff (reads copied) and ESLint's isUsedVariable with typescript-eslint's marks. referenced inherits nothing.
  • exported: Python, an explicit re-export or __all__ entry; TypeScript, an export or (with directives) an /* exported */ comment.
  • builtin (TypeScript): a configured global's variable in a script. bindings counts inside the capture within.
  • redefines: Python, the one before it on the same fork or an enclosing scope's passes, with sameScope and sameImport; TypeScript, an earlier definition passes (ESLint's no-redeclare), or with every each other definition.
Shape
"binds": { "form"?, "scope"?, "used"?, "referenced"?, "nestedUse"?, "exported"?, "latest"?, "outer"?, "builtin"?, "directives"?, "bindings"?: { "within"?: NAME, "min"?, "max"? }, "site"?, "value"?, "annotation"?, "occurrence"?: AstRule, "redefines"?: Redefines }
Composition
In the ordered group. binds starts from the binding, so it has no only or stopAtGlobal.
Captures
Sub-rules (site, value, annotation, occurrence, the shadowed binding's) share the environment and may bind. bindings.within reads a capture.
Unknown
used, referenced, nestedUse, exported are three-valued: where a read may be unseen, an unused binding's used is unknown, so neither used: false nor its negation matches. That covers:
  • Python: an assignment, walrus, with name, match capture, type alias or parameter in a function calling locals(); a module binding under an __all__ whose value may list names no string spells.
  • TypeScript: bindings a direct eval(...) or with can see; a private name's nestedUse.
Syntax errors
source-incomplete
Languages
python typescript php blade; per filter in Language support. php/blade redefines: some binding of the variable reaching the site (the one overwritten; itself on a loop's next iteration) passes, or with every each does and one reaches. They refuse sameImport, exported, builtin, directives (PHP exports no variables; a superglobal is no file's binding).
Limits
The usage index costs about 160 ms over the repository's 18 MB of Python (beyond the table's 100 ms) and about 65 ms over 10.5 MB of TypeScript, once per file revision.
Ruff's F402: a loop variable (form, site) that redefines an import.
ruff/F402.json /checks/0 · Ruff conformance port
{
  "id": "import-shadowed-by-loop-var",
  "forbid": {
    "ast": {
      "language": "python",
      "rule": {
        "kind": "identifier",
        "pattern": "$NAME",
        "binds": {
          "form": "for",
          "site": { "kind": "for_statement" },
          "redefines": { "form": "import" }
        }
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "F402",
    "message": "Import `$NAME` shadowed by loop variable"
  }
}
5 more examples
Ruff's F401: an unused, unexported, live import; Ruff's allowed-unused-imports setting is a document option that a $option reference splices in.
ruff/F401.json /checks/0 + /checks/1 · Ruff conformance port
[
  {
    "id": "options",
    "define": {
      "options": {
        "pyflakes.allowed-unused-imports": {
          "type": "array",
          "items": "string",
          "default": [],
          "description": "Modules whose imports may be unused, with their submodules and members."
        }
      }
    }
  },
  {
    "id": "unused-import",
    "forbid": {
      "ast": {
        "language": "python",
        "rule": {
          "any": [{ "kind": "dotted_name" }, { "kind": "identifier" }],
          "pattern": "$NAME",
          "binds": {
            "form": "import",
            "scope": ["module", "function"],
            "latest": true,
            "used": false,
            "exported": false,
            "outer": false
          },
          "not": {
            "any": [
              {
                "inside": {
                  "stopBy": "end",
                  "any": [
                    { "kind": "future_import_statement" },
                    {
                      "kind": "import_from_statement",
                      "has": {
                        "stopBy": "neighbor",
                        "kind": "relative_import",
                        "has": {
                          "stopBy": "neighbor",
                          "kind": "dotted_name",
                          "regex": "^__future__$"
                        }
                      }
                    }
                  ]
                }
              },
              {
                "$option": "pyflakes.allowed-unused-imports",
                "cases": { "[]": [] },
                "default": [
                  {
                    "kind": "dotted_name",
                    "resolves": {
                      "imported": { "$option": "pyflakes.allowed-unused-imports" },
                      "prefix": true
                    }
                  },
                  {
                    "kind": "identifier",
                    "inside": {
                      "stopBy": "neighbor",
                      "kind": "aliased_import",
                      "has": {
                        "stopBy": "neighbor",
                        "kind": "dotted_name",
                        "resolves": {
                          "imported": { "$option": "pyflakes.allowed-unused-imports" },
                          "prefix": true
                        }
                      }
                    }
                  }
                ],
                "spread": true
              }
            ]
          }
        }
      }
    },
    "violation": {
      "anchor": "witness.span",
      "code": "F401",
      "message": "`$NAME` imported but unused"
    }
  }
]
ESLint's no-redeclare (builtinGlobals): builtin.
eslint/eslint/no-redeclare.json /checks/1 · ESLint conformance port
{
  "id": "redeclared-builtin",
  "forbid": {
    "ast": {
      "language": "typescript",
      "languageOptions": { "jsxPragma": null },
      "rule": {
        "pattern": "$NAME",
        "any": [
          { "kind": "identifier" },
          { "kind": "type_identifier" },
          { "kind": "property_identifier" },
          { "kind": "shorthand_property_identifier_pattern" },
          { "kind": "shorthand_property_identifier" },
          { "kind": "undefined" }
        ],
        "all": [
          { "binds": { "scope": ["global", "module", "function", "block"], "builtin": true } }
        ]
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_REDECLARE",
    "message": "'$NAME' is already defined as a built-in global variable."
  }
}

Before and after: the Pyflakes usedness ports

PortBeforeAfter
F841structural walk over assignments, utilities that climb scopes and search later reads by text; fixture-fit, 148 KBbinds with forms, scope: ["function", "lambda"], latest, outer: false, used: false, plus Ruff's exclusions
F401per-name import enumerations and read searches; BLOCKED on a grammar gapone binds term (above)
F811113 KB of pairwise structural matchingbinds.redefines, one term per pair Ruff's Binding::redefines allows
F82151 KB of scope climbingresolves: { unresolved: true }
F842a structural scope walk, 44 KBbinds with form: "annotation" (below)
Ruff's F842 as committed.
ruff/F842.json /checks/0 · Ruff conformance port
{
  "id": "unused-annotation",
  "forbid": {
    "ast": {
      "language": "python",
      "rule": {
        "kind": "identifier",
        "pattern": "$N",
        "binds": {
          "form": "annotation",
          "scope": ["function", "lambda"],
          "latest": true,
          "used": false
        },
        "not": { "regex": "^(?:_+|_+[a-zA-Z0-9_]*[a-zA-Z0-9])$" }
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "F842",
    "message": "Local variable `$N` is annotated but never used"
  }
}
SIM113's counter: one augmented binding in the loop captured as $LOOP.
{
  "kind": "identifier",
  "inside": { "kind": "for_statement", "pattern": "$LOOP", "stopBy": "end" },
  "binds": { "form": "augmented", "bindings": { "within": "LOOP", "max": 1 } }
}
no-unused-private-class-members as one binds term (367 bytes where the structural port had 2,111): reported at a member's last declaration. MEMBERS spelled out.
{
  "kind": "private_property_identifier",
  "pattern": "$N",
  "binds": { "used": false },
  "not": {
    "inside": {
      "stopBy": "neighbor",
      "any": [{ "kind": "public_field_definition" }, { "kind": "method_definition" }],
      "precedes": {
        "stopBy": "end",
        "any": [{ "kind": "public_field_definition" }, { "kind": "method_definition" }],
        "has": { "stopBy": "neighbor", "kind": "private_property_identifier", "pattern": "$N" }
      }
    }
  }
}

arguments #

A call's arguments bound to a signature's named slots, as the language binds them.

In the grammar: AstRule, Arguments

  • Holds on a Python call, TypeScript call_expression or new_expression; false elsewhere.
  • Positional arguments fill positional slots in order; Python keywords fill their named slot. Extra arguments are ignored; an omitted slot is absent.
  • bind: the slot's argument becomes that single-node capture; an unpassed slot fails. absent: slots that must not be passed.
  • mode: "syntactic" (Python, PHP): Ruff's find_argument/find_keyword: the first keyword of the name, else the positional argument at its position before the first starred one.
  • mode: "estree" (TypeScript): ESLint's node.arguments[i]; every argument, spreads included, is a position. Parentheses unwrapped; a tagged template is no call.
Details: notes, shape, composition, captures, unknown, syntax errors, languages, limits
Notes
  • Python: **{'k': v} with string keys counts as keywords. / ends positional-only slots; * starts keyword-only ones. Defaulted slots need no mark.
  • syntactic: **kw passes nothing. PHP reads nikic/PHP-Parser's args as written (named arguments as keywords, unpacked as starred, parentheses unwrapped): a call PHP refuses (positional after named) is still read; f(...) is no call.
Shape
"arguments": { "signature": [Ident | "/" | "*"], "bind"?: { NAME: Slot }, "absent"?: [Slot]⁺, "mode"?: "language" | "syntactic" | "estree" }
Composition
An atomic term (after pattern, kind, regex), often with constraints on the bound capture.
Captures
Binds the bind names.
Unknown
In language mode: a slot a starred argument (*args, ...args) or **kw may fill, or one passed twice, and any verdict depending on it (no violation, negated or not). syntactic and estree: never, except a PHP argument list PHP cannot parse (a syntax error, &$x).
Syntax errors
sound (reads the call's own argument list)
Languages
python typescript php blade sql. /, *: Python; syntactic: Python, PHP, Blade; estree: TypeScript. sql binds as PostgreSQL 18.6's parser: by position, then name (name => v, name := v).
Limits
No variadic slot. estree has no "every argument equals the parameter at its index" form (use compare text: index). Bandit counts **kw as passing nothing where a language-mode port is unknown.
ESLint's radix, missing parameters: absent on the first slot.
eslint/eslint/radix.json /checks/0 · ESLint conformance port
{
  "id": "missing-parameters",
  "forbid": {
    "ast": {
      "language": "typescript",
      "languageOptions": {},
      "rule": {
        "kind": "call_expression",
        "has": { "matches": "callee" },
        "arguments": { "signature": ["string", "radix"], "absent": ["string"] }
      },
      "utils": {
        "callee": {
          "any": [
            {
              "any": [
                {
                  "kind": "identifier",
                  "resolves": { "global": ["parseInt"], "directives": "eslint" }
                },
                {
                  "kind": "member_expression",
                  "all": [
                    { "has": { "kind": "property_identifier", "regex": "^parseInt$" } },
                    { "has": { "matches": "number-obj" } }
                  ]
                },
                {
                  "kind": "subscript_expression",
                  "all": [
                    {
                      "has": {
                        "any": [
                          { "kind": "string" },
                          {
                            "kind": "template_string",
                            "not": { "has": { "kind": "template_substitution" } }
                          }
                        ],
                        "value": { "eq": "parseInt" }
                      }
                    },
                    { "has": { "matches": "number-obj" } }
                  ]
                }
              ]
            },
            { "kind": "parenthesized_expression", "has": { "matches": "callee" } }
          ]
        },
        "number-obj": {
          "any": [
            {
              "kind": "identifier",
              "resolves": { "global": ["Number"], "directives": "eslint" }
            },
            { "kind": "parenthesized_expression", "has": { "matches": "number-obj" } }
          ]
        }
      }
    }
  },
  "violation": { "anchor": "witness.span", "code": "RADIX", "message": "Missing parameters." }
}
2 more examples
requests.get without a timeout (Bandit's B113; keyword-only after *).
{
  "pattern": "requests.get($$$)",
  "arguments": { "signature": ["url", "params", "*", "timeout"], "absent": ["timeout"] }
}
no-useless-constructor's pass-through clause: estree binds a of super((a)), and name compares it with the parameter.
{
  "kind": "call_expression",
  "has": { "kind": "super", "stopBy": "neighbor" },
  "arguments": {
    "signature": ["a", "b"],
    "bind": { "A": "a" },
    "absent": ["b"],
    "mode": "estree"
  },
  "inside": {
    "kind": "method_definition",
    "stopBy": "end",
    "has": {
      "kind": "formal_parameters",
      "has": { "kind": "required_parameter", "has": { "kind": "identifier", "pattern": "$P" } }
    }
  },
  "compare": { "left": "A", "op": "eq", "right": "P", "text": "name" }
}

Sub-grammars

A string literal or comment can be a small language of its own: a printf-style format, a str.format template, a type in a string, an ESLint directive.

parse decodes the literal, parses it with a sub-grammar, and matches a rule of that grammar's kinds on the tree's root. Each parsed node maps back to its source bytes, so a capture inside the tree can anchor a violation, carry a fix range, or be enumerated by each.

Host source: a Python string literal, bytes 0–12 " % ( n a \ x 6 d e ) s " 0123456789101112 escape → whole escape decode (escapes, concatenation) % ( n a m e ) s value "%(name)s" as a document of its own format placeholder mapping_key "name" conversion "s" host bytes 1–11 host bytes 1–11 Rule, and what the violation reads "parse": { "grammar": "percentFormat", "has": { "kind": "mapping_key", "capture": "K", "stopBy": "end" } } $K in a message → name (the decoded text) anchor capture.K, fix range → host bytes 3–9 (n … e, the escape included) each: "K" → one violation per placeholder, ids apart a host compare reads K as text (source, unquoted, name)

Source mapping of a parsed string (literal/offsets.rs). Each decoded character keeps its source span: an escape maps to the whole escape, \r\n to both bytes, a line continuation to nothing; implicit concatenation chains the parts. A node spans its first character's start to its last character's end. An annotation tree maps exactly, by a shift, for a simple string (one part, its source its value), and every node to the whole string otherwise. An eslintDirective tree maps each node to its exact bytes in the comment.

parse #

Parses a literal's decoded value with a sub-grammar and matches a rule of that grammar on the tree's root.

In the grammar: AstRule, Parse

  • Grammars: Python percentFormat, braceFormat, annotation; PHP printfFormat, scanfFormat, phpDoc; Rust rustFormat; TypeScript eslintDirective. Their kinds: see Notes.
  • The inner rule runs on the tree's root, over the grammar's own kinds and values.
  • A literal a text grammar refuses is a format root holding one error node, valued by Ruff's message (PHP's ValueError message for printfFormat, rustc's for rustFormat).
Details: notes, shape, composition, captures, unknown, syntax errors, languages, limits
Notes
  • percentFormat (Ruff's cformat, F50x): format holds literal and placeholder. A placeholder holds mapping_key, flags, width, precision (each with a star), length, conversion; its value is its key, or the index of the positional value it consumes.
  • braceFormat (Ruff's format, F52x): format holds literal and field. A field holds field_name (head auto, index or name, then attribute, element_index; valued by the argument it reads), conversion, format_spec (nested fields, or fill, align, sign, alternate, zero, width, grouping, precision, type, spec_error).
  • printfFormat (PHP 8.5.11's php_formatted_print with every value passed; oracle: the runtime and PHPStan 2.2.16's PrintfFormatParser): format holds literal and placeholder. A placeholder (%[argnum$][flags][width][.precision][l]conversion) holds argnum, flags (-, +, space, 0, ' and its padding byte), width, precision (each with a star for * or *N$), length (l) and conversion. Values are indexes from 0, after the format: a placeholder's, of the value it consumes; a star's, of the value it reads; an argnum's, of the value it names. %% is literal; %5% consumes a value, as in PHP. Values are PHP's (strings are bytes).
  • scanfFormat (PHP 8.5.11's ValidateFormat for sscanf/fscanf with no variable passed; oracle: the runtime with 0–6 variables): format holds literal and placeholder. A placeholder holds suppress (*), argnum, width, length and conversion (a set child for [...]), valued by the index of the variable it assigns (none when suppressed). PHP reads no further than a NUL byte.
  • phpDoc: PHP doc comments (a comment PHP lexes as T_DOC_COMMENT) as PHPStan 2.2.16 reads them through phpstan/phpdoc-parser 2.3.5. Oracle: that parser over 5,899 written comments and every doc comment of laravel/framework and symfony/symfony. Kinds are the parser's AST classes in snake case, without Node:
    • A php_doc root holds php_doc_text and php_doc_tag (valued by its name, @param).
    • A tag holds its value: param_tag_value, var_tag_value, return_tag_value, throws_tag_value, property_tag_value, method_tag_value, template_tag_value, extends_tag_value, implements_tag_value, type_alias_tag_value, the assert_tag_* values, generic_tag_value for any other tag, Doctrine annotations, …. The @phpstan-*, @psalm-* and @phan-* variants hold their base tag's value.
    • Types: identifier_type (valued by its name), union_type, intersection_type, nullable_type, generic_type (key-of, value-of, list… as its base), array_type, array_shape, object_shape, callable_type, conditional_type, offset_access_type and const_type over constant expressions (const_expr_integer, const_expr_string, const_fetch valued Class::NAME, …).
    • name, variable and description leaves carry the names, parameter names and descriptions the text spells.
    • A value the parser refuses is an invalid_tag_value (a type alias's type an invalid_type), valued by the parser's message.
    • Each node maps to its exact source in the comment; nesting past 256 levels is unknown.
  • rustFormat (rustc 1.99.0's rustc_parse_format: templates of format_args! and the macros built on it): format holds literal and field. A field, valued by the argument it names (an explicit index or a name; none for {}), holds auto (zero width), index or name, then its format_spec (valued by its text) with the type of the trait it names (?, x?, e, x, ...). It reads a string_literal or raw_string_literal decoded as rustc decodes it (not a byte or C string); each node maps to its source, an escape to the whole escape. A template rustc refuses is a format root holding one error node valued by rustc's message.
  • annotation: a string Ruff reads as a type definition (an annotation, a type alias's value, a typing generic's slice, cast/TypeVar/NewType… arguments; not inside Literal[...]), parsed as parse_type_annotation does: an expression_statement root with Python's kinds. A string Ruff refuses (F722) is one ERROR node. Each semantic syntax error Ruff's checker reports there (await outside an async function, a walrus rebinding a comprehension variable…) is an ERROR child after the expression, valued by Ruff's message. Its names are forward references in the scope table.
  • eslintDirective (TypeScript comments ESLint reads as directives): a directive root valued by its label; an item per listed name (valued by the name); a global item's setting; a description after --. A // comment holds only the two line directives.
Shape
"parse": { "grammar": "percentFormat" | "braceFormat" | "annotation" | "eslintDirective" | "printfFormat" | "scanfFormat" | "phpDoc" | "rustFormat", ...a rule over the tree }
Composition
Atomic. Inside, a rule may use kind (checked against the grammar's kinds), regex (over decoded text), value (the grammar's values), compare (source, unquoted, value, node), capture, all, any, not, count and the relations (inside stops at the parsed root). Also: host in a text grammar's rule; pattern, matches, resolves (not binding) and a nested parse: annotation in an annotation rule; resolves with global alone in an eslintDirective rule. Other keys, field and nthChild among them, are refused.
Captures
The tree has its own environment. Its captures export to the host as span captures: decoded text plus host range.
Unknown
A literal whose value is not known is unknown: no violation, negated or not. Python: an f-string with a field, a t string, bytes, an escape Python refuses, a value past 64 KiB. PHP: an interpolated string, an escape PHP refuses, bytes that are not UTF-8.
Syntax errors
percentFormat, braceFormat, eslintDirective, printfFormat, scanfFormat, phpDoc, rustFormat sound (the literal or comment alone) · annotation source-incomplete (reads the scope table)
Languages
  • percentFormat, braceFormat, annotation: python, on a string or concatenated_string.
  • eslintDirective: typescript, on a comment.
  • printfFormat, scanfFormat: php and blade, on a string, encapsed_string, heredoc or nowdoc, decoded as PHP decodes it. A quoted string maps each node to its source; a heredoc or nowdoc maps every node to the whole literal.
  • phpDoc: php and blade, on a doc comment; in a template only the embedded PHP's, never Blade's {{-- --}}.
  • rustFormat: rust, on a string_literal or raw_string_literal.
  • A grammar of another family is refused, naming its counterpart (percentFormat on PHP names printfFormat, annotation names phpDoc) or what PHP lacks (str.format templates; types held in strings).
  • ecmaRegex is refused: see Not included.
Limits
parse holds on the literal node only, not through parentheses. braceFormat reads one level of nested fields, as Ruff's FormatSummary. Where Ruff and CPython differ, the grammars are Ruff's. Rules run over host documents only: Ruff's expression rules inside a string annotation (x: "a == None" is E711) need a port's parse. Strings nested in a type definition are read eight levels deep.
Ruff's F501: a % format the grammar refuses (an error node that is not an unsupported character).
ruff/F501.json /checks/0 · Ruff conformance port
{
  "id": "percent-format-invalid-format",
  "forbid": {
    "ast": {
      "language": "python",
      "utils": {
        "plain-string": {
          "any": [
            { "kind": "string", "not": { "regex": "^[A-Za-z]*[fFbBtT][A-Za-z]*['\"]" } },
            {
              "kind": "concatenated_string",
              "not": {
                "has": { "kind": "string", "regex": "^[A-Za-z]*[fFbBtT][A-Za-z]*['\"]" }
              }
            }
          ]
        },
        "format-string": {
          "any": [
            {
              "matches": "plain-string",
              "parse": {
                "grammar": "percentFormat",
                "has": {
                  "kind": "error",
                  "not": { "value": { "regex": "^unsupported format character" } }
                }
              }
            },
            { "kind": "parenthesized_expression", "has": { "matches": "format-string" } }
          ]
        }
      },
      "rule": { "pattern": "$S % $R" },
      "constraints": { "S": { "matches": "format-string" } }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "F501",
    "message": "`%`-format string has invalid format string"
  }
}
3 more examples
Ruff's F521: the same for .format templates.
ruff/F521.json /checks/0 · Ruff conformance port
{
  "id": "string-dot-format-invalid-format",
  "forbid": {
    "ast": {
      "language": "python",
      "utils": {
        "plain-string": {
          "any": [
            { "kind": "string", "not": { "regex": "^[A-Za-z]*[fFbBtT][A-Za-z]*['\"]" } },
            {
              "kind": "concatenated_string",
              "not": {
                "has": { "kind": "string", "regex": "^[A-Za-z]*[fFbBtT][A-Za-z]*['\"]" }
              }
            }
          ]
        },
        "format-string": {
          "any": [
            {
              "matches": "plain-string",
              "parse": { "grammar": "braceFormat", "has": { "kind": "error" } }
            },
            { "kind": "parenthesized_expression", "has": { "matches": "format-string" } }
          ]
        }
      },
      "rule": { "pattern": "$S.format($$$A)" },
      "constraints": { "S": { "matches": "format-string" } }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "F521",
    "message": "`.format` call has invalid format string"
  }
}
ESLint's no-redeclare: a /* global */ item that a var of the script redeclares, anchored inside the comment at capture.N.
eslint/eslint/no-redeclare/builtin-globals-false.json /checks/1 · ESLint conformance port
{
  "id": "global-comment-syntax",
  "forbid": {
    "ast": {
      "language": "typescript",
      "languageOptions": { "jsxPragma": null },
      "rule": {
        "kind": "comment",
        "parse": {
          "grammar": "eslintDirective",
          "value": { "regex": "^globals?$" },
          "has": {
            "kind": "item",
            "capture": "N",
            "not": { "any": [{ "has": { "kind": "setting", "regex": "^off$" } }] }
          }
        },
        "all": [
          {
            "inside": {
              "stopBy": "end",
              "kind": "program",
              "has": {
                "stopBy": "end",
                "kind": "identifier",
                "binds": { "scope": ["global"] },
                "pattern": "$V",
                "compare": { "left": "V", "op": "eq", "right": "N" }
              }
            }
          }
        ]
      }
    }
  },
  "violation": {
    "anchor": "capture.N",
    "code": "NO_REDECLARE",
    "message": "'$N' is already defined by a variable declaration."
  }
}
A @param typed mixed (written for this page; a smoke test runs it stateless and through the workspace): the identifier is the tag value's own type, so array<mixed> does not match, and the violation anchors on the type inside the comment.
{
  "language": "php",
  "rule": {
    "kind": "comment",
    "parse": {
      "grammar": "phpDoc",
      "has": {
        "kind": "php_doc_tag",
        "value": { "eq": "@param" },
        "has": {
          "kind": "param_tag_value",
          "has": { "kind": "identifier_type", "value": { "eq": "mixed" }, "capture": "T" }
        }
      }
    }
  }
}

capture #

Inside parse only: binds the parsed node under a name and exports it to the host rule.

In the grammar: AstRule

  • The exported capture is text with a source span.
  • Messages and fix templates interpolate its decoded text.
  • capture.NAME anchors, fix.range, each and layout gaps read its host range.
  • A host compare reads it as text (source, unquoted); name compares its text with a node's name.
Details: notes, shape, composition, captures, syntax errors, languages
Notes
Its source range sits under a reserved name: a NUL, then the name.
Shape
"capture": NAME
Composition
An atomic term of the tree's rule. Refused outside parse.
Captures
Binds; exported through nested parses and utilities to the host.
Syntax errors
sound
Languages
every grammar.
Each mapping key of a % format, one violation per key (each), anchored inside the string.
{
  "language": "python",
  "rule": {
    "kind": "string",
    "parse": {
      "grammar": "percentFormat",
      "has": {
        "kind": "placeholder",
        "stopBy": "end",
        "has": { "kind": "mapping_key", "capture": "K" }
      }
    }
  }
}

host #

A rule matched on the node of the other document a node stands in: the comment or string a parse holds, or the file's own node behind a node of an embedded program (ast.embedded, embedded).

In the grammar: AstRule

  • In a parsed tree: a rule of the host language, matched on the comment or string the parse holds.
  • On an embedded program's node: a rule over the file's own tree (Blade's or Vue's kinds and patterns), matched on the innermost node covering the node's host span.
  • Environment: the host's as the parse or enclosing embedded began (fresh under ast.embedded), plus the inner rule's captures bound so far.
  • So a host capture compares with an inner one: host: { compare: { left: "HOSTCAP", op: "eq", right: "PROGCAP" } } in the embedded rule.
Details: notes, shape, composition, captures, syntax errors, languages
Notes
  • Inner captures arrive as text plus host range (in a parsed tree) or file span (from a program).
  • It runs as a term of the inner rule, so a relation over the tree's nodes tests the file per candidate: the search passes over b in /*global b, a*/ var a; to a, and each over the item reaches every item the file admits.
  • It nests: a host rule's own parse may hold host.
  • A program node's host span is the file bytes its program range was copied or written from. The host rule may use embedded to reach the program again.
  • Where both apply, the innermost context decides: a parse on a program's node makes host inside it the literal the parse holds.
Shape
"parse": { "grammar": ..., "has": { ..., "capture": "N", "host": AstRule } } { "language": "vue", "embedded": "script-block", "rule": { ..., "host": AstRule } }
Composition
Last among its rule object's terms, after the captures it reads. Refused outside parse and outside a rule over an embedded program, in an annotation tree, and in formatArgs' fields. Utilities and constraints may hold it; rules over both documents reach them.
Captures
Binds nothing the inner rule sees. Reads the inner rule's captures (the tree's or the program's) and the host's as they stood when the parse or enclosing embedded began: not a sibling relation's, and none under ast.embedded.
Syntax errors
positive: as its rule alone · negated (under not or count, or the parse negated) source-incomplete
Languages
percentFormat, braceFormat (Python host), eslintDirective (TypeScript host), printfFormat, scanfFormat and phpDoc (PHP host, and the PHP a Blade template embeds), rustFormat (Rust host). Embedded programs: blade (compiled-view), vue (script-block).
no-redeclare over /* global */ items (compile/parse/host_tests.rs pins the shape): an item is reported where it is a declared global (resolves on the item), where a var or function of the file declares it (host), or where an earlier comment lists it.
{
  "language": "typescript",
  "languageOptions": { "sourceType": "script" },
  "rule": {
    "kind": "comment",
    "parse": {
      "grammar": "eslintDirective",
      "value": { "regex": "^globals?$" },
      "has": {
        "kind": "item",
        "capture": "N",
        "not": { "has": { "kind": "setting", "regex": "^off$" } },
        "any": [
          { "resolves": { "global": true } },
          {
            "host": {
              "inside": {
                "kind": "program",
                "has": {
                  "stopBy": "end",
                  "any": [{ "kind": "variable_declarator" }, { "kind": "function_declaration" }],
                  "has": { "field": "name", "pattern": "$V" },
                  "compare": { "left": "V", "op": "eq", "right": "N", "text": "name" }
                }
              }
            }
          },
          {
            "host": {
              "follows": {
                "stopBy": "end",
                "kind": "comment",
                "parse": {
                  "grammar": "eslintDirective",
                  "value": { "regex": "^globals?$" },
                  "has": {
                    "kind": "item",
                    "capture": "M",
                    "not": { "has": { "kind": "setting", "regex": "^off$" } },
                    "host": { "compare": { "left": "M", "op": "eq", "right": "N" } }
                  }
                }
              }
            }
          }
        ]
      }
    }
  }
}

ast.embedded · embedded #

A file whose language embeds another (a Blade template's PHP, a Vue component's script) is two documents: its own tree, and the program built through each via its registry entry declares (embeds). ast.embedded queries the program; embedded reaches it from a node of the file's own tree.

In the grammar: AstQuery, AstRule

  • ast.embedded names a via of the language's embeds: compiled-view for blade, script-block for vue. Another is refused.
  • Its rule, utils and constraints use the embedded language's kinds, patterns and utilities (PHP's; TypeScript's, TSX's or JavaScript's). A file without that program has no witness.
  • Witnesses, captures and fixes map to the host bytes the nodes stand for. A fix applies only where its range lies in one stretch the program copied from the file.
  • embedded, on a node of the file's own tree: some program node whose host span lies within the node's range matches its rule.
  • host leads back to the innermost node of the file's tree covering a program node. Compare a host capture with a program capture there.
Details: notes, shape, composition, captures, syntax errors, languages
Notes
  • ast.embedded compiles once per dialect the via produces.
  • embedded reads the language's one via; a language embedding through several would need ast.embedded to choose (none does today).
Shape
"ast": { "language": "blade" | "vue", "embedded": Via, "rule": AstRule, ... } "embedded": AstRule
Composition
embedded holds only on the file's own nodes: in a query without ast.embedded whose language embeds a program, or under a host reached from the program. Refused in a rule over the program (reach the host node with host first), on a language that embeds none, and inside parse. Utilities and constraints may hold it.
Captures
embedded's rule runs in a fresh environment over the program: host captures are invisible, as in parse. Its bindings export into the host rule's environment, spanning the file bytes their program nodes stand for, where a later host term (a compare, the violation) reads them. A host rule inside reads the host's captures as they stood when embedded began. compare with text: "node" also requires both nodes in one document.
Syntax errors
As its rule over the program. A Vue template's syntax error leaves the program whole; a Blade template's syntax error is the view's.
Languages
blade (compiled-view: PHP) and vue (script-block: TypeScript, TSX or JavaScript).
A v-html directive whose value calls a function: from the directive in the component's tree, embedded reaches the program's call.
{
  "language": "vue",
  "rule": {
    "kind": "directive_attribute",
    "has": { "stopBy": "neighbor", "kind": "directive_name", "regex": "^v-html$" },
    "embedded": { "kind": "call_expression" }
  }
}
2 more examples
A bound attribute whose value is the attribute's own name (:title="title", Vue 3.4's same-name shorthand): embedded exports V from the program, and the host rule compares it with the directive's argument. The same comparison may run inside the program rule as host: { compare: ... } where ARG is bound before embedded begins.
{
  "language": "vue",
  "rule": {
    "kind": "directive_attribute",
    "has": { "stopBy": "neighbor", "kind": "directive_value", "pattern": "$ARG" },
    "embedded": { "kind": "identifier", "pattern": "$V" },
    "compare": { "left": "ARG", "op": "eq", "right": "V" }
  }
}
alert called from the template, not from a script block: the query runs on the program, and host places the call in the template.
{
  "language": "vue",
  "embedded": "script-block",
  "rule": {
    "kind": "call_expression",
    "has": { "field": "function", "regex": "^alert$" },
    "host": { "inside": { "stopBy": "end", "kind": "template_element" } }
  }
}

formatArgs #

A format call's arguments no field consumes, or its fields no argument fills. Covers Ruff's Pyflakes rules (F504, F505, F507, F522, F523, F524), PHP's printf family as PHP 8.5.11 and PHPStan's argument.sprintf read it, and Rust's format macros as rustc 1.99.0 binds them.

In the grammar: AstRule, FormatArgs

  • string: a capture bound before, holding the template's plain str literal (through parentheses).
  • args: a capture bound before: a .format call's argument list (a $$$ capture), %'s right operand, or a logging call's arguments.
  • unused: a host rule matched on the arguments no field consumes.
  • missing: a rule of the grammar matched on the fields or placeholders no argument fills. Its captures export as parse's do.
  • Every key listed must find a node.
Details: notes, shape, scanf (php), printf (php), rust (rust), composition, unknown, syntax errors, languages
Notes
What counts as unused in Python: for .format, positional arguments by syntactic index and keywords by name; for %, dict pairs whose key no placeholder names, tuple elements past the placeholders, and a known non-tuple operand as one value.
Shape
"formatArgs": { "style": "brace" | "percent" | "printf" | "scanf" | "rust", "string": NAME, "args": NAME, "unused"?: AstRule, "missing"?: AstRule }
scanf (php)
sscanf/fscanf with variables passed.
  • string: the format, read as printf reads it and parsed by scanfFormat. args: a $$$ capture of the variables after the format.
  • missing: placeholders numbering a variable past the last passed. unused: variables no placeholder assigns, including a gap a %2$s leaves, which PHPStan's count does not see. Both are PHP 8.5.11 ValueErrors.
  • With no variable the call returns an array: nothing is missing or unused.
printf (php)
  • string captures the format (the argument a pattern's $F captures, or its expression, through parentheses), parsed by printfFormat. A string literal maps each node to its source.
  • Any other expression is read as PHPStan reads a constant string, through its value with follow, every node mapped to the whole expression: a concatenation, a cast, a variable's one binding, or a constant this file declares (a const, a top-level define, a class constant through self, parent, a class name, or static where the class or the constant is final).
  • args: a $$$ capture of the arguments after the format (sprintf, printf, fprintf), or one capture of the array literal a vsprintf, vprintf or vfprintf call passes.
  • missing: placeholders and stars that number a value past the last passed (PHP's ArgumentCountError).
  • unused: values (argument or array_element_initializer nodes) past the highest any field numbers, which PHP ignores and PHPStan counts. A value a numbered placeholder skips (%2$s with two values) is not unused.
  • $$$A matches no node after a comma, so a call passing the format alone needs its own pattern (sprintf($F)).
  • Oracle: PHPStan 2.2.16's PrintfParametersRule and PrintfArrayParametersRule over fixtures (fixtures/format_args).
rust (rust)
  • A format_args!-family macro call (format!, print!, println!, eprint!, eprintln!, write!, writeln!, panic!, assert!, assert_eq!, unreachable!, todo!, ...), by its bare or std::/core::/alloc:: path.
  • string captures the template, a string literal token of the macro's token tree, parsed by rustFormat. args captures the arguments after it ($$$A), split at the token tree's top-level commas.
  • unused: the first token of each argument no field, width or precision reads (rustc's "argument never used"). missing: fields reading an index past the last argument (rustc's "invalid reference to positional argument").
  • A field naming no argument captures a variable, which resolves reads.
  • A positional argument after a named one, and a template rustc refuses, are unknown.
Composition
Ordered with the relations: it reads string and args, and binds what unused binds and missing exports.
Unknown
  • Any style: a template the grammar refuses; a string that is no plain str literal (f-strings, bytes).
  • .format: a keyword past **kw; a missing field past *args/**kw.
  • %: a ** splat or a non-string key, a starred element, a format mixing keys and positions, an operand Ruff types as a tuple, an operand of unknown type with placeholders.
  • printf and scanf: a format whose value is not known (an interpolated string, a variable several bindings may reach or a compound assignment changes, a constant another file or a run-time define may declare, PHP_EOL, C::class; PHPStan reads the most placeholders a union of strings has); an ... unpacking or a named argument; values that are no array literal (or unpack one).
Syntax errors
sound (reads the literal and captured nodes; a PHP format read through a binding or constant is unknown in a broken file) · negated over an optional capture source-incomplete
Languages
python (brace, percent) · php and blade (printf, scanf) · rust (rust). Another family's style is refused, naming the counterpart.
Ruff's F504, whole: a dict pair no placeholder names.
ruff/F504.json /checks/0 · Ruff conformance port
{
  "id": "percent-format-extra-named-arguments",
  "forbid": {
    "ast": {
      "language": "python",
      "rule": {
        "pattern": "$S % $R",
        "formatArgs": {
          "style": "percent",
          "string": "S",
          "args": "R",
          "unused": { "kind": "pair" }
        }
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "F504",
    "message": "`%`-format string has unused named argument(s)"
  }
}
1 more example
PHPStan's argument.sprintf as one query (written for this page; compile/format_args/php.rs checks the same shape against PHPStan's reports): sprintf('%s %s', $a) misses a value, sprintf('%s', $a, $b) passes one no placeholder reads.
{
  "language": "php",
  "rule": {
    "all": [
      {
        "any": [
          { "pattern": "sprintf($F)" },
          { "pattern": "sprintf($F, $$$A)" },
          { "pattern": "printf($F)" },
          { "pattern": "printf($F, $$$A)" }
        ]
      },
      {
        "any": [
          {
            "formatArgs": {
              "style": "printf",
              "string": "F",
              "args": "A",
              "missing": { "any": [{ "kind": "placeholder" }, { "kind": "star" }] }
            }
          },
          {
            "formatArgs": {
              "style": "printf",
              "string": "F",
              "args": "A",
              "unused": { "kind": "argument" }
            }
          }
        ]
      }
    ]
  }
}

Before and after: format-string ports

PortBeforeAfter
F504 / F505BLOCKED (format-string-parse); only "no keyed placeholder at all" ported, 6 KBformatArgs unused: { kind: pair } / missing: placeholder has mapping_key
F522BLOCKED; only "no named field", 7 KBformatArgs unused: { kind: keyword_argument }
F507 / F523placeholder-count enumerations by regex, 78–79 KB eachformatArgs with unused and missing
B005128 ASCII alternatives, non-ASCII repeats missed, 5 KBvalue: { "regex": "(?s)(.).*\\1" } (a backreference)

transform #

Variables computed from captures after the rule matches, for messages, fixes and text comparisons. ast-grep 0.45.3's transform (object form), plus Rerail's line and count readers.

In the grammar: AstQuery, Transformation

  • Runs after the rule and its constraints match, in dependency order (a variable may transform another). The source is a capture's text ($NAME; $$$NAME from its first node to its last, de-indented) or a named regex group of the witness.
  • replace: every regex match replaced by by (expands $1 and ${name}). substring: characters startChar to endChar, negative from the end. convert: to a case, splitting words at separatedBy (default: all separators).
  • toPrecision: ECMAScript's Number.prototype.toPrecision(digits), exactly, of the number the source's text denotes. TypeScript, JavaScript and Vue only.
  • line: the 1-based line of the capture's start (edge: "end": its end). lines: the number of lines it spans. count: the named nodes a $$$ capture holds (1 for a single capture).
  • default: the text an unbound source reads as, before the operation. value: true: read the capture's value as a string, converted as the host converts it.
Details: notes, shape, composition, captures, unknown, syntax errors, languages, limits
Notes
  • A witness regex group's text is known only once the rule matched.
  • toPrecision reads a numeric literal as written (0x, 0o, 0b, legacy octal and _ separators included) or a value's text. digits is a number, or the text of another capture or variable read as an integer.
  • ESLint's no-loss-of-precision compares a literal with value.toPrecision(n) for its n significant digits: 9223372036854775808 is exact at 19 digits though String gives 9223372036854776000.
  • toPrecision gives the empty text where the source is no number (a BigInt, a name) or the digits no integer from 1 to 100, and String(x) for an infinite or NaN number. Other families' numbers are not laid out as ECMAScript's, so compiling it there is refused.
  • line uses the linter's terminators. count skips comments and Python line continuations.
  • default: "" with replace: "^$" supplies a fallback. Without default, an unbound source gives the empty text and runs no operation, as upstream.
  • value: true conversions: JavaScript String(v) ('\x61' is a, 1e2 is 100); Python str(v) (1e2 is 100.0).
Shape
"transform": { NAME: { "replace": { "source", "replace": Regex, "by": Str, "default"?, "value"? } } | { "substring": { "source", "startChar"?, "endChar"?, "default"?, "value"? } } | { "convert": { "source", "toCase", "separatedBy"?, "default"?, "value"? } } | { "line": { "source", "edge"?: "start" | "end", "default"? } } | { "lines": { "source", "default"? } } | { "count": { "source", "default"? } } | { "toPrecision": { "source", "digits": 1..100 | "$NAME", "default"?, "value"? } } }
Composition
Beside rule in the ast query. Messages and fixes interpolate variables as $NAME. A compare reads one as text (source, unquoted) where the captures it derives from are visible, after the terms that bind them.
Captures
Refused: a variable naming a capture of the query ("names a capture … and a transform variable; rename one"); a source other than a capture the query can bind, another variable or a witness regex group; a cycle. line, lines and count read a capture's node (a variable is no source for them) and take no value.
Unknown
With value, an unknown value reads as an unbound source where there is a default. Otherwise the variable is the empty text and a comparison of it is unknown.
Syntax errors
changes no verdict; a negated comparison of a variable whose root capture is optional source-incomplete
Languages
both.
Limits
No rewrite, no string form ("substring($A, startChar=1)"), no length or arithmetic (no-regex-spaces' {N}, a count minus one). A variable is text only; a frame cannot read the query's variables.
ESLint's capitalized-comments: replace then convert, and a fix that replaces only the regex group L (fix.range).
eslint/eslint/capitalized-comments.json /checks/0 · ESLint conformance port
{
  "id": "comment-case",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "all": [
          { "kind": "comment" },
          { "pattern": "$CM" },
          { "regex": "^(?://|/\\*)[\\s*]*(?<L>\\p{Ll})" },
          {
            "not": {
              "regex": "^(?://|/\\*)\\s*(?:eslint|jshint\\s+|jslint\\s+|istanbul\\s+|globals?\\s+|exported\\s+|jscs)"
            }
          },
          { "not": { "regex": "^(?://|/\\*)[\\s*]*(?:[^:/?#\\s*]\\**)+:\\**/\\**/\\**[^?#*]" } }
        ]
      },
      "transform": {
        "LT": {
          "replace": {
            "source": "$CM",
            "replace": "^(?://|/\\*)[\\s*]*(\\p{Ll})[\\s\\S]*$",
            "by": "${1}"
          }
        },
        "C": { "convert": { "source": "$LT", "toCase": "upperCase" } }
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "CAPITALIZED_COMMENTS",
    "message": "Comments should not begin with a lowercase character.",
    "fix": { "template": "$C", "range": "capture.L" }
  }
}
2 more examples
ESLint's max-params: count of a $$$ capture in the message, and replace steps that build the number.
eslint/eslint/max-params/max-1-count-this-always-2.json /checks/7 · ESLint conformance port
{
  "id": "function-type",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "all": [
          { "kind": "function_type" },
          {
            "has": {
              "stopBy": "neighbor",
              "kind": "formal_parameters",
              "count": {
                "has": {
                  "any": [{ "kind": "required_parameter" }, { "kind": "optional_parameter" }]
                },
                "min": 2
              }
            }
          },
          { "pattern": "$F" },
          {
            "any": [
              {
                "has": {
                  "field": "parameters",
                  "pattern": {
                    "context": "function f($$$P) {}",
                    "selector": "formal_parameters"
                  }
                }
              },
              { "has": { "field": "parameter", "pattern": "$PB" } }
            ]
          }
        ]
      },
      "transform": {
        "NN": { "count": { "source": "$$$P" } },
        "NC": { "replace": { "source": "$NN", "replace": "^0$", "by": "" } },
        "NB": { "replace": { "source": "$PB", "replace": "(?s)^.+$", "by": "1" } }
      }
    }
  },
  "violation": {
    "anchor": "witness.start",
    "code": "MAX_PARAMS",
    "message": "Function has too many parameters ($NC$NB). Maximum allowed is 1."
  }
}
A fallback for an unbound capture (default), and unified-signatures' "the one on line N" (line).
{
  "language": "typescript",
  "rule": {
    "kind": "function_signature",
    "pattern": "$O",
    "any": [{ "has": { "kind": "type_annotation", "pattern": "$R" } }, { "regex": "^" }]
  },
  "transform": {
    "RT": { "replace": { "source": "$R", "default": "", "replace": "^$", "by": "none" } },
    "L": { "line": { "source": "$O" } }
  }
}

Query options

languageOptions #

Configuration the file cannot show, set per ast query. Modelled on ESLint's languageOptions and Ruff's settings.

In the grammar: AstQuery, LanguageOptions

  • TypeScript keys follow ESLint (ecmaVersion, sourceType, globals, …); Python keys follow Ruff (pythonVersion, typingModules, extendGenerics).
  • Omitted keys take ESLint's defaults ("latest", the file's own source type, no globals) and Ruff's.
  • Without sourceType, each file has its own: from its extension, its nearest package.json type, then its syntax (see Notes).
  • resolves.global, unresolved, binds.builtin and value's builtin names read these options; resolves.this, declared and scriptDeclarations require them.
Details: notes, shape, composition, languages, limits
Notes
  • ecmaVersion: "latest" (= 2026), 3, 5, 6–17 or 2015–2026. Selects ESLint 10.11.0's builtin globals; at 3 and 5, blocks are not scopes.
  • sourceType: script (top-level declarations join the global scope; top-level this is the global object), module (refused before ES2015) or commonjs (adds require, module, exports, global).
  • Omitted sourceType: .cjs and .cts are commonjs; .mjs, .mts, .tsx and declaration files module. A .js, .jsx or .ts file follows the type of its nearest package.json (the host's package, see file; Node runs .ts files like .js ones). With no valid type, Node's syntax detection decides; with no package.json, module (ESLint's default). Omitting it with ecmaVersion before ES2015 is refused.
  • Syntax detection makes a module of a file with a top-level import or export declaration that type stripping keeps, import.meta, a top-level await outside functions, or a top-level const, let or class binding require, module, exports, __dirname or __filename. Declarations that do not count: import type, export type, an exported interface, type alias or declare, import x = require(…) and export =. Anything else is commonjs.
  • moduleSystem: default "file" (the reading above); refused beside sourceType. "package-type" uses only the extension and the nearest package.json's type, as eslint-plugin-n's configs do: .mjs and .mts are module, .cjs and .cts commonjs, any other file module only under type: "module", else commonjs (files without a manifest included). No syntax is read. The query's when.sourceType gates on the same.
  • globals: names mapped to readonly, writable or off, with ESLint's aliases; at most 16,384. parserOptions.ecmaFeatures: globalReturn (the top level is a function scope) and impliedStrict.
  • jsxPragma (default "React"; null for none) and jsxFragmentName: the first JSX element (fragment) whose scope sees a variable of that name reads it. Base-ESLint ports set null.
  • lib: typescript-eslint's lib names. Their variables are declared in their namespaces: a type reference reads a lib type, a value reference a lib value. off removes none.
  • pythonVersion: "3.7"–"3.15", default Ruff's "3.10". Selects Ruff's builtins and magic globals for that version; from 3.14 every annotation is read after the module (PEP 649).
  • typingModules (Ruff's lint.typing-modules) and extendGenerics (lint.pyflakes.extend-generics): which strings are type definitions, and what the typing-aware readings see.
Shape
"languageOptions": { "ecmaVersion"?, "sourceType"?, "moduleSystem"?, "globals"?, "parserOptions"?, "jsxPragma"?, "jsxFragmentName"?, "lib"?, "pythonVersion"?, "typingModules"?, "extendGenerics"? }
Composition
On the ast query, beside rule (a document field would need a catalog column). Applies to every resolves of the query, its utilities, its constraints and the shared utilities it reaches.
Languages
TypeScript keys are refused on Python, and Python keys on TypeScript.
Limits
Per query: a code rule with several clauses repeats the block. No lib means none, not typescript-eslint's parser default. Among conflicting /* global */ comments, off wins.
typescript-eslint's no-redeclare: lib: ["esnext"] makes lib globals builtins for binds.builtin.
eslint/typescript-eslint/no-redeclare/ignore-declaration-merge-false.json /checks/1 · ESLint conformance port
{
  "id": "redeclared-builtin",
  "forbid": {
    "ast": {
      "language": "typescript",
      "languageOptions": { "lib": ["esnext"] },
      "rule": {
        "pattern": "$NAME",
        "any": [
          { "kind": "identifier" },
          { "kind": "type_identifier" },
          { "kind": "property_identifier" },
          { "kind": "shorthand_property_identifier_pattern" },
          { "kind": "shorthand_property_identifier" },
          { "kind": "undefined" }
        ],
        "all": [
          {
            "all": [
              {
                "binds": {
                  "scope": ["global", "module", "function", "block"],
                  "site": { "not": { "kind": "function_signature" } }
                }
              },
              {
                "not": {
                  "all": [
                    { "binds": { "scope": "function" } },
                    {
                      "inside": {
                        "kind": "class_static_block",
                        "stopBy": {
                          "any": [
                            { "kind": "function_declaration" },
                            { "kind": "generator_function_declaration" },
                            { "kind": "function_expression" },
                            { "kind": "generator_function" },
                            { "kind": "arrow_function" },
                            { "kind": "method_definition" },
                            { "kind": "class_static_block" }
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            ]
          },
          { "binds": { "builtin": true } }
        ]
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_REDECLARE",
    "message": "'$NAME' is already defined as a built-in global variable."
  }
}
1 more example
A placement example: ES5 script globals and a configured jQuery.
{
  "language": "typescript",
  "languageOptions": {
    "ecmaVersion": 5,
    "sourceType": "script",
    "globals": { "jQuery": "readonly" }
  },
  "rule": { "pattern": "$F($$$)" },
  "constraints": { "F": { "resolves": { "global": ["Reflect", "JSON", "jQuery"] } } }
}

when #

The configurations a query applies to.

In the grammar: AstQuery, When, Range

  • Ranges are inclusive, in languageOptions' spellings: at least one bound, min not after max.
  • sourceType takes one value or a list. Each ecmaFeatures flag must equal the query's.
  • Keys read the query's own languageOptions, else the defaults. Without a configured sourceType, sourceType reads each file's own.
  • Every key must hold, or the query matches no file: no violation and no failure, sound rather than unknown.
  • The query is compiled and checked in full whether the gate holds or not.
Details: shape, composition, languages, limits
Shape
"when": { "pythonVersion"?: { "min"?, "max"? }, "ecmaVersion"?: { "min"?, "max"? }, "sourceType"?: SourceType | [SourceType], "ecmaFeatures"?: { "globalReturn"?, "impliedStrict"? }, "option"?: { Name: Condition } }
Composition
Beside languageOptions, not inside it: the ESLint harness replaces languageOptions per case.
Languages
pythonVersion python · ecmaVersion, sourceType, ecmaFeatures typescript · option both.
Limits
Gates only on the query's configured version and features, and on its configured source type or else the file's.
ESLint's strict (global): one clause per mode, this one gated on scripts and CommonJS without impliedStrict.
eslint/eslint/strict/global.json /checks/4 · ESLint conformance port
{
  "id": "global-missing-explicit",
  "forbid": {
    "ast": {
      "language": "typescript",
      "languageOptions": {},
      "when": {
        "sourceType": ["script", "commonjs"],
        "ecmaFeatures": { "impliedStrict": false }
      },
      "rule": {
        "all": [
          { "regex": "." },
          { "not": { "kind": "comment" } },
          { "not": { "matches": "us-stmt" } },
          { "matches": "in-program" },
          {
            "not": {
              "follows": {
                "stopBy": "end",
                "all": [{ "regex": "." }, { "not": { "kind": "comment" } }]
              }
            }
          }
        ]
      },
      "utils": {
        "us-lit": {
          "any": [
            { "kind": "string", "regex": "^['\"]use strict['\"]$" },
            {
              "kind": "parenthesized_expression",
              "has": { "stopBy": "neighbor", "matches": "us-lit" }
            }
          ]
        },
        "us-stmt": {
          "kind": "expression_statement",
          "has": { "stopBy": "neighbor", "matches": "us-lit" }
        },
        "in-program": { "inside": { "kind": "program", "stopBy": "neighbor" } }
      }
    }
  },
  "violation": {
    "anchor": "witness.start",
    "code": "STRICT",
    "message": "Use the global form of 'use strict'."
  }
}
1 more example
ESLint's no-redeclare: the ES3 builtin list, gated by ecmaVersion.max, with each over a directive's items.
eslint/eslint/no-redeclare.json /checks/2 · ESLint conformance port
{
  "id": "global-comment-builtin-es3",
  "forbid": {
    "ast": {
      "language": "typescript",
      "languageOptions": { "jsxPragma": null },
      "when": { "ecmaVersion": { "max": 3 } },
      "rule": {
        "kind": "comment",
        "parse": {
          "grammar": "eslintDirective",
          "value": { "regex": "^globals?$" },
          "has": {
            "kind": "item",
            "capture": "N",
            "regex": "^(?:Array|Boolean|Date|Error|EvalError|Function|Infinity|Math|NaN|Number|Object|RangeError|ReferenceError|RegExp|String|SyntaxError|TypeError|URIError|constructor|decodeURI|decodeURIComponent|encodeURI|encodeURIComponent|escape|eval|hasOwnProperty|isFinite|isNaN|isPrototypeOf|parseFloat|parseInt|propertyIsEnumerable|toLocaleString|toString|undefined|unescape|valueOf)$",
            "not": { "has": { "kind": "setting", "regex": "^off$" } }
          }
        }
      }
    }
  },
  "violation": {
    "anchor": "capture.N",
    "code": "NO_REDECLARE",
    "message": "'$N' is already defined as a built-in global variable.",
    "each": "N"
  }
}

file #

Conditions on the file the query reads.

In the grammar: AstQuery, File, TextCond, Package

  • Every key must hold, or the query does not apply to the file: its grammar matcher is skipped, on both paths. jsxRuntime is checked once the file is read.
  • path tests the path the host sends. Python keys read the module path; TypeScript's package reads the nearest package.json.
  • Missing input satisfies neither value: no module path fails every Python text condition; no manifest fails bin: true and bin: false alike (see Notes).
Details: notes, shape, languages, limits
Notes
  • declaration: typescript-eslint's isDefinitionFile (.d.ts, .d.mts, .d.cts, .d.css.ts).
  • byteOrderMark: whether the file's text opens with a byte order mark (U+FEFF). Grammars read the mark as whitespace before the program, so no node's text holds it, the program's included.
  • Python module path: the host's module (a dotted string, or a list of names as on disk), else the file's stem. module is the names joined by .; moduleName the last name (__init__ for a package's __init__.py); package the first, the top package's directory as on disk. inPackage: the host sent a module path (the enforcer sends one only inside a package).
  • TypeScript package reads the file's nearest package.json at or above it: never above the workspace root, nor in a folder whose path resolves outside it through a symlink. It must be a regular file of at most 1 MiB; one that cannot be read has no fields.
  • bin: the file is one of the manifest's bin targets (the string, or each string value of the object), resolved against the manifest's folder as eslint-plugin-n's isBinFile does: the file's path, that path without .js, or without .js and a last /index.
  • nodeVersion.subsetOf / nodeVersion.notSubsetOf: the configured Node range is / is not a subset of the given npm range (node-semver's subset). The configured range is eslint-plugin-n's: engines.node, else the version of the devEngines.runtime entry named node, else >=16.0.0; a range that does not parse falls through to the next.
  • dependsOn: the manifest lists one of the names in dependencies, devDependencies, peerDependencies or optionalDependencies. path: the file's path relative to the manifest's folder. private: the manifest's private is true.
  • serverGraph: the manifest names next and the file is in its Next App Router server module graph. Roots are server file conventions (page, layout, template, default, loading, not-found, forbidden, unauthorized, global-not-found, route, or a metadata image or route file) under the package's app/ (else src/app/, which Next ignores beside a root app/), outside private _ folders.
  • Graph edges: static imports, export … from, and import() or require() of a string, never passing a module whose directive prologue holds "use client". Specifiers resolve relative to the importer with Next's extensions and index files, then through the package's tsconfig.json (else jsconfig.json) paths and baseUrl over relative extends (see Limits).
  • No edge reaches a module of another package (a bare specifier, a file below a nested package.json), one in a hidden folder or one hosts ignore (node_modules, __pycache__, dist, build, coverage, target, vendor), nor, on disk, one in a nested checkout (a folder holding .git). A package of more than 20,000 source files has no graph, on every path.
  • published: npm publishes the file, as eslint-plugin-n 18.4.1's getNpmignore reads it. The file is left out where the manifest's files array leaves it out, or where the .npmignore in the manifest's folder (else, without a files array, its .gitignore) ignores it.
  • files entries: each non-empty string, a leading ! or / dropped and the path normalized, matches as /<entry> and /<entry>/** in the npm ignore package's gitignore semantics. A ! entry matches as <entry> and <entry>/** at any depth. An entry with braces or an extglob goes through globrex, numeric and alphabetic brace sequences expanded.
  • Ignore files use the ignore package, case-insensitive; a file below an ignored folder is ignored too. They are read as the manifest is (a regular file of at most 1 MiB, on disk never a symlink). A pattern whose regex does not compile is left out (upstream throws).
  • Always published: the manifest's main (resolved against the folder), package.json, and a readme, licence, changelog or history file at the top of the folder. Upstream tests those names against a path that depends on ESLint's working directory; they are read as when ESLint runs in the package folder. Without files or an ignore file, every file below the manifest is published.
  • Host input: the host sends the manifest as the file's package, its nodeVersion the configured range, which must be a valid npm range (a package with any other is malformed). It adds the file's own serverGraph membership where a code rule reads it, its own published, and private: true where the manifest is private. A snapshot or workspace file without one reads the package.json, .npmignore, .gitignore, tsconfig.json and jsconfig.json entries and sources of its file set.
  • Missing input: without a module path no Python text condition holds, notRegex included. Without a manifest, neither value of bin, private, published or serverGraph holds, nor dependsOn or package.path; serverGraph also needs a manifest naming next. A host that sends a file no published gives it neither value.
  • jsxRuntime: the file's JSX runtime is one listed, from the first evidence found:
    1. A @jsxImportSource pragma naming React or a React wrapper (react, @emotion/react, theme-ui, @theme-ui/core, or a subpath) gives react; one naming another JSX library (preact, solid-js, vue, @stencil/core, hono, @builder.io/qwik, inferno, mithril, hyperapp, or a subpath) gives other.
    2. Else a @jsx pragma naming React or React.… gives react. Each pragma comes from the first comment that has one, read without its //, /* and */ as Babel reads a comment's value (/*@jsx React*/ names React). Any other pragma is no evidence (Emotion's @jsx jsx renders React).
    3. Else a top-level static import of h or createElement from @stencil/core, preact, vue, solid-js, solid-js/h, hyperapp or mithril gives other.
    4. Else the compilerOptions.jsxImportSource of the tsconfig.json (else jsconfig.json) beside the nearest package.json, through relative extends, where it names React's or another library's as a pragma would.
    5. Else the manifest's dependencies: React's packages (react, react-dom, react-native, next) without another JSX library's (vue, @stencil/core, preact, solid-js, @builder.io/qwik, inferno, mithril) give react, the reverse other; both, neither or no manifest give unknown.
Shape
"file": { "path"?: TextCond, "declaration"?: Bool, "module"?: TextCond, "moduleName"?: TextCond, "package"?: TextCond | Package, "inPackage"?: Bool, "jsxRuntime"?: ["react" | "other" | "unknown"], "byteOrderMark"?: Bool } TextCond = { "regex"?: Str, "notRegex"?: Str } Package = { "bin"?: Bool, "nodeVersion"?: { "subsetOf": SemverRange } | { "notSubsetOf": SemverRange }, "dependsOn"?: [Str], "path"?: TextCond, "serverGraph"?: Bool, "private"?: Bool, "published"?: Bool }
Languages
path, byteOrderMark both · declaration, package (a Package), jsxRuntime typescript · module, moduleName, package (a TextCond), inPackage python
Limits
  • A name with a dot qualifies nothing, and module cannot tell a dotted name from two names. No key reads the names between the first and the last.
  • package reads the nearest manifest only (no workspace or monorepo root); jsxRuntime reads no bundler's settings.
  • A project configuration's relative extends is followed only to another tsconfig*.json or jsconfig*.json (the names hosts route), never to a package's: through the first 16 entries of an extends array, at most 8 bases deep.
  • On disk, no configuration (the package's own tsconfig.json included) is read whose real path leaves the workspace through a symlink, of the file or of a folder on the way. A symlink that resolves inside it is followed.
  • A configuration that does not parse as JSON with comments and trailing commas (TypeScript recovers from more) sets nothing, its extends included, and reports no failure. serverGraph resolves no import through the paths or baseUrl it would set, jsxRuntime reads no jsxImportSource from it, and the jsconfig.json beside an unparsable tsconfig.json does not replace it.
  • serverGraph roots take Next's default page extensions (.tsx, .ts, .jsx, .js; next.config's pageExtensions is not read); an .mjs, .cjs, .mts or .cts module is still reached by import. A configuration's paths maps through its first 64 patterns (by pattern text) and the first 16 targets of each.
  • A package read on disk (hosts, the repository index) has no graph, until it reads again, if a source or folder cannot be read, or past 20,000 sources, 512 MiB of sources (counting those of at most 1 MiB) or 100,000 entries its walk lists (files and folders of any kind, skipped folders included but not their contents). Without a graph neither serverGraph: true nor serverGraph: false holds for the package's files.
  • That walk is not cancelled with an audit.
Ruff's INP001: inPackage: false, a shebang test (layout.start) and a PEP 723 script test, anchored at file.start (byte 0, where the module node of a file that starts with blank lines does not start).
ruff/INP001.json /checks/0 · Ruff conformance port
{
  "id": "implicit-namespace-package",
  "forbid": {
    "ast": {
      "language": "python",
      "file": { "inPackage": false },
      "rule": {
        "kind": "module",
        "not": {
          "any": [
            {
              "has": {
                "stopBy": "neighbor",
                "kind": "comment",
                "layout": { "start": { "eq": 0 } },
                "regex": "^#!"
              }
            },
            {
              "regex": "(?m)^# /// script(\\r\\n|\\r|\\n)(#( [^\\r\\n]*)?(\\r\\n|\\r|\\n))*# ///(\\r?$|\\r)"
            }
          ]
        }
      }
    }
  },
  "violation": {
    "anchor": "file.start",
    "code": "INP001",
    "message": "File is part of an implicit namespace package. Add an `__init__.py`."
  }
}
2 more examples
Ruff's A005, one of its per-version clauses: a module name matched by module, dotted stems excluded by moduleName.notRegex, gated by when.pythonVersion.
ruff/A005.json /checks/2 · Ruff conformance port
{
  "id": "stdlib-module-shadowing-3.7-3.7",
  "forbid": {
    "ast": {
      "language": "python",
      "when": { "pythonVersion": { "max": "3.7" } },
      "file": {
        "module": { "regex": "(?:^|\\.)(?:macpath)(?:\\.__init__)?$" },
        "moduleName": { "notRegex": "\\." }
      },
      "rule": { "kind": "module" }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "A005",
    "message": "Module shadows a Python standard-library module"
  }
}
typescript-eslint's no-unused-vars: the clause for ordinary files (declaration: false); a twin clause reads declaration files.
eslint/typescript-eslint/no-unused-vars/args-all-vars-local.json /checks/0 · ESLint conformance port
{
  "id": "unused-args",
  "forbid": {
    "ast": {
      "language": "typescript",
      "file": { "declaration": false },
      "languageOptions": {},
      "rule": {
        "pattern": "$NAME",
        "any": [
          { "kind": "identifier" },
          { "kind": "type_identifier" },
          { "kind": "property_identifier" },
          { "kind": "shorthand_property_identifier_pattern" },
          { "kind": "shorthand_property_identifier" },
          { "kind": "undefined" }
        ],
        "all": [
          {
            "all": [
              {
                "resolves": {
                  "write": "last",
                  "binding": {
                    "scope": [
                      "module",
                      "function",
                      "class",
                      "block",
                      "namespace",
                      "typeParameters"
                    ],
                    "directives": "eslint",
                    "form": "parameter",
                    "used": false,
                    "exported": false
                  }
                }
              }
            ]
          }
        ]
      }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_UNUSED_VARS",
    "message": "'$NAME' is defined but never used."
  }
}

Violations

anchor · message · code #

Where a violation points, and what it says.

In the grammar: Clause, Violation

  • anchor: the whole witness, its start or end, byte 0 of the file (file.start), or a capture (capture.NAME).
  • message: $NAME and $$$NAME interpolate captures and transform variables. A name the query cannot bind is refused.
  • ${NAME} and $$${NAME} end the name at the brace, so a variable can sit before name characters (${A}_ID). \$ is a literal $.
  • variables is reserved: only {} is accepted.
Details: notes, shape, composition, captures
Notes
  • Anchors: file.start is byte 0 whatever the witness, for file-level diagnostics such as INP001. A capture is an ast metavariable, a witness-level regex's or regex query's named group, or a parsed tree's export.
  • No span: a file.path match or a file query's file names the file with no line, unless anchored at file.start. It takes no capture.NAME anchor.
  • Id: from the witness span (bytes 0 to 0 without one), plus the binding's range under each.
  • Message: a digit-led name ($100) stays literal. ${NAME} and $$${NAME} are Rerail's; ast-grep renders them literally. \${NAME} is a backslash before the variable.
Shape
"violation": { "code": Str, "anchor": "witness.span" | "witness.start" | "witness.end" | "file.start" | "capture.NAME", "message": Template, "fix"?, "each"?, "suppress"?, "variables"?: {} }
Composition
Fact clauses (require_each, budget, llm) take only witness.span. An unbound capture anchor falls back to the witness.
Captures
The query's own, what calls return, and exports; never a frame's own.
Ruff's F823: $N in the message.
ruff/F823.json /checks/0 · Ruff conformance port
{
  "id": "undefined-local",
  "forbid": {
    "ast": {
      "language": "python",
      "rule": { "kind": "identifier", "pattern": "$N", "resolves": { "beforeLocal": true } }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "F823",
    "message": "Local variable `$N` referenced before assignment"
  }
}
2 more examples
Ruff's INP001: file.start.
ruff/INP001.json /checks/0 · Ruff conformance port
{
  "id": "implicit-namespace-package",
  "forbid": {
    "ast": {
      "language": "python",
      "file": { "inPackage": false },
      "rule": {
        "kind": "module",
        "not": {
          "any": [
            {
              "has": {
                "stopBy": "neighbor",
                "kind": "comment",
                "layout": { "start": { "eq": 0 } },
                "regex": "^#!"
              }
            },
            {
              "regex": "(?m)^# /// script(\\r\\n|\\r|\\n)(#( [^\\r\\n]*)?(\\r\\n|\\r|\\n))*# ///(\\r?$|\\r)"
            }
          ]
        }
      }
    }
  },
  "violation": {
    "anchor": "file.start",
    "code": "INP001",
    "message": "File is part of an implicit namespace package. Add an `__init__.py`."
  }
}
${F} sits before name characters in the message and the fix.
{
  "id": "prefer-async",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "pattern": "$F($$$ARGS)",
        "has": { "kind": "identifier", "regex": "^read", "stopBy": "neighbor" }
      }
    }
  },
  "violation": {
    "code": "PREFER_ASYNC",
    "anchor": "witness.span",
    "message": "Prefer ${F}Async.",
    "fix": "${F}Async($$$ARGS)"
  }
}

each #

One violation per distinct binding of a capture at a witness.

In the grammar: Violation

  • The rule is rematched, excluding ranges already bound, until nothing new binds or maxWitnesses is reached.
  • Each violation carries its binding in captures; its id joins the binding's range.
  • A match leaving the capture unbound is one violation, only where no match binds it.
  • Works on exported captures (parse, formatArgs' missing), unused bindings and a witness-level regex's named group.
Details: notes, shape, composition, syntax errors, limits
Notes
Bindings are host ranges. A relation's search and an any skip a candidate that would bind the capture to an excluded range, so the violations are the distinct bindings a first-match search reaches. A regex group yields one per non-overlapping match; a witness where none binds it reports nothing.
Shape
"violation": { ..., "each": NAME }
Composition
An ast query's single-node capture. Negations, counts, stop rules and nthChild probes exclude nothing. In a frame, the capture is excluded under the parameter it is passed as.
Syntax errors
changes no verdict.
Limits
Bindings needing two sub-rules to change together are not reached.
ESLint's no-irregular-whitespace: each over a regex group, one violation per run.
eslint/eslint/no-irregular-whitespace/skip-templates.json /checks/0 · ESLint conformance port
{
  "id": "in-token",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "any": [{ "kind": "comment" }, { "kind": "regex" }],
        "regex": "(?P<WS>[\\x0B\\x0C\\x{85}\\x{FEFF}\\x{A0}\\x{1680}\\x{180E}\\x{2000}-\\x{200B}\\x{202F}\\x{205F}\\x{3000}]+|[\\x{2028}\\x{2029}])"
      }
    }
  },
  "violation": {
    "anchor": "capture.WS",
    "code": "NO_IRREGULAR_WHITESPACE",
    "message": "Irregular whitespace not allowed.",
    "each": "WS"
  }
}
1 more example
Ruff's W605 in format specs: an alternation that walks every escape, naming the group only where the escape is invalid.
ruff/W605.json /checks/1 · Ruff conformance port
{
  "id": "invalid-escape-sequence-format-spec",
  "forbid": {
    "ast": {
      "language": "python",
      "rule": {
        "kind": "format_specifier",
        "regex": "\\\\[\\n\\r\\\\'\"abfnrtv0-7xNuU]|(?<BS>\\\\(?:[^\\n\\r\\\\'\"abfnrtv0-7xNuU]|$))",
        "inside": {
          "stopBy": "neighbor",
          "kind": "interpolation",
          "inside": {
            "stopBy": "neighbor",
            "kind": "string",
            "not": { "has": { "stopBy": "neighbor", "kind": "string_start", "regex": "[rR]" } }
          }
        }
      }
    }
  },
  "violation": {
    "anchor": "capture.BS",
    "code": "W605",
    "message": "Invalid escape sequence",
    "each": "BS"
  }
}

Before and after: E712

Before: "rule": { "kind": "comparison_operator", "has": { "stopBy": "neighbor", "matches": "flagged-bool-operand" } }, one violation per comparison (21 diagnostics on the fixture). After: bind the operand ("pattern": "$OP" in the has) and add "each": "OP": res == True != False gives two violations at one span with two ids, as Ruff does (26 diagnostics, exactly Ruff's). PIE810, SIM101, SIM109, SIM220/221 and T100 follow the same recipe.

fix #

A suggested replacement, never applied.

In the grammar: Violation

  • ast query: ast-grep's TemplateFix over metavariables and transform variables, indentation rules kept.
  • file.content regex: the template substitutes named groups.
  • range replaces only a capture's range in the witness: a node, a $$$ capture (first to last node), a witness regex group, or an export (its host range).
  • A range outside the witness or an unbound capture gives no fix; the violation stays.
Details: shape, composition, limits
Shape
"fix": Template | { "template": Template, "range": "witness.span" | "capture.NAME" }
Composition
Only on forbid over an ast or file.content regex query; range only on an ast query.
Limits
A fix after a $$$ capture ending in a line comment lands in the comment (as upstream). The host resolves overlapping fixes.
ESLint's no-unneeded-ternary: a template over two metavariables.
eslint/eslint/no-unneeded-ternary/no-default-assignment.json /checks/4 · ESLint conformance port
{
  "id": "invert-eq",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": { "kind": "ternary_expression", "pattern": "$L == $R ? false : true" }
    }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_UNNEEDED_TERNARY",
    "message": "Unnecessary use of boolean literals in conditional expression.",
    "fix": "$L != $R"
  }
}
1 more example
ESLint's no-div-regex: a fix that replaces only the =, a regex group.
eslint/eslint/no-div-regex.json /checks/0 · ESLint conformance port
{
  "id": "leading-equals-regex",
  "forbid": {
    "ast": { "language": "typescript", "rule": { "kind": "regex", "regex": "^/(?<EQ>=)" } }
  },
  "violation": {
    "anchor": "witness.span",
    "code": "NO_DIV_REGEX",
    "message": "A regular expression literal can be confused with '/='.",
    "fix": { "template": "[=]", "range": "capture.EQ" }
  }
}

suppress #

Honours another linter's suppression comments as that linter does, opt-in per violation. Rerail's own rerail-ignore needs no opt-in.

In the grammar: Violation, Suppress

  • suppress names, per linter, the upstream rules its comments must name; each is read as a pinned upstream version reads it.
  • A violation is placed at its anchor's start; for oxlint and biome, over its anchored span.
  • A suppressed violation is dropped before the witness budget counts it.
  • Without suppress, only rerail-ignore silences a rule.
Details: notes, shape, composition, rerail's comment, limits
Notes
  • eslint (TypeScript, Vue): ESLint 10.11.0's applyDisableDirectives. Block eslint-disable/eslint-enable from where they stand, eslint-disable-line on its line, eslint-disable-next-line on the line after the comment ends; each names one of rules or none, -- descriptions ignored. Vue: script comments hold ESLint's directives (vue-eslint-parser passes no other); template HTML comments hold eslint-plugin-vue 10.11.1's (vue/comment-directive) <!-- eslint-disable -->, eslint-enable, eslint-disable-line and eslint-disable-next-line, read as its processor reads them. An HTML-like comment (<!--, or --> first on its line) is a // comment whose -line and -next-line directives count; ESLint parses it as a line comment in a script, not at all in a module.
  • oxlint (TypeScript): Oxlint 1.88.0's DisableDirectivesBuilder and DisableDirectives::contains (oxc tag oxlint_v1.88.0), default respectEslintDisableDirectives. Reads oxlint- and eslint- directives in line and block comments, JSX's {/* */} and HTML-like comments (<!--, or --> first on its line: line comments in a script; they leave a module unparsed). The name ends the comment or precedes white space; the list splits on commas and white space, ending at -- or a - between white space. Only pairs make a block: enable closes the first open disable naming no rule, enable X the first open disable X. Unclosed blocks run to end of file, so enable X inside a disable leaves X disabled. A block covers a violation whose anchored span overlaps it, even one starting before the disable. -line covers one starting on the comment's first line, before it; -next-line one starting after it on its last line, or on the next line. rules are oxlint's names without a plugin; a listed name counts by its part after the last / (react-hooks/rules-of-hooks and foo/rules-of-hooks both name rules-of-hooks; canonical/no-re-export does not name export).
  • biome (TypeScript): Biome 2.5.15's JavaScript analyzer (tag @biomejs/biome@2.5.15: biome_suppression's parse_suppression_comment, biome_analyze's to_analyzer_suppressions and Suppressions). A comment line (each block-comment line past its *s) starts biome-ignore, biome-ignore-all, biome-ignore-start or biome-ignore-end, then white-space-separated categories, : and a reason: // biome-ignore lint/correctness/useHookAtTopLevel: intentional. lint names every lint rule, lint/<group> a group, lint/<group>/<rule> one. lint/<group>/<rule>(value) names an instance and never matches here (a violation reports no instance). A line with no reason or :, an unclosed (, or a category Biome lacks (lint/correctness/useHookAtTopLevell) is refused whole and suppresses nothing; a group or rule the JavaScript analyzer lacks (a CSS rule) is refused alone. No HTML-like comments: biome-ignore after <!-- or --> does nothing. biome-ignore covers violations starting from the comment to the end of the line after its last line; consecutive suppression comments move on together, covering the code after the last. biome-ignore-all covers the file if before the first token (a shebang aside), else nothing. biome-ignore-start covers violations it holds whole, up to the closing biome-ignore-end of the same category (the latest open one) or end of file. rules are full names, lint/<group>/<rule>, of Biome 2.5.15's JavaScript lint rules.
  • noqa (Python): Ruff 0.16.9's check_noqa: a file-level # ruff: noqa or # flake8: noqa on its own line, then # noqa or # noqa: CODES on the violation's line via Ruff's NoqaMapping (a multi-line string, f-string or continuation run maps to its last line). parent: "statement" also accepts a directive where the enclosing import or expression statement starts (Ruff sets it for F401, F811, F822 among Pyflakes rules, not F841).
  • nosec (Python): Bandit 1.9.4's # nosec (NOSEC_COMMENT, _parse_nosec_comment, _get_nosecs_from_contexts): #\s*nosec:? anywhere in a comment, case-sensitive. Its list (to the next #) names tests by id or name (B101, assert_used); naming no known test, or none, makes it blanket (# nosec TODO). Of ids joined by a bare comma, only the last counts (B101,B102 names B102). It counts on the violation's line and the witness's first line carrying one (Bandit's linerange of the visited node). The lists join, so a blanket comment beside a naming one names only those tests. Suppresses when naming one of codes, or blanket.
  • phpstan (PHP): PHPStan 2.2.16's @phpstan-ignore naming one of identifiers on the line it applies to, and @phpstan-ignore-line/-next-line.
  • psalm (PHP): Psalm 6.19.1's @psalm-suppress in doc comments (/**, never // or /* */) on the next node. Before a statement it covers the statement, and an earlier suppressing doc comment holds to the end of the function (at top level, the file and its classes), as in Psalm. Before a function, method, closure, class, interface, enum, property or class constant, the last doc comment covers the declaration. An inline doc comment inside an expression covers nothing. A tag names an issue of issues or its Config::getParentIssueType parent; all counts only with suppressAll: true (Psalm's disableSuppressAll="false").
  • phpcs (PHP): PHP_CodeSniffer 4.0.4's phpcs:ignore (alone on its line, it ignores the next), phpcs:disable/phpcs:enable ranges and phpcs:ignoreFile, also as @phpcs:, naming one of sniffs, its sniff, category or standard, or nothing. version: 3 reads as PHP_CodeSniffer 3.13.6, with its .except rules and the @codingStandardsIgnoreLine/Start/End/File annotations 4.0 removed.
  • clippy (Rust): rustc's and Clippy 0.1.99's #[allow(..)], #[expect(..)], #[warn(..)], #[deny(..)] and #[forbid(..)]. Outer on an item, statement, field, variant, arm or expression; inner (#![..]) on a module, block or the file; also via #[cfg_attr(..)] when its predicate holds (all, any, not over clippy; other cfg names are false). Drops the violation when the level in force for one of lints is allow or expect. Lints are spelled as in attributes: clippy::double_parens, a group such as clippy::all, or warnings. The narrower attribute wins, then the later on one item; a forbid is never lowered. With msrv ("1.53", the Rust version the lint needs), a #[clippy::msrv = ".."] in force naming an older version drops it too. Names are compared as written, not resolved for renamed or removed lints.
  • sqlfluff (SQL): SQLFluff 4.4.0's noqa (core/rules/noqa.py) in -- and /* */ comments: -- noqa or -- noqa: LIST on the violation's line; -- noqa: disable=LIST/enable=LIST ranges from their line on; all names every rule. rules are the keys of the rule's RulePack.reference_map: code, legacy aliases, groups and name (["AL05", "L025", "aliasing", "aliasing.unused", "all", "core"]). An entry matches by name or as a case-sensitive fnmatch glob.
Shape
"suppress": { "eslint"?: { "rules": [RuleName]⁺ }, "noqa"?: { "codes": [Code]⁺, "parent"?: "statement" }, "nosec"?: { "codes": [TestId]⁺ }, "oxlint"?: { "rules": [OxlintRuleName]⁺ }, "biome"?: { "rules": [BiomeLintRule]⁺ }, "phpstan"?: { "identifiers": [Identifier]⁺ }, "psalm"?: { "issues": [Issue]⁺, "suppressAll"?: true }, "phpcs"?: { "sniffs": [SniffCode]⁺, "version"?: 3 | 4 }, "clippy"?: { "lints": [LintName]⁺, "msrv"?: RustVersion }, "sqlfluff"?: { "rules": [Reference]⁺ } }
Composition
On an ast query's violation only, each linter on its own language:
  • Python: noqa, nosec.
  • TypeScript and JavaScript: eslint, oxlint, biome.
  • PHP: phpstan, psalm, phpcs; any one removes the violation.
  • Rust: clippy. Vue: eslint. SQL: sqlfluff. Blade: none.
PHP's @ operator silences run-time warnings, not a linter: no suppression. The engine reads these comments nowhere else. A ported rule carries suppress, naming the upstream rule, when its linter honours the comment; pack validation (check 4, docs/pack-format.md) fails a port that does not. So a comment that silences the linter (an agent's # noqa or # nosec too) silences the port, unless the repository sets the rule's suppressions setting to ignore (docs/rule-scoping.md). A pack's honourSuppressionComments: false makes ignore its rules' starting value.
Rerail's comment
rerail-ignore is Rerail's own suppression comment, separate from suppress. Every code rule answers to it, with nothing in the rule document. It is always honoured; the repository's suppressions setting governs only other linters' comments.
  • Syntax: a comment node of the file's grammar (never string text). After its opener (//, /*, /**, #, --, {{--, <!--), any * and white space, it starts rerail-ignore, then white space and code rule ids separated by commas or white space, to the line's end or a -- reason (two or more dashes between white space): // rerail-ignore complexity-cyclomatic-typescript, acme:no-print -- generated. Ids match a code rule's document id exactly, case-sensitively. No id silences nothing; rerail-ignored and rerail-ignore-next are not directives.
  • Scope, by node; a covered node is covered over all its lines:
    • Before the file's first code (only comments, white space, a shebang and PHP's <?php before it): the whole file.
    • On its own line: the next named sibling in its parent, skipping comments (a Python comment before a body's first statement leads it). Nothing at the end of a block.
    • Trailing code on its line: the nearest preceding named sibling, if only comments lie between and it ends on that line (b(); // rerail-ignore x covers b();).
    • Past a , or ; ending a list element or class field on that line: the elements before it on the line, from the one holding the line's first code.   a(), // rerail-ignore x on its own line of an argument list covers a(), not later arguments;   x = 1; // rerail-ignore x in a class body covers the field, not later methods.
    • Otherwise: the comment's smallest ancestor holding the line's first code, never the file's root. const e = k( // rerail-ignore x covers the declaration; m(n, // rerail-ignore x the call.
  • Effect: a witness is dropped, before the witness budget counts it, when its anchor's start lies in a node covered by a directive naming its rule. For witness.end, the last byte counts: the end point is where the next sibling may start, which that node's directive does not cover.
  • Facts stay: only a reported violation's witness answers. No comment, Rerail's or a linter's, removes require_each evidence rows, budget rows (it silences the group's violation only at the row it is reported at), a select_when's exists side, or a calls seed. suppress is refused on require_each and budget.
  • Languages: TypeScript, TSX, JavaScript, Python, PHP and Blade (its {{-- --}} and <!-- --> comments and its embedded PHP's).
Limits
Not modelled:
  • unused-directive reports (UnusedPsalmSuppress, SQLFluff's warn_unused_ignores);
  • Bandit's string tests reading # nosec over the string's parent node lines (a nosec port reads its witness's lines);
  • /* eslint rule: off */ configuration comments; oxlint's respectEslintDisableDirectives: false;
  • Biome's instance suppressions (useExhaustiveDependencies(dep)) and configuration (biome.json);
  • Ruff's range suppressions, RUF100, code redirects (a port lists old codes itself);
  • phpcs:set property changes; a Psalm class tag reaching the class's trait methods;
  • SQLFluff's disable_noqa and disable_noqa_except.
ESLint's strict valid/13, where /* eslint-disable rule-to-test/strict */ removes the report (the harness renames the rule, so both names are listed).
{
  "id": "function-missing",
  "forbid": {
    "ast": {
      "language": "typescript",
      "rule": {
        "kind": "function_declaration",
        "not": {
          "has": {
            "kind": "statement_block",
            "has": { "kind": "expression_statement", "regex": "^['\"]use strict['\"]" }
          }
        }
      }
    }
  },
  "violation": {
    "code": "STRICT",
    "anchor": "witness.start",
    "message": "Use the function form of 'use strict'.",
    "suppress": { "eslint": { "rules": ["strict", "rule-to-test/strict"] } }
  }
}

4. Unknown and soundness #

How a rule decides when the file alone cannot, and which rules still run on files with syntax errors.

The three-valued model #

A term's verdict is true, false or unknown. Unknown means the file alone cannot decide; see the sources below.

CombinatorVerdict
not Xtrue ↔ false; unknown stays unknown
any [..]a true branch decides; else unknown if any branch is; else false
all [..], rule objecta false item decides, even after an unknown one. Items mentioning a capture an unknown item would bind stay open, their bindings withheld. Else unknown if any is; else true
constraintsthe first failing constraint's verdict (unknown before false is unknown)
WalkVerdict
relation, rule binds nothinga later match decides past unknown candidates
relation, rule bindsthe first unknown candidate no other term makes false ends the search unknown
stop rule unknownthe walk ends open
counttrue count in [matches, matches + unknowns]; unknown when that straddles a bound

Sources of unknown:

Budgets are never unknown or false: running out fails the clause for the file (budget-exceeded).

Soundness under syntax errors #

tree-sitter recovers from syntax errors with ERROR and zero-width MISSING nodes. The engine drops every witness whose range meets an error region, so claims about nodes inside a witness read only cleanly parsed source.

Each rule is classified at compile time (soundness.rs):

A file with a syntax error f() g(ERROR / MISSING h() witness outside errors: kept witness meets a region: dropped kept Relations inside the witness (has, pattern, kind, regex) only read clean source. An outward relation reads a parent or sibling the parser produced: a fact. Its absence, or a binding elsewhere, is no fact. sound → reported around errors the clause runs; witnesses outside error regions stand; the file also gets a source-partial failure pattern, kind, regex, has, not has, arguments, value, compare, … unsound → not evaluated on that file the clause reports a source-incomplete failure instead negated outward relations, stop rules, count over outward relations, name resolution, binds, value.follow, nthChild, …
ConstructClassWhy
pattern, kind, regex, capture, argumentssoundread the witness itself
has, not has, field on hassoundread inside the witness; a stop rule on has classifies as the stop rule negated
inside, precedes, follows (positive, no stop rule)soundthe related node is a fact of the recovered tree; stopBy: "end" only widens along the same axis
the same, negated, or with a stop ruleincompleteclaims absence over nodes the parser may not have read
countas the relation negatedclaims presence and absence at once: sound over has, incomplete over outward relations
compare, formatArgs, constraintssound / incompleteread captures. Incomplete where negated (or a constraint) over a capture an outward relation binds under any: an error region can leave it unbound, which passes
valuesoundreads the node's text; builtin names are unknown in a broken file
value.follow, resolves, binds, nthChild, parse: annotationincompleteread bindings, siblings or the scope table anywhere in the file
resolves: { imported } alone, positivesoundreads its import statement; one broken before the name imports nothing
layout span/start, positive gapssoundthe neighbour is a fact; negated gaps are incomplete
parse with a text grammarsoundreads the literal alone; a host rule is as sound as on its own, incomplete negated
utilities, callsas their rulewalked once per negation and position
regex queries, fact queriessoundonly AST leaves mark a file partial

Mostly broken files are not read. If error regions cover half a file's bytes or more, no AST clause runs on it, sound or not; tree-sitter often returns one ERROR over the whole file. Each clause reports source-incomplete with file did not parse: error regions cover N% of it, first at line L, column C. Regex clauses still run.

Known gaps:

5. Captures #

Which constructs bind captures, which read them, the order terms run in, and where bindings are visible.

What binds, what reads #

BindsReadsNeither
  • pattern metavariables ($A, $$$A)
  • a relation's, all's, any's matching items
  • matches: what its utility binds, transitively
  • a call: what the utility binds to a parameter
  • arguments.bind
  • resolves.binding and binds sub-rules
  • nthChild.ofRule
  • formatArgs (unused, missing's exports)
  • capture in a tree, exported by parse
  • embedded: what its rule binds over the program, exported with file spans
  • named groups of a witness-level regex (violations only)
  • transform variables (text, after the match)
  • compare (both sides)
  • bindings.within
  • a layout gap's to: "capture.NAME"
  • formatArgs' string and args
  • a not: everything its rule would bind
  • a transform comparison: the captures the variable derives from
  • the violation ($NAME, capture.NAME, each, fix templates)
  • host: the parsed tree's or the program rule's captures, and the host's as the parse or embedded began
  • kind
  • regex (as a matcher)
  • value
  • resolves (but binding)
  • count (keeps nothing it binds)
  • stop rules (own environment)

A metavariable met again after it is bound must match the same text (ast-grep's repeated metavariable). A capture name is [A-Z0-9_]+; in Python patterns only such names are metavariables.

Ordering rules #

Written (a rule object's keys) { "kind": "call", "not": { "compare": { "left": "A", "op": "eq", "right": "B" } }, "has": { "pattern": "$A.close()" }, "inside": { "pattern": "$B = $_" }, "compare": { "left": "A", … "B" } } Run 1 atomic kind pattern, kind,regex, capture,parse, nthChild,arguments 2 ordered group (by dependency) inside has not not reads A and B: it runs after the has and inside that bind them (written first, run last in the group) 3 late compare count, compare, layout,value, then resolves(reads the bindings madeby phases 1 and 2) Refused at compile time • two siblings that each read what the other binds: "no order binds both first" • an explicit all whose item reads what only a later item binds: "move all/2 before all/0" (all keeps its written order) • a comparison, gap or within of a name never bound where it runs (walked per utility call, in the callers' scopes) • a transform variable named like a capture of the query

Environments and frames #

ConstructEnvironment
relation's rule, all, any, plain utility, binding sub-rulesthe caller's: reads its bindings; what it binds comes back
notthe caller's plus its own; nothing leaves it
countreads the caller's; keeps nothing it binds
stop rulefresh: reads and binds nothing of the caller's
parameterized calla frame: only the parameters (copied in), fresh captures per level; parameters bound inside come back under the caller's names
parsethe tree's own; captures are exported to the host as span captures (text + host range)
embeddedfresh, over the program; its captures are exported to the host rule's environment (text + file span)
hostthe host's as the parse or enclosing embedded began (fresh under ast.embedded), plus the inner rule's captures so far; binds nothing the inner rule sees
constraints, transformafter the whole rule matched, over the witness's bindings

Pitfalls #

6. Known limits #

Deliberate boundaries and measured gaps, grouped by area. None is an open bug.

Matching, ordering and budgets

Names and bindings

Values, arguments and comparison

Sub-grammars, layout, transforms and suppression

Configuration, reuse and the catalog

7. Coverage matrix #

How many committed code-rule documents use each construct at least once, per suite: every rule pack's documents (packs/*/rules), the bundled code rules (the complexity pack core ships) and the engine's two worked examples (fixtures/*/compile/examples/); --ruff and --eslint add Ruff and ESLint conformance ports when you have them. Rows link to the construct cards; a cell's tooltip gives the share. Generated by node scripts/generate/docs/code-rule-dsl-reference.mjs coverage --write, which also prints the table.

Packsn=621Bundledn=3Workedn=2Documentforbidforbid: 621 of 621 Packs documents (100.0%)621forbid: 3 of 3 Bundled documents (100.0%)3forbid: 2 of 2 Worked documents (100.0%)2require_eachrequire_each: 0 of 621 Packs documents (0.0%)·require_each: 0 of 3 Bundled documents (0.0%)·require_each: 0 of 2 Worked documents (0.0%)·budgetbudget: 0 of 621 Packs documents (0.0%)·budget: 0 of 3 Bundled documents (0.0%)·budget: 0 of 2 Worked documents (0.0%)·definedefine: 122 of 621 Packs documents (19.6%)122define: 0 of 3 Bundled documents (0.0%)·define: 2 of 2 Worked documents (100.0%)2regex queryregex query: 2 of 621 Packs documents (0.3%)2regex query: 0 of 3 Bundled documents (0.0%)·regex query: 0 of 2 Worked documents (0.0%)·fact queryfact query: 0 of 621 Packs documents (0.0%)·fact query: 0 of 3 Bundled documents (0.0%)·fact query: 0 of 2 Worked documents (0.0%)·select_whenselect_when: 0 of 621 Packs documents (0.0%)·select_when: 0 of 3 Bundled documents (0.0%)·select_when: 0 of 2 Worked documents (0.0%)·ast queryast query: 620 of 621 Packs documents (99.8%)620ast query: 3 of 3 Bundled documents (100.0%)3ast query: 2 of 2 Worked documents (100.0%)2pythonpython: 235 of 621 Packs documents (37.8%)235python: 1 of 3 Bundled documents (33.3%)1python: 1 of 2 Worked documents (50.0%)1typescripttypescript: 263 of 621 Packs documents (42.4%)263typescript: 1 of 3 Bundled documents (33.3%)1typescript: 1 of 2 Worked documents (50.0%)1scopescope: 621 of 621 Packs documents (100.0%)621scope: 3 of 3 Bundled documents (100.0%)3scope: 1 of 2 Worked documents (50.0%)1Atomickindkind: 570 of 621 Packs documents (91.8%)570kind: 3 of 3 Bundled documents (100.0%)3kind: 2 of 2 Worked documents (100.0%)2patternpattern: 286 of 621 Packs documents (46.1%)286pattern: 0 of 3 Bundled documents (0.0%)·pattern: 0 of 2 Worked documents (0.0%)·pattern context/selectorpattern context/selector: 15 of 621 Packs documents (2.4%)15pattern context/selector: 0 of 3 Bundled documents (0.0%)·pattern context/selector: 0 of 2 Worked documents (0.0%)·regexregex: 482 of 621 Packs documents (77.6%)482regex: 2 of 3 Bundled documents (66.7%)2regex: 0 of 2 Worked documents (0.0%)·valuevalue: 113 of 621 Packs documents (18.2%)113value: 0 of 3 Bundled documents (0.0%)·value: 0 of 2 Worked documents (0.0%)·value.followvalue.follow: 10 of 621 Packs documents (1.6%)10value.follow: 0 of 3 Bundled documents (0.0%)·value.follow: 0 of 2 Worked documents (0.0%)·nthChildnthChild: 44 of 621 Packs documents (7.1%)44nthChild: 0 of 3 Bundled documents (0.0%)·nthChild: 0 of 2 Worked documents (0.0%)·nthChild.ofRulenthChild.ofRule: 29 of 621 Packs documents (4.7%)29nthChild.ofRule: 0 of 3 Bundled documents (0.0%)·nthChild.ofRule: 0 of 2 Worked documents (0.0%)·layoutlayout: 19 of 621 Packs documents (3.1%)19layout: 0 of 3 Bundled documents (0.0%)·layout: 0 of 2 Worked documents (0.0%)·layout.before/afterlayout.before/after: 16 of 621 Packs documents (2.6%)16layout.before/after: 0 of 3 Bundled documents (0.0%)·layout.before/after: 0 of 2 Worked documents (0.0%)·layout.span/startlayout.span/start: 4 of 621 Packs documents (0.6%)4layout.span/start: 0 of 3 Bundled documents (0.0%)·layout.span/start: 0 of 2 Worked documents (0.0%)·Relationshashas: 450 of 621 Packs documents (72.5%)450has: 2 of 3 Bundled documents (66.7%)2has: 0 of 2 Worked documents (0.0%)·insideinside: 351 of 621 Packs documents (56.5%)351inside: 0 of 3 Bundled documents (0.0%)·inside: 0 of 2 Worked documents (0.0%)·precedesprecedes: 159 of 621 Packs documents (25.6%)159precedes: 0 of 3 Bundled documents (0.0%)·precedes: 0 of 2 Worked documents (0.0%)·followsfollows: 238 of 621 Packs documents (38.3%)238follows: 0 of 3 Bundled documents (0.0%)·follows: 0 of 2 Worked documents (0.0%)·stopBy: endstopBy: end: 267 of 621 Packs documents (43.0%)267stopBy: end: 0 of 3 Bundled documents (0.0%)·stopBy: end: 0 of 2 Worked documents (0.0%)·stopBy: neighborstopBy: neighbor: 312 of 621 Packs documents (50.2%)312stopBy: neighbor: 0 of 3 Bundled documents (0.0%)·stopBy: neighbor: 0 of 2 Worked documents (0.0%)·stopBy: rulestopBy: rule: 152 of 621 Packs documents (24.5%)152stopBy: rule: 3 of 3 Bundled documents (100.0%)3stopBy: rule: 0 of 2 Worked documents (0.0%)·fieldfield: 171 of 621 Packs documents (27.5%)171field: 0 of 3 Bundled documents (0.0%)·field: 0 of 2 Worked documents (0.0%)·Combinatorsallall: 385 of 621 Packs documents (62.0%)385all: 0 of 3 Bundled documents (0.0%)·all: 0 of 2 Worked documents (0.0%)·anyany: 480 of 621 Packs documents (77.3%)480any: 3 of 3 Bundled documents (100.0%)3any: 0 of 2 Worked documents (0.0%)·notnot: 425 of 621 Packs documents (68.4%)425not: 0 of 3 Bundled documents (0.0%)·not: 0 of 2 Worked documents (0.0%)·Reusematchesmatches: 332 of 621 Packs documents (53.5%)332matches: 3 of 3 Bundled documents (100.0%)3matches: 0 of 2 Worked documents (0.0%)·matches callmatches call: 6 of 621 Packs documents (1.0%)6matches call: 0 of 3 Bundled documents (0.0%)·matches call: 0 of 2 Worked documents (0.0%)·utils (query)utils (query): 264 of 621 Packs documents (42.5%)264utils (query): 3 of 3 Bundled documents (100.0%)3utils (query): 0 of 2 Worked documents (0.0%)·define.utilsdefine.utils: 100 of 621 Packs documents (16.1%)100define.utils: 0 of 3 Bundled documents (0.0%)·define.utils: 0 of 2 Worked documents (0.0%)·paramsparams: 6 of 621 Packs documents (1.0%)6params: 0 of 3 Bundled documents (0.0%)·params: 0 of 2 Worked documents (0.0%)·define.optionsdefine.options: 87 of 621 Packs documents (14.0%)87define.options: 0 of 3 Bundled documents (0.0%)·define.options: 2 of 2 Worked documents (100.0%)2$option$option: 73 of 621 Packs documents (11.8%)73$option: 0 of 3 Bundled documents (0.0%)·$option: 0 of 2 Worked documents (0.0%)·when.optionwhen.option: 49 of 621 Packs documents (7.9%)49when.option: 0 of 3 Bundled documents (0.0%)·when.option: 0 of 2 Worked documents (0.0%)·constraintsconstraints: 73 of 621 Packs documents (11.8%)73constraints: 0 of 3 Bundled documents (0.0%)·constraints: 0 of 2 Worked documents (0.0%)·comparecomparecompare: 77 of 621 Packs documents (12.4%)77compare: 0 of 3 Bundled documents (0.0%)·compare: 0 of 2 Worked documents (0.0%)·text: sourcetext: source: 31 of 621 Packs documents (5.0%)31text: source: 0 of 3 Bundled documents (0.0%)·text: source: 0 of 2 Worked documents (0.0%)·text: unquotedtext: unquoted: 8 of 621 Packs documents (1.3%)8text: unquoted: 0 of 3 Bundled documents (0.0%)·text: unquoted: 0 of 2 Worked documents (0.0%)·text: valuetext: value: 4 of 621 Packs documents (0.6%)4text: value: 0 of 3 Bundled documents (0.0%)·text: value: 0 of 2 Worked documents (0.0%)·text: keytext: key: 12 of 621 Packs documents (1.9%)12text: key: 0 of 3 Bundled documents (0.0%)·text: key: 0 of 2 Worked documents (0.0%)·text: membertext: member: 2 of 621 Packs documents (0.3%)2text: member: 0 of 3 Bundled documents (0.0%)·text: member: 0 of 2 Worked documents (0.0%)·text: nametext: name: 10 of 621 Packs documents (1.6%)10text: name: 0 of 3 Bundled documents (0.0%)·text: name: 0 of 2 Worked documents (0.0%)·text: referencetext: reference: 2 of 621 Packs documents (0.3%)2text: reference: 0 of 3 Bundled documents (0.0%)·text: reference: 0 of 2 Worked documents (0.0%)·text: reference-no-static-keystext: reference-no-static-keys: 1 of 621 Packs documents (0.2%)1text: reference-no-static-keys: 0 of 3 Bundled documents (0.0%)·text: reference-no-static-keys: 0 of 2 Worked documents (0.0%)·text: tokenstext: tokens: 13 of 621 Packs documents (2.1%)13text: tokens: 0 of 3 Bundled documents (0.0%)·text: tokens: 0 of 2 Worked documents (0.0%)·text: structuretext: structure: 3 of 621 Packs documents (0.5%)3text: structure: 0 of 3 Bundled documents (0.0%)·text: structure: 0 of 2 Worked documents (0.0%)·text: structure-hashtext: structure-hash: 2 of 621 Packs documents (0.3%)2text: structure-hash: 0 of 3 Bundled documents (0.0%)·text: structure-hash: 0 of 2 Worked documents (0.0%)·text: structure-partstext: structure-parts: 1 of 621 Packs documents (0.2%)1text: structure-parts: 0 of 3 Bundled documents (0.0%)·text: structure-parts: 0 of 2 Worked documents (0.0%)·text: nodetext: node: 14 of 621 Packs documents (2.3%)14text: node: 0 of 3 Bundled documents (0.0%)·text: node: 0 of 2 Worked documents (0.0%)·text: indextext: index: 8 of 621 Packs documents (1.3%)8text: index: 0 of 3 Bundled documents (0.0%)·text: index: 0 of 2 Worked documents (0.0%)·commutecommute: 2 of 621 Packs documents (0.3%)2commute: 0 of 3 Bundled documents (0.0%)·commute: 0 of 2 Worked documents (0.0%)·collation (non-ordinal)collation (non-ordinal): 3 of 621 Packs documents (0.5%)3collation (non-ordinal): 0 of 3 Bundled documents (0.0%)·collation (non-ordinal): 0 of 2 Worked documents (0.0%)·countcountcount: 76 of 621 Packs documents (12.2%)76count: 3 of 3 Bundled documents (100.0%)3count: 0 of 2 Worked documents (0.0%)·resolvesresolvesresolves: 198 of 621 Packs documents (31.9%)198resolves: 0 of 3 Bundled documents (0.0%)·resolves: 0 of 2 Worked documents (0.0%)·global: [names]global: [names]: 77 of 621 Packs documents (12.4%)77global: [names]: 0 of 3 Bundled documents (0.0%)·global: [names]: 0 of 2 Worked documents (0.0%)·global: trueglobal: true: 3 of 621 Packs documents (0.5%)3global: true: 0 of 3 Bundled documents (0.0%)·global: true: 0 of 2 Worked documents (0.0%)·import: [names]import: [names]: 113 of 621 Packs documents (18.2%)113import: [names]: 0 of 3 Bundled documents (0.0%)·import: [names]: 0 of 2 Worked documents (0.0%)·import: trueimport: true: 1 of 621 Packs documents (0.2%)1import: true: 0 of 3 Bundled documents (0.0%)·import: true: 0 of 2 Worked documents (0.0%)·importedimported: 6 of 621 Packs documents (1.0%)6imported: 0 of 3 Bundled documents (0.0%)·imported: 0 of 2 Worked documents (0.0%)·prefixprefix: 3 of 621 Packs documents (0.5%)3prefix: 0 of 3 Bundled documents (0.0%)·prefix: 0 of 2 Worked documents (0.0%)·relativerelative: 4 of 621 Packs documents (0.6%)4relative: 0 of 3 Bundled documents (0.0%)·relative: 0 of 2 Worked documents (0.0%)·seenBeforeseenBefore: 2 of 621 Packs documents (0.3%)2seenBefore: 0 of 3 Bundled documents (0.0%)·seenBefore: 0 of 2 Worked documents (0.0%)·thisthis: 3 of 621 Packs documents (0.5%)3this: 0 of 3 Bundled documents (0.0%)·this: 0 of 2 Worked documents (0.0%)·sloppyFunctionssloppyFunctions: 2 of 621 Packs documents (0.3%)2sloppyFunctions: 0 of 3 Bundled documents (0.0%)·sloppyFunctions: 0 of 2 Worked documents (0.0%)·bindingbinding: 58 of 621 Packs documents (9.3%)58binding: 0 of 3 Bundled documents (0.0%)·binding: 0 of 2 Worked documents (0.0%)·binding.site/value/annotationbinding.site/value/annotation: 56 of 621 Packs documents (9.0%)56binding.site/value/annotation: 0 of 3 Bundled documents (0.0%)·binding.site/value/annotation: 0 of 2 Worked documents (0.0%)·binding.onlybinding.only: 10 of 621 Packs documents (1.6%)10binding.only: 0 of 3 Bundled documents (0.0%)·binding.only: 0 of 2 Worked documents (0.0%)·binding.stopAtGlobalbinding.stopAtGlobal: 3 of 621 Packs documents (0.5%)3binding.stopAtGlobal: 0 of 3 Bundled documents (0.0%)·binding.stopAtGlobal: 0 of 2 Worked documents (0.0%)·binding.destructuredValuebinding.destructuredValue: 12 of 621 Packs documents (1.9%)12binding.destructuredValue: 0 of 3 Bundled documents (0.0%)·binding.destructuredValue: 0 of 2 Worked documents (0.0%)·unresolvedunresolved: 9 of 621 Packs documents (1.4%)9unresolved: 0 of 3 Bundled documents (0.0%)·unresolved: 0 of 2 Worked documents (0.0%)·beforeLocalbeforeLocal: 1 of 621 Packs documents (0.2%)1beforeLocal: 0 of 3 Bundled documents (0.0%)·beforeLocal: 0 of 2 Worked documents (0.0%)·write: lastwrite: last: 2 of 621 Packs documents (0.3%)2write: last: 0 of 3 Bundled documents (0.0%)·write: last: 0 of 2 Worked documents (0.0%)·declareddeclared: 2 of 621 Packs documents (0.3%)2declared: 0 of 3 Bundled documents (0.0%)·declared: 0 of 2 Worked documents (0.0%)·directivesdirectives: 21 of 621 Packs documents (3.4%)21directives: 0 of 3 Bundled documents (0.0%)·directives: 0 of 2 Worked documents (0.0%)·scriptDeclarationsscriptDeclarations: 3 of 621 Packs documents (0.5%)3scriptDeclarations: 0 of 3 Bundled documents (0.0%)·scriptDeclarations: 0 of 2 Worked documents (0.0%)·bindsbindsbinds: 25 of 621 Packs documents (4.0%)25binds: 0 of 3 Bundled documents (0.0%)·binds: 0 of 2 Worked documents (0.0%)·formform: 25 of 621 Packs documents (4.0%)25form: 0 of 3 Bundled documents (0.0%)·form: 0 of 2 Worked documents (0.0%)·scopescope: 15 of 621 Packs documents (2.4%)15scope: 0 of 3 Bundled documents (0.0%)·scope: 0 of 2 Worked documents (0.0%)·used / referencedused / referenced: 13 of 621 Packs documents (2.1%)13used / referenced: 0 of 3 Bundled documents (0.0%)·used / referenced: 0 of 2 Worked documents (0.0%)·exportedexported: 4 of 621 Packs documents (0.6%)4exported: 0 of 3 Bundled documents (0.0%)·exported: 0 of 2 Worked documents (0.0%)·latest / outerlatest / outer: 4 of 621 Packs documents (0.6%)4latest / outer: 0 of 3 Bundled documents (0.0%)·latest / outer: 0 of 2 Worked documents (0.0%)·nestedUsenestedUse: 1 of 621 Packs documents (0.2%)1nestedUse: 0 of 3 Bundled documents (0.0%)·nestedUse: 0 of 2 Worked documents (0.0%)·redefinesredefines: 7 of 621 Packs documents (1.1%)7redefines: 0 of 3 Bundled documents (0.0%)·redefines: 0 of 2 Worked documents (0.0%)·bindings (count)bindings (count): 2 of 621 Packs documents (0.3%)2bindings (count): 0 of 3 Bundled documents (0.0%)·bindings (count): 0 of 2 Worked documents (0.0%)·occurrenceoccurrence: 24 of 621 Packs documents (3.9%)24occurrence: 0 of 3 Bundled documents (0.0%)·occurrence: 0 of 2 Worked documents (0.0%)·builtinbuiltin: 2 of 621 Packs documents (0.3%)2builtin: 0 of 3 Bundled documents (0.0%)·builtin: 0 of 2 Worked documents (0.0%)·argumentsargumentsarguments: 37 of 621 Packs documents (6.0%)37arguments: 0 of 3 Bundled documents (0.0%)·arguments: 0 of 2 Worked documents (0.0%)·mode: syntacticmode: syntactic: 12 of 621 Packs documents (1.9%)12mode: syntactic: 0 of 3 Bundled documents (0.0%)·mode: syntactic: 0 of 2 Worked documents (0.0%)·mode: estreemode: estree: 7 of 621 Packs documents (1.1%)7mode: estree: 0 of 3 Bundled documents (0.0%)·mode: estree: 0 of 2 Worked documents (0.0%)·Sub-grammarsparse: percentFormatparse: percentFormat: 6 of 621 Packs documents (1.0%)6parse: percentFormat: 0 of 3 Bundled documents (0.0%)·parse: percentFormat: 0 of 2 Worked documents (0.0%)·parse: braceFormatparse: braceFormat: 2 of 621 Packs documents (0.3%)2parse: braceFormat: 0 of 3 Bundled documents (0.0%)·parse: braceFormat: 0 of 2 Worked documents (0.0%)·parse: annotationparse: annotation: 6 of 621 Packs documents (1.0%)6parse: annotation: 0 of 3 Bundled documents (0.0%)·parse: annotation: 0 of 2 Worked documents (0.0%)·parse: eslintDirectiveparse: eslintDirective: 3 of 621 Packs documents (0.5%)3parse: eslintDirective: 0 of 3 Bundled documents (0.0%)·parse: eslintDirective: 0 of 2 Worked documents (0.0%)·capturecapture: 9 of 621 Packs documents (1.4%)9capture: 0 of 3 Bundled documents (0.0%)·capture: 0 of 2 Worked documents (0.0%)·hosthost: 20 of 621 Packs documents (3.2%)20host: 0 of 3 Bundled documents (0.0%)·host: 0 of 2 Worked documents (0.0%)·ast.embeddedast.embedded: 24 of 621 Packs documents (3.9%)24ast.embedded: 0 of 3 Bundled documents (0.0%)·ast.embedded: 0 of 2 Worked documents (0.0%)·embeddedembedded: 4 of 621 Packs documents (0.6%)4embedded: 0 of 3 Bundled documents (0.0%)·embedded: 0 of 2 Worked documents (0.0%)·formatArgsformatArgs: 9 of 621 Packs documents (1.4%)9formatArgs: 0 of 3 Bundled documents (0.0%)·formatArgs: 0 of 2 Worked documents (0.0%)·transformtransformtransform: 32 of 621 Packs documents (5.2%)32transform: 0 of 3 Bundled documents (0.0%)·transform: 0 of 2 Worked documents (0.0%)·replacereplace: 22 of 621 Packs documents (3.5%)22replace: 0 of 3 Bundled documents (0.0%)·replace: 0 of 2 Worked documents (0.0%)·substringsubstring: 7 of 621 Packs documents (1.1%)7substring: 0 of 3 Bundled documents (0.0%)·substring: 0 of 2 Worked documents (0.0%)·convertconvert: 4 of 621 Packs documents (0.6%)4convert: 0 of 3 Bundled documents (0.0%)·convert: 0 of 2 Worked documents (0.0%)·lineline: 1 of 621 Packs documents (0.2%)1line: 0 of 3 Bundled documents (0.0%)·line: 0 of 2 Worked documents (0.0%)·lineslines: 0 of 621 Packs documents (0.0%)·lines: 0 of 3 Bundled documents (0.0%)·lines: 0 of 2 Worked documents (0.0%)·countcount: 3 of 621 Packs documents (0.5%)3count: 0 of 3 Bundled documents (0.0%)·count: 0 of 2 Worked documents (0.0%)·defaultdefault: 3 of 621 Packs documents (0.5%)3default: 0 of 3 Bundled documents (0.0%)·default: 0 of 2 Worked documents (0.0%)·value: truevalue: true: 11 of 621 Packs documents (1.8%)11value: true: 0 of 3 Bundled documents (0.0%)·value: true: 0 of 2 Worked documents (0.0%)·regex-group sourceregex-group source: 0 of 621 Packs documents (0.0%)·regex-group source: 0 of 3 Bundled documents (0.0%)·regex-group source: 0 of 2 Worked documents (0.0%)·Query optionslanguageOptionslanguageOptions: 45 of 621 Packs documents (7.2%)45languageOptions: 0 of 3 Bundled documents (0.0%)·languageOptions: 0 of 2 Worked documents (0.0%)·ecmaVersionecmaVersion: 0 of 621 Packs documents (0.0%)·ecmaVersion: 0 of 3 Bundled documents (0.0%)·ecmaVersion: 0 of 2 Worked documents (0.0%)·sourceTypesourceType: 20 of 621 Packs documents (3.2%)20sourceType: 0 of 3 Bundled documents (0.0%)·sourceType: 0 of 2 Worked documents (0.0%)·globalsglobals: 17 of 621 Packs documents (2.7%)17globals: 0 of 3 Bundled documents (0.0%)·globals: 0 of 2 Worked documents (0.0%)·parserOptionsparserOptions: 0 of 621 Packs documents (0.0%)·parserOptions: 0 of 3 Bundled documents (0.0%)·parserOptions: 0 of 2 Worked documents (0.0%)·liblib: 1 of 621 Packs documents (0.2%)1lib: 0 of 3 Bundled documents (0.0%)·lib: 0 of 2 Worked documents (0.0%)·jsxPragmajsxPragma: 5 of 621 Packs documents (0.8%)5jsxPragma: 0 of 3 Bundled documents (0.0%)·jsxPragma: 0 of 2 Worked documents (0.0%)·pythonVersionpythonVersion: 0 of 621 Packs documents (0.0%)·pythonVersion: 0 of 3 Bundled documents (0.0%)·pythonVersion: 0 of 2 Worked documents (0.0%)·typingModulestypingModules: 0 of 621 Packs documents (0.0%)·typingModules: 0 of 3 Bundled documents (0.0%)·typingModules: 0 of 2 Worked documents (0.0%)·extendGenericsextendGenerics: 0 of 621 Packs documents (0.0%)·extendGenerics: 0 of 3 Bundled documents (0.0%)·extendGenerics: 0 of 2 Worked documents (0.0%)·whenwhen: 54 of 621 Packs documents (8.7%)54when: 0 of 3 Bundled documents (0.0%)·when: 0 of 2 Worked documents (0.0%)·filefile: 24 of 621 Packs documents (3.9%)24file: 0 of 3 Bundled documents (0.0%)·file: 0 of 2 Worked documents (0.0%)·file.path / declarationfile.path / declaration: 21 of 621 Packs documents (3.4%)21file.path / declaration: 0 of 3 Bundled documents (0.0%)·file.path / declaration: 0 of 2 Worked documents (0.0%)·file module keysfile module keys: 3 of 621 Packs documents (0.5%)3file module keys: 0 of 3 Bundled documents (0.0%)·file module keys: 0 of 2 Worked documents (0.0%)·violationanchor: capture.NAMEanchor: capture.NAME: 82 of 621 Packs documents (13.2%)82anchor: capture.NAME: 0 of 3 Bundled documents (0.0%)·anchor: capture.NAME: 0 of 2 Worked documents (0.0%)·anchor: witness.start/endanchor: witness.start/end: 103 of 621 Packs documents (16.6%)103anchor: witness.start/end: 0 of 3 Bundled documents (0.0%)·anchor: witness.start/end: 0 of 2 Worked documents (0.0%)·anchor: file.startanchor: file.start: 4 of 621 Packs documents (0.6%)4anchor: file.start: 0 of 3 Bundled documents (0.0%)·anchor: file.start: 0 of 2 Worked documents (0.0%)·message $VARmessage $VAR: 120 of 621 Packs documents (19.3%)120message $VAR: 3 of 3 Bundled documents (100.0%)3message $VAR: 0 of 2 Worked documents (0.0%)·message ${VAR}message ${VAR}: 4 of 621 Packs documents (0.6%)4message ${VAR}: 0 of 3 Bundled documents (0.0%)·message ${VAR}: 0 of 2 Worked documents (0.0%)·eacheach: 21 of 621 Packs documents (3.4%)21each: 0 of 3 Bundled documents (0.0%)·each: 0 of 2 Worked documents (0.0%)·fix (template)fix (template): 32 of 621 Packs documents (5.2%)32fix (template): 0 of 3 Bundled documents (0.0%)·fix (template): 0 of 2 Worked documents (0.0%)·fix rangefix range: 3 of 621 Packs documents (0.5%)3fix range: 0 of 3 Bundled documents (0.0%)·fix range: 0 of 2 Worked documents (0.0%)·suppress.noqasuppress.noqa: 159 of 621 Packs documents (25.6%)159suppress.noqa: 0 of 3 Bundled documents (0.0%)·suppress.noqa: 0 of 2 Worked documents (0.0%)·suppress.eslintsuppress.eslint: 266 of 621 Packs documents (42.8%)266suppress.eslint: 0 of 3 Bundled documents (0.0%)·suppress.eslint: 0 of 2 Worked documents (0.0%)·

Reading the matrix: the packs' documents port ESLint and its plugins, typescript-eslint, Ruff, Bandit and the PHP and Blade tools, and the bundled complexity rules use count. A construct no committed document uses here may still be used by the full Ruff and ESLint conformance suites, which --ruff and --eslint add.

8. Not included #

Features the DSL deliberately leaves out.

The ECMAScript regex grammar is not part of the DSL. It would add parse with grammar: "ecmaRegex" (a port of regexpp) for ESLint's regex rules. Those rules are of low product value, the port would be about 3,000 lines of Rust, and the ESLint suite never ported them.

  • ecmaRegex is refused ("not supported yet").
  • The TypeScript parse plumbing it would need exists in a minimal form, built for eslintDirective.

Also not planned:

9. Index and glossary #

Every DSL key with the section that defines it, then the terms this reference uses.

Key index

Glossary

Witness
A node, regex match or fact row a query's rule matched. One violation per witness, or per binding with each.
Candidate
A node the anchor plan hands to the rule: the rule's root kinds, narrowed by text probes. A kindless rule gets every node.
Term
One key of a rule object. An object's terms form an implicit all, run in the order of Rule objects and term order.
Capture, metavariable
A name bound to a node ($A), a node list ($$$A), a parsed tree's text and host range, or a regex group. Written without $ in compare, each, capture, bind.
Span capture, export
A capture a parsed tree (parse, formatArgs.missing) hands to the host: decoded text plus its host range.
Frame
A parameterized utility call's environment: only its parameters, fresh captures per level.
Grammar gap, dialect
A kind, field or pattern one dialect (TypeScript, TSX) cannot express. The rule compiles for the other, and fails only when no dialect can.
Unknown
A verdict the file alone cannot decide: no violation, negated or not.
source-partial
A failure reported by a sound clause on a file with syntax errors: it ran, and witnesses outside the error regions stand.
source-incomplete
A failure: the clause did not run there (or a cross-file source was incomplete). Reported by:
  • an unsound clause on a file with syntax errors;
  • every AST clause on a file whose error regions cover half of it or more (file did not parse: …).
budget-exceeded
A failure: a clause ran out of maxVisitedNodes (or maxVisitsPerNode per node of a larger file), utility depth, its own work allowance or its deadline on a file. Never a false verdict.
  • A rule's own: the audit reports a coverage gap of the file and completes.
  • A scan guardrail, or a file past the byte budget (also budget-exceeded): fails the audit.
file-budget-exceeded
A failure: the file's work stopped a clause, not its own: the file's shared work (parsing, its tape, its anchor index) ran out, or earlier clauses spent the file's clause-work ceiling. A coverage gap, never stored; a later evaluation tries again.
witness-limit
A failure: a clause found more than maxWitnesses witnesses (or regex matches) on a file. It read the file, so this is neither a coverage gap nor a blind file. Fails the audit.
input-limit
A failure: a file is past a rule's input limits (maxSourceBytes, or maxFiles of 0), so the rule did not run on it. Fails the audit; not a coverage gap.
Scope table, usage index
A file's bindings and references under its language's scope rules, and the reads each binding receives. Built lazily, once per file revision and reading.
Module path
A Python file's dotted module (the host's module, a string or a list of names as on disk), else its stem. It qualifies definitions and resolves relative imports.
Port, suite
A code-rule document reproducing another linter's rule (Ruff, ESLint, typescript-eslint). A conformance suite scores it against that linter's fixtures (PASS, PARTIAL, BLOCKED; TP/FP/FN).
Fixture-fit
A port that passes its fixtures with a hand-written approximation that is wrong on unusual forms. Engine features replaced many.
Oracle
An upstream tool (Ruff 0.16.9, ESLint 10.11.0, typescript-eslint 8.70.1, eslint-utils 4.10.1, CPython 3.13, ast-grep 0.45.3) whose output, generated offline and committed, pins a semantics.

Generated: the coverage heatmap and relations figure (coverage --write, figures), and every example quoted from a port (fill). Checks: grammar and examples --engine PATH. All in scripts/generate/docs/code-rule-dsl-reference.mjs.