# 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:

- `format`: always `"rerail.policy/v1"`;
- `id` and a positive integer `revision`;
- optional `description` and `scope` (path globs);
- a non-empty list of **clauses**.

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**:

| Form          | What it holds                                               | What it reports                                                      |
| ------------- | ----------------------------------------------------------- | -------------------------------------------------------------------- |
| forbid        | a [query](#queries): ast, regex, file, or select\_when      | one violation per witness (per binding with [each](#violation-each)) |
| require\_each | two [evidence relations](#g-Evidence), subject and evidence | each projected subject without evidence                              |
| budget        | an evidence relation, group\_by keys and count.lte          | groups over the budget                                               |
| llm           | anything (the schema reads unknown)                         | reserved                                                             |
| define        | shared utils, options and config                            | nothing: 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:

- **`log`** records it.
- **`steer`** reports it to the agent.
- **`block`** acts like `steer` until block enforcement lands.

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`):

- **Planned** language: stored, but refused at compile time with `language-not-supported`; the catalog keeps the rule inactive until the engine reads it.
- **`not-supported-yet`** capability: its predicates are refused likewise.
- **`refused`** capability: its predicates are invalid.

A complete document: one `forbid` clause over a Python `ast` query whose rule is a `kind` and a `regex`.

example document `(whole document)`

```json
{
  "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`

```json
{
  "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 language | Dialects it compiles for    | File labels and extensions                                                | Family |
| ------------------ | --------------------------- | ------------------------------------------------------------------------- | ------ |
| python             | python                      | python: .py, .pyi                                                         | python |
| typescript         | typescript, tsx, javascript | typescript: .ts, .mts, .cts; tsx: .tsx; javascript: .js, .jsx, .mjs, .cjs | ecma   |
| javascript         | javascript                  | javascript: .js, .jsx, .mjs, .cjs                                         | ecma   |
| php                | php                         | php: .php except .blade.php                                               | php    |
| blade              | blade                       | blade: .blade.php                                                         | blade  |
| rust               | rust                        | rust: .rs                                                                 | rust   |
| vue                | vue                         | vue: .vue                                                                 | vue    |
| sql                | sql                         | sql: .sql                                                                 | sql    |

| Capability       | Predicates      | python      | ecma                  | php                   | blade | rust   | vue    | sql      |
| ---------------- | --------------- | ----------- | --------------------- | --------------------- | ----- | ------ | ------ | -------- |
| Scope            | resolves, binds | yes         | yes                   | yes                   | yes   | yes    | yes    | yes      |
| Value            | value, compare  | yes         | yes                   | yes                   | yes   | yes    | yes    | yes      |
| Arguments        | arguments       | yes         | yes                   | yes                   | yes   | yes    | yes    | yes      |
| Calls            | calls           | yes         | no                    | yes                   | yes   | yes    | no     | no       |
| Format arguments | formatArgs      | yes         | no                    | yes                   | yes   | yes    | no     | no       |
| Suppression      | suppress        | noqa, nosec | eslint, oxlint, biome | phpstan, psalm, phpcs | none  | clippy | eslint | sqlfluff |

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.

- **Values**: PHP 8.5 constant expressions. `eq` is `===`; `compare` orders as PHP's operators. `follow` reads the one binding every path gives a variable, and the file's constants.
- **Arguments**: bound as PHP binds them, named arguments and literal `...[...]` unpacking included. `mode: "syntactic"`: by name, else by position before the first unpacking (Ruff's `find_argument` on PHP-Parser's `args`).
- **Names**: classes, functions and constants as PHP-Parser's `NameResolver` (`resolves: { import, prefix }`).
- **Variables**: a flow over control flow. `resolves.unresolved`: no path defines the read (PHPStan's `variable.undefined`). `resolves.global`: superglobals. Usage as Psalm's unused-variable analysis; the dynamic symbol table is unknown.
- **Bindings**: forms `assignment`, `augmented`, `for`, `except`, `parameter`, `global`, `static`; scopes `global`, `function`, `closure`.
- **Compare**: `key`, `member`, `name`, `reference`, `reference-no-static-keys`, `structure`, `structure-hash`, per nikic/PHP-Parser 5.9.0 and PHP 8.5 conversion and name lookup (the modes).
- **Calls**: `calls` follows the workspace's functions and methods.
- **Formats**: printf strings are `printfFormat` trees (PHP 8.5.11's `php_formatted_print`); `formatArgs` style `printf` reads `sprintf`, `printf`, `fprintf`, `v*printf` as PHP and PHPStan's `argument.sprintf` do. `sscanf`/`fscanf` strings are `scanfFormat` trees (PHP 8.5.11's `ValidateFormat`); style `scanf` reads their variables.
- **Doc comments**: `phpDoc` trees (phpstan/phpdoc-parser's AST, as PHPStan 2.2.16 reads it: tags, types, variables and descriptions).
- **Suppression**: `suppress.phpstan`, `suppress.psalm`, `suppress.phpcs`. Blade templates honour none, since no linter reads a template's `{{-- --}}` comments.
- **Refused**: `structure-parts` (PHP has no implicit string concatenation), `mode: "estree"` (ESLint's `node.arguments` of ESTree), Python's format grammars and styles (naming PHP's counterpart), `annotation` (PHP holds no type in a string: its declared types are syntax, its PHPDoc types `phpDoc`).

**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.

- **Template or view**: a Blade rule matches the template with Blade's kinds; with `ast.embedded: "compiled-view"`, the view with PHP's. `embedded` and `host` cross between them.
- **Comments**: template comments (`{{-- --}}`, `<!-- -->`) in a Blade rule; PHP comments under `ast.embedded`, where `kind: "comment"` with `parse: { grammar: "phpDoc" }` reads doc comments (Comments).
- **Patterns** under `ast.embedded` are PHP (`sprintf($F, $$$A)`); a contextual pattern selecting a metavariable is refused.
- **Defined**: `$loop`, `$message`, `$value` and `@inject` variables, where Blade defines them.
- **Unknown**: top-level view data; variables after `@props` or `@aware`; tokens Blade writes but the template lacks (`@unless`'s `!(`); top-level `calls` through `$this`, `self::`, `static::`.
- **Refused**, naming blade and why, as on PHP: `structure-parts`, `mode: "estree"`, `stopAtGlobal: false`, `resolves.write`, `sameImport`, `exported`, `builtin`, `directives`, Python's format grammars and styles, `annotation`, `languageOptions`.
- **Tested** by `fixtures/blade/blade/`: 212 regions, 78 PHPStan reports over 154 reads, 49 constant expressions, 19 templates. 14257 of 14257 copied bytes agree over 128 Laravel templates.

**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:

- **Script block**: `;` over its start tag (which tells blocks apart), its content, `;`.
- **HTML template**: a block `{ … }` over the template element, then per node in document order:  
  - `(E);`: an interpolation `{{ E }}`, a directive value (`:x="E"`, `v-model`, `v-on="{…}"`, a custom `v-foo`…), or a member or function `v-on` handler;
  - `(() => { S });`: an inline handler (`$event` is an unresolved read);
  - `(A);`: a dynamic argument `[A]`;
  - `(x);`: a value-less `:x` (Vue 3.4 shorthand, camelized);
  - `if (C) { … } else if (C2) { … } else { … }`: a `v-if`, `v-else-if`, `v-else` chain;
  - `(SRC), (ALIAS) => { … };`: a `v-for`, around its element (inside a `v-if` on the same element, as in Vue 3);
  - `(P) => { … };`: slot parameters (`v-slot`, `#name`, `slot-scope`, `scope` on a `<template>`), around the children;
  - `Tag;` or `Ns.Tag;`: a component tag that is an identifier or dotted chain (`<ElTable.TableColumn>` is one tag, as vue-eslint-parser reads it).

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

- **Component or program**: a Vue rule matches the component with Vue's kinds (`element`, `start_tag`, `directive_attribute`, `interpolation`…); with `ast.embedded: "script-block"`, the program with its dialect's. `embedded` and `host` cross between them. Comments and patterns split the same way; embedded patterns are TypeScript and match every program dialect.
- **Unspelled tokens**: a node holding a token the view writes but the file lacks (an interpolation's parentheses) is not spelled.
- **Refused**: `calls`, `formatArgs` (as on ECMAScript: vue-eslint-parser hands the scripts to ESLint, which has neither) and all TypeScript refuses. `languageOptions` and `when` take TypeScript's keys.
- **Suppression**: `suppress.eslint`: ESLint directives in script comments and eslint-plugin-vue 10.11.1's template directives (`vue/comment-directive`).
- **Shared scope**: both scripts share module scope, as in vue-eslint-parser 10.4.1\. A `<script>` read of a `<script setup>` name resolves to it.
- **Shorthand**: the identifier spans its argument (`aria-label`) and reads camelized (`ariaLabel`).
- **Template syntax errors** leave the program whole: an `ast.embedded: "script-block"` query, even a source-incomplete one, runs unless the program has its own. The file gets `source-partial`; the program holds what tree-sitter-vue recovers.
- **Parser quirks**: tree-sitter-vue reads `:a :b` as one attribute (the view reads the first) and `</A.B>` as a syntax error.
- **Tested** by `fixtures/vue/language/view/`: 74 template regions and 80 references in 9 components, against @vue/compiler-sfc 3.5.43.

**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.

- **Names** fold as in PostgreSQL: naked to lower case, quoted exact.
- **Scope**: query blocks (scope `query`). `SELECT`, `UPDATE`, `DELETE`, `INSERT` bind `FROM`/`JOIN` relations (form `relation`: alias, else the table name's last part) and the target table (`target`); a `WITH` binds CTE names (`cte`).
- **Resolution**: a column's table qualifier resolves in its block or an enclosing one (SQLFluff's RF01, AL05); a CTE only through a relation selecting from it. `resolves.unresolved`: a `SELECT` qualifier nothing binds (RF01). `binds: { used: false }`: a relation unused in its block and nested ones (AL05).
- **Values**: literals only where PostgreSQL and SQLite agree; `type` refused (PostgreSQL's `pg_typeof` and SQLite's `typeof` name different types). `compare` reads values and names; key, member, reference and structural modes are refused (use `tokens`).
- **Arguments** of an `invocation` bind as in PostgreSQL 18.6's parser: by position, then name (`name => v`, `name := v`); `extract`, `position`, `trim` in PostgreSQL's operand order. Signature markers, `syntactic` and `estree` are refused.
- **Refused**: `calls` (routine bodies are other languages, `$$ … $$`, PL/pgSQL; a `.sql` file has no workspace call graph) and `formatArgs` (ISO SQL has no format function; PostgreSQL's `format()` and SQLite's `printf()` are extensions). No SQLFluff rule reads either.
- **Suppression**: `-- noqa` (`suppress.sqlfluff`). No `parse` grammar, no `languageOptions`.

**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.

- **Visibility**: items in their whole block or module (`extern` functions and statics too); `let` and pattern bindings from where made; generics and `for<'a>` lifetimes in their item or type.
- **Shadowing**: patterns shadow functions and lowercase imports, never constants, unit structs or capitalised imports, which rustc's naming makes a constant, a struct or a variant (`use Ordering::{Less, Greater}`).
- **Isolation**: a nested `fn` sees no outer locals; a module only its parent's macros. Types, values, macros, lifetimes and labels are separate namespaces.
- **Item macros** (`thread_local!`, `lazy_static!`, `bitflags!`: any but std's expression macros) and glob imports may define names, so an otherwise unbound name is not `unresolved`.
- **`resolves.import`**: crate paths (`std::env::var`, `crate::m::f`). Heads go through the file's `use` and `extern crate` items, a `use`'s own head where it stands (`use crate::b as a; use a::f;` imports `crate::b::f`). Cycles, and heads a glob or item macro may bind, give no path.
- **`self::`, `super::`** normalise to `crate::` paths of the file's module, placed by the host's path under the crate's `src/`.
- **Values**: literals as rustc evaluates them, through operators, casts and const blocks: suffix-typed integers of any width, floats, `char`, strings, byte and C strings. `follow` reads the file's `const` items as their declared type. Past 256 nesting levels: unknown.
- **`value.type`**: `bool`, `char`, `&str`, `&[u8]`, `&CStr`, integer and float types; none for an unsuffixed number.
- **Arguments**: positional.
- **Calls**: crate functions by path, `Self::f`/`Type::f` via the crate's `impl` blocks, `self.m()` via the enclosing `impl`. Open: other method calls, trait dispatch, closures, function pointers; `Some`/`Ok`/`Err` where a glob may bind them; calls into a crate with a `#[path]` module.
- **Macros**: unparsed, so their calls are open, never absent. That covers any macro but std's formatting, assertion and constructor macros (`println!`, `format!`, `assert!`, `vec!`, `matches!`, ...), and those holding a call (`println!("{}", f())`).
- **Formats**: `rustFormat` trees; `formatArgs` style `rust` reads `format_args!` macros as rustc 1.99.0 binds them.
- **Suppression**: `suppress.clippy` reads rustc and Clippy lint-level attributes and `#[clippy::msrv]`.
- **Refused**: `languageOptions`, `resolves.imported`, `compare` modes over property names, references, names and structures, `arguments`' signature markers and modes `syntactic`/`estree`, other families' grammars and format styles.
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.

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`.

- **Scaled**: in the workspace, shared work (parsing, tape, anchor index) and each clause's allowance grow with file size. Visits: `maxVisitedNodes` or `maxVisitsPerNode` (16) per node, whichever is more.
- **Isolated**: each clause selects its roots and runs under its own allowance; one that runs out fails alone, reported as a coverage gap, not a failed audit. Fix: simplify the rule, or exclude generated or vendored files from its scope.
- **Ceiling**: one evaluation of a file may spend eight allowances; a clause that finds them spent fails without running.
- **Caching**: running out while evaluating all of a file's clauses is stored. A failure that depends on what else ran (the ceiling, exhausted shared work, a partial evaluation) is not; a later workspace retries it.
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.
- **Explicit `all`** keeps written order: reading a capture only a later item binds is refused ("move all/2 before all/0") unless bound before the `all`.
- **`host`** runs last among a `parse` tree's terms.
- **Kindless rules** (no `kind` or `pattern`) are accepted, unlike upstream ast-grep. They test every node, including anonymous tokens (`==`, `,`, `default`): the only way to report a token.
- **Refused**: abstract kinds (`expression`, a supertype no node carries) and Python pattern variables that are no metavariable (`$At`).
Comments 

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

- **TypeScript, TSX, JavaScript** add `html_comment` (`<!--` and `-->` lines).
- **Blade, Vue**: the template's comments; under `ast.embedded`, the program's (Blade, Vue).
- **Rust**: `line_comment` and `block_comment`; a doc comment has an `outer_doc_comment_marker` or `inner_doc_comment_marker` child and a `doc_comment` text child.

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

| Language                                 | Line                                               | Block                                                        | Doc                                                                                                                                                         |
| ---------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| typescript (typescript, tsx, javascript) | ^//                                                | ^/\\\*(\[^\*\]\|\\\*/)                                       | ^/\\\*\\\*\[^/\] (JSDoc)                                                                                                                                    |
| python                                   | ^# (the only comment; a docstring is a string)     | none                                                         | none                                                                                                                                                        |
| php                                      | ^(#\|//) (#\[ opens an attribute, never a comment) | ^/\\\* and not doc                                           | ^/\\\*\\\*\\s (T\_DOC\_COMMENT; [phpDoc](#phpDoc) parses it)                                                                                                |
| blade                                    | PHP's, under ast.embedded                          | ^\\{\\{-- (Blade), ^<!-- (HTML); PHP's under ast.embedded    | PHP's, under ast.embedded                                                                                                                                   |
| rust                                     | kind: "line\_comment" (^//), not doc               | kind: "block\_comment" (^/\\\*), not doc                     | has: { kind: "outer\_doc\_comment\_marker" } (///, /\*\*) or has: { kind: "inner\_doc\_comment\_marker" } (//!, /\*!); //// and /\*\*\* are no doc comments |
| vue                                      | TypeScript's, under ast.embedded                   | ^<!-- (the template's HTML); TypeScript's under ast.embedded | TypeScript'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`:

- **Leading**: `follows: { stopBy: "neighbor", kind: "comment" }` on the node, or `precedes: { stopBy: "neighbor", kind: … }` on the comment. In TypeScript, a comment before `export function f` precedes the `export_statement`, not the `function_declaration`. `stopBy: { not: { kind: "comment" } }` skips a run of comments.
- **Attached** (no blank line after): `layout: { after: { to: "token", blank: { eq: 0 } } }`. **Trailing** (code before it on the line): `layout: { before: { to: "token", breaks: { eq: 0 } } }`. See layout.
- **Inside**: `inside` and `has` reach only comments within a node's span. Python puts a comment indented after a block's last statement in the block; at the outer indentation, in the outer list.
- **Counted** by `nthChild`, `count`, kindless rules and `$$$` captures. Add `not: { kind: "comment" }` where one could stand.
- **Skipped** by `arguments`, `formatArgs`, `calls`, and `compare`'s `index` and `structure`, as their linters do.

**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.

```json
{
  "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.

```json
{
  "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

#### Document — One code rule: its identity, mode, scope and clauses

- `format`: "rerail.policy/v1" — Always rerail.policy/v1
- `id`: Str — The code rule's id, which rerail-ignore comments name
- `revision`: Int≥1 — The 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?`: String — What the code rule asks for, in plain words
- `scope?`: Scope — The files the code rule reads; every file when omitted
- `clauses`: list of Clause, at least 1 — The 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.

#### Scope — The repository-relative path globs a code rule or evidence relation reads

- `include`: list of Glob, at least 1 — Globs of the files to read
- `exclude`: list of Glob — Globs of files to leave out, applied after include

#### Clause — One check in a document: an id, exactly one form, and its violation

- `id`: Str — The clause's id, unique within the document
- `violation?`: Violation  (required except on define, refused on define) — Where each violation points and what it says; required except on a define
- exactly one form
- `forbid?`: Query — Reports every witness of its query
- `require_each?` — Reports each subject row that no evidence row matches
  - `subject`: Evidence — The evidence relation whose rows must each be matched
  - `evidence`: Evidence — The evidence relation whose rows satisfy a subject, usually through where
- `budget?` — Counts evidence rows per group and reports groups past the limit
  - `query`: Evidence — The evidence relation whose rows are counted
  - `group_by?`: list of Str — Projected alias.field operands that split the rows into groups
  - `count` — The limit each group's count is held to
    - `lte`: Int≥0 — The most rows a group may hold before it is reported
    - `distinct?`: list of Str — Counts distinct values of these operands instead of rows
- `llm?`: any — Reserved for a future clause form
- `define?`: Define — Holds utilities, options and config the other clauses share; reports nothing

#### Violation — Where a clause's violation points, what it says, and how it may be fixed

- `code`: Str — A short code naming the violation, such as SELECT\_ONE
- `anchor`: Anchor — Where the violation points: the witness, its start or end, the file, or a capture
- `message`: Template — The text reported, with $NAME interpolating captures and transform variables
- `fix?`: Template, or: — A suggested replacement for the witness, never applied
  - `template`: Template — The 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?`: NAME — Reports one violation per distinct binding of this capture at a witness
- `suppress?`: Suppress — Honours other linters' suppression comments for this violation
- `variables?`: empty object — Reserved; 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

#### Suppress — Which other linters' suppression comments drop the violation, one entry per linter

- `eslint?` — Honours eslint-disable comments in TypeScript and Vue queries
  - `rules`: list of RuleName, 1 to 256 — The ESLint rules a directive may name; one naming no rule also suppresses
- `noqa?` — Honours Ruff's noqa comments in Python queries
  - `codes`: list of Code, 1 to 256 — The 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
  - `codes`: list of BanditTest, 1 to 256 — The Bandit tests, by id or name, a nosec comment may name
- `oxlint?` — Honours oxlint and eslint disable directives in TypeScript queries
  - `rules`: list of OxlintRuleName, 1 to 256 — The Oxlint rule names a directive may name to suppress
- `biome?` — Honours biome-ignore comments in TypeScript queries
  - `rules`: list of BiomeLintRule, 1 to 256 — The Biome lint rules a biome-ignore comment may name
- `phpstan?` — Honours @phpstan-ignore comments in PHP queries
  - `identifiers`: list of PhpstanIdentifier, 1 to 256 — The PHPStan error identifiers a comment may name
- `psalm?` — Honours @psalm-suppress doc comments in PHP queries
  - `issues`: list of PsalmIssue, 1 to 256 — The Psalm issue types a tag may name, or their parent types
  - `suppressAll?`: Bool — Lets @psalm-suppress all count as a suppression
- `phpcs?` — Honours phpcs:ignore and phpcs:disable comments in PHP queries
  - `sniffs`: list of PhpcsCode, 1 to 256 — The sniff codes a directive may name, or their category or standard
  - `version?`: 3 | 4 — Reads directives as PHP\_CodeSniffer 3 or 4 does; 4 when omitted
- `clippy?` — Honours lint-level attributes such as allow and expect in Rust queries
  - `lints`: list of LintName, 1 to 256 — The lint names an allow or expect attribute may name
  - `msrv?`: RustVersion — The Rust version the lint needs; an older clippy::msrv in force drops it
- `sqlfluff?` — Honours SQLFluff's noqa comments in SQL queries
  - `rules`: list of SqlfluffReference, 1 to 256 — The SQLFluff rule references a noqa entry may name or glob

At least one.

#### Define — A 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 Utility — Named rules every query of the define's language can call with matches
- `options?`: map of OptionName to OptionDecl — Typed options declared once for the whole document
- `config?`: map of OptionName to Json — Values set for declared options; an unset option takes its default

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

#### OptionDecl — One typed option: its type, allowed values and default

- `type`: "boolean" | "string" | "number" | "integer" | "array" — The kind of value the option holds
- `items?`: ScalarType — The type of each item of an array option; string when omitted
- `enum?`: list of Json, at least 1 — The only values, or array items, the option accepts
- `nullable?`: Bool — Also accepts null as the option's value
- `default`: Json — The value used when config leaves the option unset
- `description?`: String — What the option controls, in plain words

#### OptionRef — Stands for an option's value and is replaced by it before validation

- `"$option"`: OptionName — The name of the option whose value replaces the reference
- `cases?`: map of Text to Json — Picks a replacement by the option's value, keyed by its text
- `default?`: Json — The replacement when no case matches the option's value
- `escape?`: "regex" — Escapes the value as regex text
- `map?`: String — A template applied to each array item, {} standing for the item
- `join?`: String — Joins array items into one string with this separator
- `template?`: String — Text to embed the value in, {} standing for the value
- `spread?`: true — Splices an array value into the array holding the reference

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

#### Utility — A named rule that matches calls in place, optionally taking captures as parameters

- an AstRule, or
- `params`: list of NAME, at least 1 — The capture names a call must pass; the utility runs in its own frame
- `rule`: AstRule — The rule the utility matches, reading only its parameters and own captures

- UtilId: Str without ( ) , = — A utility's name, which matches uses to call it

### Queries

#### Query — What a forbid clause matches: an ast, regex or file query, or a select\_when

- exactly one source
- `regex`: RegexQuery — Runs a Rust regex over the file's text or its path
- `ast`: AstQuery — Matches a rule object on a language's syntax tree
- `file`: empty object — Matches every file in scope, with no span
- `select_when` — Reports select's witnesses when exists has a witness in the workspace
  - `select`: Query — The query whose witnesses are reported
  - `exists`: Query — The query that must have at least one witness anywhere

#### RegexQuery — A 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
- `pattern`: Str — The Rust regex; every match is a witness

A Rust regex; named groups are captures.

#### Evidence — A relation of rows from an AST query, projected to named fields for cross-file clauses

- `evidence` — Wraps an AST query whose matches become rows
  - `as`: Str — The alias rows are known by in where and group\_by operands
  - `paths?`: Scope — Path globs limiting the files this relation's query reads
  - `query`: AstQuery — The AST query whose matches are the rows
  - `project`: map of Str to ProjectionSource — The fields each row carries, taken from captures or file metadata
  - `where?`: Predicate — A 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

#### Predicate — A test over projected alias.field operands that filters evidence rows

- exactly one of
- `all`: list of Predicate, at least 1 — Every predicate holds
- `any`: list of Predicate, at least 1 — At least one predicate holds
- `not`: Predicate — The predicate does not hold
- `eq`: two Json values — The two operands are equal
- `ne`: two Json values — The two operands differ

#### AstQuery — A 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?`: Via — Runs the query on the program a Blade or Vue file embeds
- `rule`: AstRule — The rule object matched on every candidate node
- `utils?`: map of UtilId to Utility — Named rules local to this query, called with matches
- `constraints?`: map of NAME to AstRule — Rules each named capture's node must also match after the rule matched
- `languageOptions?`: LanguageOptions — Configuration the file cannot show, such as globals or the parser's settings
- `when?`: When — The configurations the query applies to, gated on option values
- `file?`: File — Conditions on the file the query reads
- `transform?`: map of NAME to Transformation — Variables 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

#### AstRule — A 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
  - `context`: Str — A whole snippet to parse, for fragments that do not parse alone
  - `selector`: Kind — The kind of the sub-node of context that is matched
- `kind?`: Kind — The node's tree-sitter kind
- `regex?`: Str — A Rust regex the node's source text must match
- `nthChild?`: NthChild — The node's 1-based position among its parent's named children
- `capture?`: NAME  (inside parse only) — Inside parse only: binds the parsed node under a name for the host rule
- composite
- `all?`: list of AstRule, at least 1 — Every item matches, in written order
- `any?`: list of AstRule, at least 1 — At least one item matches
- `not?`: AstRule — The 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?`: Relation — An ancestor matches the relation's rule
- `has?`: Relation — A descendant matches the relation's rule
- `precedes?`: Relation — A later sibling matches the relation's rule
- `follows?`: Relation — An earlier sibling matches the relation's rule
- Rerail
- `compare?`: Compare — Compares two captures: their texts, values, names, structures or positions
- `count?`: Count — How many nodes one relation reaches that match its rule, within bounds
- `resolves?`: Resolves — What the node's name resolves to: a global, an import, a binding, or nothing
- `value?`: ValueTest — Tests what the node evaluates to, judged from its own source
- `arguments?`: Arguments — Binds a call's arguments to a signature's named slots
- `binds?`: Binds — The node is a binding's name node and the binding passes the filters
- `parse?`: Parse — Parses a literal's decoded value with a sub-grammar and matches a rule on it
- `formatArgs?`: FormatArgs — A format call's unused arguments or unfilled fields
- `layout?`: Layout — Tests the node's lines, first byte, and lines to a neighbour
- `host?`: AstRule  (inside parse, or over an embedded program) — A 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 parse) — A rule matched on the embedded program a host file's node holds
- `calls?`: Calls — The call reaches, through the call graph, a definition the inner rule matches
- `importTarget?`: ImportTarget  (javascript and typescript; not inside parse) — Tests the import or require target a module specifier names

At least one key.

#### Calls — A 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?`: NAME — Binds a capture to the definitions on the way, for messages
- and every AstRule key but calls: the inner rule, over function definitions

#### Relation — A rule matched on related nodes, with how far the walk goes and which field it enters

- `stopBy?`: "neighbor" | "end" | AstRule — How far the walk goes: the nearest node, every node, or up to a stop rule
- `field?`: Str  (inside and has only) — Restricts 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

#### NthChild — A node's position among its parent's named children, optionally counted over a rule

- Int≥0, AnB, or
- `position`: Int≥0 | AnB — The 1-based position, as a number or CSS An+B
- `ofRule?`: AstRule — Counts only the siblings this rule matches
- `reverse?`: Bool — Counts from the last child

- AnB: CSS An+B, as ast-grep's parse\_an\_b reads it: "2n+1", "-n+3"; "odd" is refused — A CSS An+B formula for nthChild positions, such as 2n+1

#### Count — How many nodes one relation reaches that match its rule, held between min and max

- exactly one relation, without field
- `inside?`: Relation — Counts matching ancestors
- `has?`: Relation — Counts matching descendants, descending below each match
- `precedes?`: Relation — Counts matching later siblings
- `follows?`: Relation — Counts matching earlier siblings
- bounds
- `min?`: Int≥0 — The fewest matches allowed; 0 when omitted
- `max?`: Int≥0 — The most matches allowed
- `as?`: NAME — Binds the count plus offset as text, for the message only
- `offset?`: Int — A 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

#### Compare — Compares two captures by text, value, name, structure, identity or list position

- `left`: NAME — The 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
- `right`: NAME — The 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 only) — Binary 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.

#### ValueTest — Predicates over what an expression evaluates to, judged from its own source

- `eq?`: Str | Num | Bool | null — The value equals this, in the language's sense of equality
- `type?`: TypeName, or a list of 1 to 1024 — The value's type name is this one or one listed
- `constant?`: Bool — Whether the whole value is known; alone, asks whether it is known
- `startsWith?`: Str, or a list of 1 to 1024 — The decoded string starts with this text or one listed
- `regex?`: Str — The decoded string matches this regex, backreferences allowed
- `bitsAny?`: Int≥1 — The integer shares at least one bit with this mask
- `gt?`: Num — The number is greater than this
- `ge?`: Num — The number is greater than or equal to this
- `lt?`: Num — The number is less than this
- `le?`: Num — The number is less than or equal to this
- `truthy?`: Bool — Whether the value is truthy, as the language's bool or ToBoolean reads it
- `follow?`: Bool — Reads a name's value through its binding, up to 16 names deep
- `name?`: Bool  (true only with eq, startsWith or regex) — Makes 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.

#### Arguments — Binds a call's arguments to the named slots of a signature, as the language binds them

- `signature`: list of up to 256: Ident, "/" or "\*" — The parameter slots in order; / ends positional-only slots, \* starts keyword-only
- `bind?`: map of NAME to Slot — Binds the argument passed to each slot as a capture; an unpassed slot fails
- `absent?`: list of Slot, at least 1 — Slots 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

#### Resolves — Whether a reference resolves to a global, an import, a local binding, or nothing

- `global?`: list of 1 to 1024 Ident, or true — A reference to a listed (or any) global or builtin that nothing local shadows
- `import?`: list of 1 to 1024 Dotted, RustPath or PhpName, or true — A name or attribute chain whose qualified name, through imports, is listed
- `imported?`: list of 1 to 1024 Dotted, RustPath or PhpName — An import site whose imported qualified name is listed
- `prefix?`: true — A listed name also matches every name under it
- `relative?`: "never" | "text" — Whether a relative import is named by its text without the dots
- `seenBefore?`: true — The node comes after an absolute import of a listed module
- `directives?`: "eslint" — Reads ESLint /\* global \*/ comments as declaring or removing globals
- `declared?`: Bool — false asks only that nothing shadows the name, declared as a global or not
- `scriptDeclarations?`: true — In 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?`: true — Also a this owned by an ordinary function that is not strict
- `binding?`: ResolvesBinding — A 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?`: true — A read in a function before the function's own later binding of the name

The resolves cards say which keys combine.

- RustPath: Ident, then :: Ident any number of times — A Rust path from a crate, segments joined by ::, as import lists it
- PhpName: an optional \\, then Label, then \\ Label any number of times — A 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).

#### ResolvesBinding — Filters, 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

#### Binds — Matches 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 1 — The binding construct: assignment, import, parameter, definition and so on
- `scope?`: ScopeKind, or a list of ScopeKind, at least 1 — The kind of scope that holds the binding
- `used?`: Bool — Whether some read resolves to the binding
- `referenced?`: Bool — Whether some reference, writes included, resolves to the binding
- `builtin?`: Bool — Whether it is a configured global's variable in a script
- `directives?`: "eslint" — Counts ESLint /\* exported \*/ comments as exports
- `nestedUse?`: Bool — Whether the binding is read from another scope
- `exported?`: Bool — Whether the binding is explicitly re-exported or exported
- `latest?`: Bool — Whether this is the live binding of the name
- `outer?`: Bool — Whether the binding was made under global or nonlocal
- `bindings?` — How many bindings of the name its scope holds
  - `within?`: NAME — Counts only the bindings inside this capture
  - `min?`: Int≥0 — The fewest bindings of the name the scope may hold
  - `max?`: Int≥0 — The most bindings of the name the scope may hold
- `site?`: AstRule — A rule over the construct that binds
- `value?`: AstRule — A rule over the value the binding carries
- `annotation?`: AstRule — A rule over the binding's annotation, without its colon
- `occurrence?`: AstRule — A rule over some definition name or reference of the variable
- `redefines?`: Redefines — The binding shadows or redeclares an earlier one that passes these terms

- Form: assignment | annotated | annotation | augmented | named | for | with | except | import | definition | parameter | global | match | typeParameter | typeAlias | implicit | deletion | field | method | accessor | relation | cte | target — The binding constructs form can name, per language
- ScopeKind: python: module | class | function | lambda | comprehension | typeParameters · typescript: global | module | function | class | block | namespace | typeParameters | functionName · sql: query — The scope kinds scope can name, per language

#### Redefines — Terms 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?`: AstRule — A rule over the construct that made the shadowed binding
- `value?`: AstRule — A rule over the value the shadowed binding carries
- `annotation?`: AstRule — A rule over the shadowed binding's annotation
- `sameScope?`: Bool — Whether the shadowed binding is in the same scope
- `sameImport?`: Bool — Whether both bindings import the same qualified name
- `every?`: Bool — Every other definition must pass, not just some earlier one

### Sub-grammars, layout and imports

#### Parse — Parses 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.

#### FormatArgs — A 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
- `string`: NAME — The capture holding the format string
- `args`: NAME — The capture holding the arguments passed after the format
- `unused?`: AstRule — A rule matched on the arguments no field consumes
- `missing?`: AstRule — A 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.

#### Layout — Tests the node's line span, its start offset, and the lines between it and a neighbour

- `span?`: LineCount — The line breaks inside the node, 0 on one line
- `start?`: LineCount — The byte offset of the node's first byte, 0 at the file's start
- `before?`: Gap — The gap from the previous neighbour's end to the node's start
- `after?`: Gap — The gap from the node's end to the next neighbour's start

At least one.

#### LineCount — A count of lines, exact or within bounds

- `eq`: Int≥0 — The count is exactly this
- or
- `min?`: Int≥0 — The count is at least this
- `max?`: Int≥0 — The count is at most this

#### Gap — The 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?`: Bool — A comment counts as the neighbour instead of being passed over
- `edge?`: Bool — With no neighbour, the file's start or end stands in
- `breaks?`: LineCount — The line breaks between the two
- `blank?`: LineCount — The lines between the two that no token or comment touches

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

#### ImportTarget — The 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 1 — The specifier's module type is this one or one listed
- `builtin?`: Bool — Whether the specifier is a Node built-in module
- `resolved?`: Bool — Whether the target has a file path
- `failed?`: Bool — Whether resolution failed
- `extensionSpelled?`: Bool — Whether the specifier spells the extension file-extension-in-import expects
- `published?`: Bool — Whether npm publishes the file the target names
- `devDependency?`: Bool — Whether the module is listed only in devDependencies
- `declared?`: Bool — Whether the importing package declares the module as a dependency
- `aliased?`: Bool — Whether 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

#### Transformation — One transform variable: a single operation over a source's text

**replace** — Replaces every match of a regex in the text 
- `source`: Src — The capture, variable or regex group the text comes from
- `default?`: String — The text an unbound source reads as before the operation
- `value?`: Bool — Reads the capture's value as a string instead of its source text
- `replace`: Str — The regex whose every match is replaced
- `by`: String — The replacement text, expanding $1 and ${name}
**substring** — Keeps the characters between two positions 
- `source`: Src — The capture, variable or regex group the text comes from
- `default?`: String — The text an unbound source reads as before the operation
- `value?`: Bool — Reads the capture's value as a string instead of its source text
- `startChar?`: Int — The first character kept; negative counts from the end
- `endChar?`: Int — The character the substring stops before; negative counts from the end
**convert** — Converts the text to a case, splitting it into words 
- `source`: Src — The capture, variable or regex group the text comes from
- `default?`: String — The text an unbound source reads as before the operation
- `value?`: Bool — Reads the capture's value as a string instead of its source text
- `toCase`: Case — The case to convert the text to
- `separatedBy?`: list of Separator — Where to split words, by default at every separator
**line** — The 1-based line of the capture's start or end 
- `source`: Src — The capture, variable or regex group the text comes from
- `default?`: String — The 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
**lines** — The number of lines the capture spans 
- `source`: Src — The capture, variable or regex group the text comes from
- `default?`: String — The text an unbound source reads as before the operation
**count** — The number of named nodes a $$$ capture holds 
- `source`: Src — The capture, variable or regex group the text comes from
- `default?`: String — The text an unbound source reads as before the operation
**toPrecision** — The number formatted as Number.prototype.toPrecision does 
- `source`: Src — The capture, variable or regex group the text comes from
- `default?`: String — The text an unbound source reads as before the operation
- `value?`: Bool — Reads the capture's value as a string instead of its source text
- `digits`: 1 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
- Case: lowerCase | upperCase | capitalize | camelCase | snakeCase | kebabCase | pascalCase — The cases convert can produce
- Separator: caseChange | dash | dot | slash | space | underscore — The word boundaries convert can split at

### Query options

#### LanguageOptions — Configuration the file cannot show, after ESLint's languageOptions and Ruff's settings

- typescript
- `ecmaVersion?`: "latest" | 3 | 5 | 6 to 17 | 2015 to 2026 — The 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 16384 — Extra global names, each readonly, writable or off
- `parserOptions?` — ESLint parser options
  - `ecmaFeatures?` — ESLint's ecmaFeatures flags
    - `globalReturn?`: Bool — The top level is a function scope
    - `impliedStrict?`: Bool — Every file is strict mode code
- `jsxPragma?`: Ident | null — The variable a JSX element reads, React by default; null for none
- `jsxFragmentName?`: Ident | null — The variable a JSX fragment reads
- `lib?`: list of LibName — typescript-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 Dotted — Extra modules treated as typing, as Ruff's lint.typing-modules
- `extendGenerics?`: list of up to 256 Dotted — Extra generic base classes, as Ruff's lint.pyflakes.extend-generics

#### When — The configurations a query applies to; otherwise it matches no file

- `pythonVersion?`: Range of PyVersion — The configured Python version lies in this inclusive range
- `ecmaVersion?`: Range of EcmaVersion — The configured ECMAScript version lies in this inclusive range
- `sourceType?`: SourceType, or a list of SourceType, at least 1 — The source type, configured or the file's own, is this one or one listed
- `ecmaFeatures?` — The query's ecmaFeatures flags equal these
  - `globalReturn?` — The query's globalReturn flag equals this
  - `impliedStrict?` — The query's impliedStrict flag equals this
- `option?`: map of OptionName to Condition — Each named document option's value meets its condition

#### Range — An inclusive version range with at least one bound

- `min?`: T — The lowest version included
- `max?`: T — The highest version included

At least one; min not after max.

#### Condition — A 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
- `in`: list of Json — The value equals one of these
- `includes`: Scalar — The array option holds this item
- `not`: Condition — The condition does not hold

#### File — Conditions on the file the query reads; otherwise the query skips the file

- `path?`: TextCond — Tests the path the host sends
- `declaration?`: Bool  (ts) — Whether the file is a TypeScript declaration file
- `module?`: TextCond — Tests the Python module path, its names joined by dots
- `moduleName?`: TextCond — Tests the last name of the Python module path
- `package?`: TextCond  (python) — Python: tests the top package name; TypeScript: tests the nearest package.json
- `inPackage?`: Bool  (python) — Whether the host sent a Python module path
- `package?`: Package  (ts) — Python: tests the top package name; TypeScript: tests the nearest package.json
- `jsxRuntime?`: list of "react" | "other" | "unknown"  (ts) — The file's JSX runtime is one listed
- `byteOrderMark?`: Bool — Whether the file's text opens with a byte order mark

#### TextCond — A regex test on a text

- `regex?`: Str — The text matches this regex
- `notRegex?`: Str — The text does not match this regex

At least one.

#### Package — Conditions on the file's nearest package.json

- `bin?`: Bool — Whether the file is one of the package's bin targets
- `nodeVersion?` — Tests the package's configured Node version range
  - exactly one of
  - `subsetOf`: SemverRange — The configured Node range is a subset of this npm range
  - `notSubsetOf`: SemverRange — The configured Node range is not a subset of this npm range
- `dependsOn?`: list of Str — The package lists one of these names among its dependencies
- `path?`: TextCond — Tests the file's path relative to the package's folder
- `serverGraph?`: Bool — Whether the file is in the Next.js App Router server module graph
- `private?`: Bool — Whether the package is private
- `published?`: Bool — Whether 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)`

```json
{
  "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.

```json
{
  "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.

```json
{
  "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.

```json
{
  "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)`

```json
{
  "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

```json
{
  "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

```json
{
  "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.

```json
{
  "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`.

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
[
  {
    "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

```json
{
  "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.

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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).

```json
{
  "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.

```json
{ "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`.

```json
{ "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**:

- `stopBy: "neighbor"` (default): the nearest ones only.
- `"end"`: every node along the axis.
- A stop rule: up to and including the first node it matches.
- `field`: `has` tests one child; `inside` only ancestors entered through it.

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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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.

```json
{
  "kind": "statement_block",
  "regex": "^\\{\\s*\\}",
  "inside": { "all": [{ "kind": "method_definition" }, "…"] }
}
```

After: told by field.

```json
{
  "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`

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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.

```json
{
  "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

```json
[
  {
    "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

```json
[
  {
    "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

```json
[
  {
    "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.

```json
[
  {
    "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

```json
{
  "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.

| text                     | Lang   | Compares                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | Ops   |
| ------------------------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
| source                   | both   | the captures' source text (default)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | all   |
| unquoted                 | both   | the text without one pair of enclosing quotes; escapes stay                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | all   |
| value                    | both   | what the expressions evaluate to, as [value](#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 strings                                                                                                                                                                                                                                                                                                                                                                                               | all   |
| key                      | ts php | TypeScript: 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 unknown | all   |
| member                   | ts php | TypeScript: 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 fold                                                                                                                                                                                                                                                                                                               | all   |
| name                     | ts php | TypeScript: 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 text                                                                                                                                                       | all   |
| reference                | ts php | TypeScript: 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-keys | ts php | reference 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 |
| tokens                   | both   | leaf tokens, ignoring comments and white space (ESLint's equalTokens). With commute, listed binary operators compare in either order                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | eq ne |
| structure                | py php | Python: 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-hash           | py php | Python: 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 structure                                                                                                                                                                                                                                                                                                                                                                                     | eq ne |
| structure-parts          | py     | Ruff's ComparableLiteral: implicitly concatenated strings part by part ("a" "b" != "ab" ""). PHP refuses it: no implicit concatenation                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | eq ne |
| node                     | both   | whether both captures are the same node (range and kind), not just the same text                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | eq ne |
| index                    | both   | each 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 false                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | all   |

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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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

| Port   | Mode            | Size           | TP/FP/FN         | Eval           |
| ------ | --------------- | -------------- | ---------------- | -------------- |
| PIE796 | structure       | 38.9 → 19.1 KB | 45/1/21 → 66/0/0 | 0.57 → 0.42 ms |
| SIM116 | structure-parts | 35.2 → 11.3 KB | 14/0/1 → 15/0/0  | 8.99 → 2.71 ms |
| F601   | structure-hash  | 11.5 → 4.3 KB  | 89/1/6 → 95/0/0  | 0.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

```json
{
  "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.

```json
{ "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

```json
{
  "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.

```json
{
  "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

```json
{
  "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

```json
{
  "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.

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).

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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($$$)`.

```json
{
  "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

```json
{
  "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

```json
[
  {
    "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.

```json
{
  "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

```json
{
  "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

```json
{
  "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

| Port     | New key                       | Size           | Ruff fixtures                 |
| -------- | ----------------------------- | -------------- | ----------------------------- |
| B008     | only                          | 42.5 → 21.0 KB | 10/0/0 PASS both              |
| ASYNC220 | syntactic, stopAtGlobal, node | 9.5 → 6.6 KB   | 8/0/0 PASS both; probes clean |
| SIM113   | unpacking, node               | 19.6 → 14.5 KB | 7/0/1 → 8/0/0 PASS            |
| ASYNC230 | unpacking (no JSON change)    | 7.3 KB         | 11/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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
[
  {
    "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

```json
{
  "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

| Port | Before                                                                                                            | After                                                                                                        |
| ---- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| F841 | structural walk over assignments, utilities that climb scopes and search later reads by text; fixture-fit, 148 KB | binds with forms, scope: \["function", "lambda"\], latest, outer: false, used: false, plus Ruff's exclusions |
| F401 | per-name import enumerations and read searches; BLOCKED on a grammar gap                                          | one binds term (above)                                                                                       |
| F811 | 113 KB of pairwise structural matching                                                                            | binds.redefines, one term per pair Ruff's Binding::redefines allows                                          |
| F821 | 51 KB of scope climbing                                                                                           | resolves: { unresolved: true }                                                                               |
| F842 | a structural scope walk, 44 KB                                                                                    | binds with form: "annotation" (below)                                                                        |

Ruff's `F842` as committed.

ruff/F842.json `/checks/0` · Ruff conformance port

```json
{
  "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`.

```json
{
  "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.

```json
{
  "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

```json
{
  "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 `*`).

```json
{
  "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.

```json
{
  "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`.

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 `capture`s 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

```json
{
  "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

```json
{
  "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

```json
{
  "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.

```json
{
  "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 `parse`s 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.

```json
{
  "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.

```json
{
  "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.

```json
{
  "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.

```json
{
  "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.

```json
{
  "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 `ValueError`s.
- 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

```json
{
  "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.

```json
{
  "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

| Port        | Before                                                                         | After                                                                     |
| ----------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
| F504 / F505 | BLOCKED (format-string-parse); only "no keyed placeholder at all" ported, 6 KB | formatArgs unused: { kind: pair } / missing: placeholder has mapping\_key |
| F522        | BLOCKED; only "no named field", 7 KB                                           | formatArgs unused: { kind: keyword\_argument }                            |
| F507 / F523 | placeholder-count enumerations by regex, 78–79 KB each                         | formatArgs with unused and missing                                        |
| B005        | 128 ASCII alternatives, non-ASCII repeats missed, 5 KB                         | value: { "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

```json
{
  "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

```json
{
  "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`).

```json
{
  "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

```json
{
  "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`.

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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.

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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

```json
{
  "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).

```json
{
  "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.

- At the root, unknown is no violation.
- Under `not` it stays unknown, so a negated rule never reports what it could not decide.
- Only clauses that read values or usedness can be unknown. Every other rule matches exactly as two-valued ast-grep would.

| Combinator              | Verdict                                                                                                                                                                      |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| not X                   | true ↔ false; unknown stays unknown                                                                                                                                          |
| any \[..\]              | a true branch decides; else unknown if any branch is; else false                                                                                                             |
| all \[..\], rule object | a 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 |
| constraints             | the first failing constraint's verdict (unknown before false is unknown)                                                                                                     |

| Walk                         | Verdict                                                                            |
| ---------------------------- | ---------------------------------------------------------------------------------- |
| relation, rule binds nothing | a later match decides past unknown candidates                                      |
| relation, rule binds         | the first unknown candidate no other term makes false ends the search unknown      |
| stop rule unknown            | the walk ends open                                                                 |
| count                        | true count in \[matches, matches + unknowns\]; unknown when that straddles a bound |

**Sources of unknown:**

- `value` on an expression it cannot decode.
- `compare` in the `value`, `member`, structural and transform-of-value readings.
- `arguments` in `language` mode past a starred argument or `**kw`.
- The usage filters of `binds` and `resolves.binding` under `locals()`, a dynamic `__all__`, a direct `eval` or `with`.
- `parse` and `formatArgs` on literals they cannot read.
- `nthChild.ofRule` over an unknown sibling.

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`):

| Construct                                                  | Class                   | Why                                                                                                                                                              |
| ---------------------------------------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| pattern, kind, regex, capture, arguments                   | sound                   | read the witness itself                                                                                                                                          |
| has, not has, field on has                                 | sound                   | read inside the witness; a stop rule on has classifies as the stop rule negated                                                                                  |
| inside, precedes, follows (positive, no stop rule)         | sound                   | the related node is a fact of the recovered tree; stopBy: "end" only widens along the same axis                                                                  |
| the same, negated, or with a stop rule                     | incomplete              | claims absence over nodes the parser may not have read                                                                                                           |
| count                                                      | as the relation negated | claims presence and absence at once: sound over has, incomplete over outward relations                                                                           |
| compare, formatArgs, constraints                           | sound / incomplete      | read 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 |
| value                                                      | sound                   | reads the node's text; builtin names are unknown in a broken file                                                                                                |
| value.follow, resolves, binds, nthChild, parse: annotation | incomplete              | read bindings, siblings or the scope table anywhere in the file                                                                                                  |
| resolves: { imported } alone, positive                     | sound                   | reads its import statement; one broken before the name imports nothing                                                                                           |
| layout span/start, positive gaps                           | sound                   | the neighbour is a fact; negated gaps are incomplete                                                                                                             |
| parse with a text grammar                                  | sound                   | reads the literal alone; a host rule is as sound as on its own, incomplete negated                                                                               |
| utilities, calls                                           | as their rule           | walked once per negation and position                                                                                                                            |
| regex queries, fact queries                                | sound                   | only 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:**

- An outward relation's _first_ binding can change around an error region; a rule reading that capture stays classified sound.
- tree-sitter's false positives make `resolves` rules incomplete there (13 `web/src` files, mostly `&` in JSX text).

## 5\. Captures 

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

What binds, what reads 

| Binds                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | Reads                                                                                                                                                                                                                                                                                                                                                                  | Neither                                                                                                            |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| 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 
- **Atomic terms** bind first and never read a relation's capture.
- **The ordered group** is a stable topological sort (`compile/order.rs`). A term that reads a capture runs after every sibling that binds it, unless an atomic term bound it already; otherwise terms keep ast-grep's order. Reading means a comparison side, a `within`, what a `not` would bind, a transform variable's roots, or a gap's target.
- **Late terms** read everything bound before them.
- **`any`** reads what any branch reads, even where another branch binds the name.
- **`matches`** binds what its utility binds, inside another utility too. A call binds and reads its parameters by the caller's names.
- **`host`** reads the host captures as they stood when its `parse`, or the `embedded` around it, began (an atomic term): not a sibling relation's, and none under `ast.embedded`. It binds nothing; compare a host capture with an inner one inside it.
- **`embedded`**'s rule sees no host capture; what it binds is exported to the host rule.
Environments and frames 

| Construct                                                   | Environment                                                                                                                                              |
| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| relation's rule, all, any, plain utility, binding sub-rules | the caller's: reads its bindings; what it binds comes back                                                                                               |
| not                                                         | the caller's plus its own; nothing leaves it                                                                                                             |
| count                                                       | reads the caller's; keeps nothing it binds                                                                                                               |
| stop rule                                                   | fresh: reads and binds nothing of the caller's                                                                                                           |
| parameterized call                                          | a **frame**: only the parameters (copied in), fresh captures per level; parameters bound inside come back under the caller's names                       |
| parse                                                       | the tree's own; captures are exported to the host as span captures (text + host range)                                                                   |
| embedded                                                    | fresh, over the program; its captures are exported to the host rule's environment (text + file span)                                                     |
| host                                                        | the 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, transform                                      | after the whole rule matched, over the witness's bindings                                                                                                |

Pitfalls 
- **A relation that binds stops at an unknown candidate.** If only existence matters, write `count: { ..., "min": 1 }`, which searches on.
- **A relation binds the first node its walk reaches:** `has` in document order, `inside` outwards, `precedes` forwards, `follows` backwards. A later sibling's comparison sees only that binding. Enumerate with `each`, or move the comparison into the relation's rule so the walk goes on to the first node whose captures compare.
- **An explicit `all` is a sequence.** Write the binder first; the compiler refuses the reverse.
- **Negation reads.** A `not` naming a capture a sibling binds tests the bound value; a `not` with a fresh name binds nothing outward.
- **A frame's captures are its own.** A message cannot name `$YP` bound inside a parameterized utility; return it through a parameter.
- **Python dict literals in port generators** (`{**base, "not": …}`) drop a repeated key before any JSON exists. The engine refuses repeated keys only in raw JSON and YAML.
- **A fix after a `$$$` capture** that can end in a line comment lands inside the comment.

## 6\. Known limits 

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

Matching, ordering and budgets 
- Kindless rules test every node, anonymous tokens included: add `kind` unless the rule targets a token. Abstract kinds are refused.
- With a zero-width node in the file, the sibling check is quadratic in one container's siblings; it crosses the 500 ms budget at several thousand.
- Atomic terms run first and never read a relation's capture; the ordered group sorts by dependency; terms reading each other's captures are refused. The `all` order check refuses only misorders it can prove: a binding outside the rule object excuses a conflict.
- A binding relation's search ends at the first unknown candidate no other term makes false. `all` goes on past an unknown term; terms mentioning its would-be captures stay open.
- `each` reaches what a first-match search reaches by skipping reported bindings, not bindings needing two sub-rules to change together.
- Utility recursion: depth 256 or 1 MiB of stack (a debug build runs out after roughly 60 hops); each hop spends a step. At most 1,024 distinct parameterized calls per query and grammar.
- A fix outside its witness is dropped, not widened.
Names and bindings 
- Resolution is lexical and per file: no flow-sensitive bindings, types, other files or TypeScript `import` resolution. An unconditional Python `del` unbinds by order (Ruff's `Deletion`); a conditional one does not.
- A name has a value only via a binding that carries one: plain or annotated assignment, walrus, an unpacked name paired with a display element, a TypeScript single-definition declarator nothing writes. Bindings are flow-insensitive; at most 16 names per decode. ESLint's `ReferenceTracker` follows every write; Rerail models only the effectively constant case.
- `resolves` binds a name at the reference's position. Ruff resolves some checks elsewhere: annotations at the call site, B008 defaults and RET503/F811 at the body or module end, SIM220–223 over the whole bool-op.
- In a Python scope declaring a name `global`, it is never a `global` match. Python 2's `print >> x` is a keyword token, not a `print` reference. Notebook builtins are not modelled; only `unresolved` knows `__path__`.
- A walrus in class bases binds in the enclosing scope (Ruff: the class's type scope); in a deferred annotation, after the module (Ruff: after function bodies too). A comprehension read after the code that follows it reads its names at one position.
- In a stub, a callee or subscript in a function body resolves against imports before the function (Ruff reads bodies after the module).
- Usedness is unknown under Python `locals()` or a dynamic `__all__` (Ruff reports F401 there), and for TypeScript bindings a direct `eval` or `with` can see.
- Python only: `latest`, `outer`. TypeScript only: `occurrence`, `builtin`, `redefines.every`, `directives` on a filter, `write`, `file.declaration`, `lib`, private forms.
- `only` counts the bindings the table records (Ruff also counts a builtin it materialized). `binds` has no `only`; `stopAtGlobal` is `resolves.binding`'s alone. `redefines` compares forks by the innermost branch; ports approximate Ruff's `TYPE_CHECKING` dominance and `@overload` by qualified name. A `nonlocal` binding redefines nothing.
- Private names are bindings of `binds` only. tree-sitter-typescript 0.23 does not parse `accessor` fields.
- Python identifiers are NFKC-normalized wherever names are resolved or compared, as in CPython and Ruff (`𝒞` is `C`, `F821_28`). Kept as written: string contents (`__all__` entries, `**{…}` keys, format fields), violations, spans, `regex`, `source`/`tokens` text, and names a code rule lists.
- ast-grep's matcher compares pattern identifiers and twice-bound metavariables by source text, so `$X == $X` misses `𝒞 == C`; use `compare` with `structure`. ECMAScript and PHP names compare as written. Identifier tests use Rust's current Unicode tables, newer than `esutils`'.
- Declared TypeScript globals are ESLint's configured globals plus, with `lib`, typescript-eslint's lib variables (none without `lib`). `readonly`/`writable` are not told apart. Among conflicting `/* global */` comments, `off` wins; ESLint takes the last.
- `sloppyFunctions` models strictness, not which calls bind `this`. `parserOptions` reads only `globalReturn` and `impliedStrict`.
- `resolves.imported` reads a relative module by text only. `seenBefore` places deferred annotations, string annotations and a stub's class bases where written. A dotted name qualifies nothing; `file.module` cannot tell a dotted name from two names.
Values, arguments and comparison 
- TypeScript folds only the allow-listed builtin calls in `calls.rs`. Not folded: `new`, non-builtin functions, transcendental `Math`, `Object.keys`/`values`/`entries`, `Date`, `Symbol`, `Map`/`Set`, URI functions, `toExponential`, callback methods, an array's or object's ToPrimitive.
- Python folds nine builtin constructors (not `frozenset`); no `%` formatting, complex division or powers.
- Unknown by design: f-strings with a field, unknown `\N{…}` names, lone surrogates, Python ints past 4,096 bits and BigInts past 128, strings past 64 KiB, nesting past 256.
- `value` holds only on expressions. `key` is ESLint core's (a nameless key equals no key). `member` is unknown wherever a key does not fold, so "both unknown" is never equal (typescript-eslint's `hasOverloadSignatures` calls it equal).
- `tokens` compares leaf texts, not token types. JavaScript orders follow the relational operators while `eq` stays `Object.is` (`null <= 0` but not `eq`); no loose-equality mode.
- `arguments` has no variadic slot and ignores arguments without a slot. `estree` has no "every argument equals the parameter at its index" form.
- The structural modes are Python's. `structure-parts` reads parts only at the top of a capture. Comments are ignored (SIM114 compares them separately).
- JSX attribute strings decode as in typescript-estree, but tree-sitter-typescript's JSX string grammar rejects an entity name with a digit (`&sup2;`). JSX text has no value.
Sub-grammars, layout, transforms and suppression 
- `parse` holds on literal nodes only and reads only `str` literals; f-strings with a field, `t` strings and bytes are unknown. `braceFormat` reads one level of nested fields. Where Ruff and CPython differ, the grammars follow Ruff. Python `re` patterns are not parsed; `ecmaRegex` is not planned.
- Rules run over host documents only: Ruff's expression rules inside string type definitions need a port's `parse: annotation`. One witness per host node, so a port reports one undefined name per string (with `each`, one per capture). Type comments and doctests are not read. Three differences in semantic syntax errors of complex strings remain.
- `eslintDirective` has no items for `eslint` and `eslint-env` comments; a setting lies outside its item's range; an item's `resolves` takes `global` alone.
- A `host` rule binds nothing outward and cannot read a sibling relation's capture.
- An embedded program's fix applies only where its range lies in one stretch copied from the file. Ranges over bytes the lowering wrote (an interpolation's parentheses, PHP Blade's wrapping of directive arguments) carry no fix.
- `embedded` reads a language's one `via`, and its rule sees no host capture. Compare host and program captures inside a `host` rule, or in the host rule after `embedded` exports them.
- `layout` reads lines, not columns; a token is a tree-sitter leaf; `blank` treats the file's edge as a token; a layout on a parsed node is false. curly's `isCollapsedOneLiner` ends a body before a trailing `;`, which `span` counts.
- With bare `\r` line endings, tree-sitter-python's comment token swallows the code after it (a grammar gap).
- `transform` has no `rewrite`, string form, length or arithmetic. Variables compare as text only; `value` converts scalars only. A transform of a regex group is text computed per witness, so no comparison reads it. `${NAME}` is Rerail's (ast-grep renders it literally).
- `count` counts a `$$$` capture's named nodes, not its comments or line continuations; it cannot count a relation's matches.
- `nthChild` and `index` count tree-sitter's named children and commas; `field` names tree-sitter's fields.
- `suppress` is opt-in, applied at the anchor's start. Not modelled: ESLint's unused-directive reports and configuration comments, Ruff's range suppressions, RUF100, code redirects, `UnusedPsalmSuppress`, `phpcs:set`.
Configuration, reuse and the catalog 
- Language options are per ast query: scoring one port under many configurations compiles a variant per configuration. `typingModules`/`extendGenerics` build a table per setting. `when` gates on configured values only. `file.inPackage` reads whether the host sent a module path.
- A parameterized call passes captures only. A frame's captures are not the query's, and the query's `transform` is not readable in a frame.
- Every query reaching a shared utility compiles it; its `all` order conflicts are settled only where it is called.
- Options resolve before validation, and a gated query is still compiled, so every reference must be valid in every configuration. `cases` keys numbers by JavaScript's `String(n)` (`1.0` selects `"1"`); integral option numbers beyond ±(253−1) are refused.
- Grammar gaps: tree-sitter-python 0.25 has no PEP 810 lazy imports. tree-sitter-typescript 0.23 does not parse `using` declarations and misparses `` f<T>`…` `` and a few ASI-dependent lines. tree-sitter-vue (ce8011a) reads two value-less shorthands in a row (`:a :b`) as one attribute and a dotted end tag (`</A.B>`) as a syntax error.
- A Vue script in a language other than JavaScript or TypeScript (`coffee`) embeds no program; a template in pug or with a `src` embeds none of its expressions.
- An older Rerail breaks on a newer catalog row (the strict `codeRuleSchema`), since one `~/.rerail/rerail.sqlite` serves every worktree; no tolerant read is done.

## 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.

- **Packs**: 621 documents in `packs/*/rules`
- **Bundled**: 3 documents in `packs/complexity/rules`
- **Worked**: 2 documents in `fixtures/*/compile/examples`

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:

- flow-sensitive bindings;
- per-file `select_when`;
- cross-file analysis and types;
- shared utilities across documents.

## 9\. Index and glossary 

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

Key index 
absent arguments anchor all annotation (grammar) annotation (binding sub-rule) any ast (query) beforeLocal bind braceFormat budget binding bindings (count) binds biome builtin capture clauses collation commute compare config constraints constant declared declaration default (transform) define destructuredValue directives each ecmaVersion embedded eslintDirective eq exported extendGenerics fact field file file.start fix fix.range follow follows forbid form formatArgs global globals has host import imported importTarget inPackage index inside jsxPragma key kind languageOptions latest layout lib line, lines, count (transform) llm matches member message mode module, moduleName, package name nestedUse node noqa nosec oxlint not nthChild occurrence only $option options outer params parse parserOptions path pattern percentFormat phpDoc precedes prefix printfFormat rustFormat scanfFormat pythonVersion redefines referenced reference regex (rule) regex (query) relative require\_each resolves scope (document) scope (binding) scriptDeclarations seenBefore select\_when site sloppyFunctions sourceType source, unquoted stopAtGlobal stopBy structure, structure-hash, structure-parts suppress this tokens transform truthy, type, startsWith, bitsAny, gt/ge/lt/le typingModules unresolved used utils value (predicate) value (compare text) value (transform) when when.option write 
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`.
