For AI agents: this page as Markdown · llms.txt · JSON Schema
Rerail code-rule DSL reference
Every construct of rerail.policy/v1: the rule keys, relations, combinators and predicates the native engine compiles, with their JSON shape, semantics, capture behaviour, three-valued behaviour, soundness under syntax errors, language support and limits, each mapped to the rule documents committed in the rule packs and the bundled code rules. For complete, tested rules that hold a codebase's design in place, start with the architecture rule examples.
Sources of truth. Accepted JSON: src/ (zod) and src/ (types). Semantics: native/. 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/).
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";idand a positive integerrevision;- optional
descriptionandscope(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: ast, regex, file, or select_when | one violation per witness (per binding with each) |
require_each | two evidence relations, 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:
logrecords it.steerreports it to the agent.blockacts likesteeruntil 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-yetcapability: its predicates are refused likewise.refusedcapability: its predicates are invalid.
forbid clause over a Python ast query whose rule is a kind and a regex.(whole document){
"format": "rerail.policy/v1",
"id": "example:model-config-plain-dict",
"revision": 1,
"mode": "steer",
"description": "Use ConfigDict() rather than a plain dict for Pydantic model configuration.",
"scope": {
"include": ["**/*.py"],
"exclude": [
"**/node_modules/**",
"**/.venv/**",
"**/vendor/**",
"**/__pycache__/**",
"tmp/**"
]
},
"clauses": [
{
"id": "model-config-plain-dict",
"forbid": {
"ast": {
"language": "python",
"rule": {
"kind": "assignment",
"regex": "(?s)^model_config\\s*(?::[^=]*)?=\\s*(?:\\{|dict\\s*\\()"
}
}
},
"violation": {
"code": "MODEL_CONFIG_PLAIN_DICT",
"anchor": "witness.span",
"message": "Pydantic model_config must be a ConfigDict(...) rather than a plain dict so the keys are type-checked."
}
}
]
}regex query over the file's text./clauses/0{
"id": "uuid7-wrap",
"forbid": { "regex": { "pattern": "(?m)\\w+Id\\(uuid7\\(\\)\\)", "source": "file.content" } },
"violation": {
"anchor": "witness.span",
"code": "UUID7_WRAP",
"message": "Prefer the branded factory API over manual uuid7 wrapping."
}
}Language support #
Rerail follows one language standard: a code-rule language names the dialects it compiles for; its family supplies semantics. Structural terms work in every dialect: kind, pattern, regex, relations, field, nthChild, count, layout, matches. A term the family lacks is refused at compile time by the engine and zod, with its path and code-rule language.
| Code-rule 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.
eqis===;compareorders as PHP's operators.followreads 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'sfind_argumenton PHP-Parser'sargs). - 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'svariable.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; scopesglobal,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:
callsfollows the workspace's functions and methods. - Formats: printf strings are
printfFormattrees (PHP 8.5.11'sphp_formatted_print);formatArgsstyleprintfreadssprintf,printf,fprintf,v*printfas PHP and PHPStan'sargument.sprintfdo.sscanf/fscanfstrings arescanfFormattrees (PHP 8.5.11'sValidateFormat); stylescanfreads their variables. - Doc comments:
phpDoctrees (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'snode.argumentsof 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 typesphpDoc).
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., the view with PHP's.embedded: "compiled-view" embeddedandhostcross between them. - Comments: template comments (
{{-- --}},<!-- -->) in a Blade rule; PHP comments underast.embedded, wherekind: "comment"withparse: { grammar: "phpDoc" }reads doc comments (Comments). - Patterns under
ast.embeddedare PHP (sprintf($F, $$$A)); a contextual pattern selecting a metavariable is refused. - Defined:
$loop,$message,$valueand@injectvariables, where Blade defines them. - Unknown: top-level view data; variables after
@propsor@aware; tokens Blade writes but the template lacks (@unless's!(); top-levelcallsthrough$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 customv-foo…), or a member or functionv-onhandler;(() => { S });: an inline handler ($eventis an unresolved read);(A);: a dynamic argument[A];(x);: a value-less:x(Vue 3.4 shorthand, camelized);if (C) { … } else if (C2) { … } else { … }: av-if,v-else-if,v-elsechain;(SRC), (ALIAS) => { … };: av-for, around its element (inside av-ifon the same element, as in Vue 3);(P) => { … };: slot parameters (v-slot,#name,slot-scope,scopeon a<template>), around the children;Tag;orNs.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…); withast., the program with its dialect's.embedded: "script-block" embeddedandhostcross 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.languageOptionsandwhentake 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.query, even a source-incomplete one, runs unless the program has its own. The file getsembedded: "script-block" source-partial; the program holds what tree-sitter-vue recovers. - Parser quirks: tree-sitter-vue reads
:a :bas one attribute (the view reads the first) and</A.B>as a syntax error. - Tested by
fixtures/: 74 template regions and 80 references in 9 components, against @vue/compiler-sfc 3.5.43.vue/ language/ view/
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/). Where dialects differ, PostgreSQL wins.
- Names fold as in PostgreSQL: naked to lower case, quoted exact.
- Scope: query blocks (scope
query).SELECT,UPDATE,DELETE,INSERTbindFROM/JOINrelations (formrelation: alias, else the table name's last part) and the target table (target); aWITHbinds 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: aSELECTqualifier nothing binds (RF01).binds: { used: false }: a relation unused in its block and nested ones (AL05). - Values: literals only where PostgreSQL and SQLite agree;
typerefused (PostgreSQL'spg_typeofand SQLite'stypeofname different types).comparereads values and names; key, member, reference and structural modes are refused (usetokens). - Arguments of an
invocationbind as in PostgreSQL 18.6's parser: by position, then name (name => v,name := v);extract,position,trimin PostgreSQL's operand order. Signature markers,syntacticandestreeare refused. - Refused:
calls(routine bodies are other languages,$$ … $$, PL/pgSQL; a.sqlfile has no workspace call graph) andformatArgs(ISO SQL has no format function; PostgreSQL'sformat()and SQLite'sprintf()are extensions). No SQLFluff rule reads either. - Suppression:
-- noqa(suppress.sqlfluff). Noparsegrammar, nolanguageOptions.
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 (
externfunctions and statics too);letand pattern bindings from where made; generics andfor<'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
fnsees 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 notunresolved. resolves.import: crate paths (std::env::var,crate::m::f). Heads go through the file'suseandextern crateitems, ause's own head where it stands (use crate::importsb as a; use a:: f; crate::b::f). Cycles, and heads a glob or item macro may bind, give no path.self::,super::normalise tocrate::paths of the file's module, placed by the host's path under the crate'ssrc/.- 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.followreads the file'sconstitems 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::fvia the crate'simplblocks,self.m()via the enclosingimpl. Open: other method calls, trait dispatch, closures, function pointers;Some/Ok/Errwhere 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:
rustFormattrees;formatArgsstylerustreadsformat_args!macros as rustc 1.99.0 binds them. - Suppression:
suppress.clippyreads rustc and Clippy lint-level attributes and#[clippy::msrv]. - Refused:
languageOptions,resolves.imported,comparemodes over property names, references, names and structures,arguments' signature markers and modessyntactic/estree, other families' grammars and format styles.
Evaluation pipeline #
For each file in a clause's scope, the engine:
- picks the grammar matcher, skipping the file if
when,fileor the language fail; - narrows nodes to candidates via the rule's anchor plan;
- matches the rule object at each candidate: three-valued (true, false, unknown);
- applies the soundness gate to files with syntax errors;
- drops suppressed violations and renders the rest.
The stateless evaluator (CompiledCodeRuleDsl., 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:
maxVisitedNodesormaxVisitsPerNode(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:
- Atomic terms:
pattern,kind,regex,capture,parse,nthChild(withoutofRule),arguments. - The ordered group:
nthChildwithofRule,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. - Late terms:
count,compare,layout,value; thenresolves(butbinding/write), which binds and reads nothing; thenimportTarget, which binds nothing and reads other files.
- Explicit
allkeeps written order: reading a capture only a later item binds is refused ("move all/2 before all/0") unless bound before theall. hostruns last among aparsetree's terms.- Kindless rules (no
kindorpattern) 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_commentandblock_comment; a doc comment has anouter_doc_comment_markerorinner_doc_comment_markerchild and adoc_commenttext 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 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_ (///, /**) or has: { kind: "inner_ (//!, /*!); //// 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, orprecedes: { stopBy: "neighbor", kind: … }on the comment. In TypeScript, a comment beforeexport function fprecedes theexport_statement, not thefunction_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:
insideandhasreach 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. Addnot: { kind: "comment" }where one could stand. - Skipped by
arguments,formatArgs,calls, andcompare'sindexandstructure, 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/. 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.
export statement.{
"language": "typescript",
"rule": {
"kind": "export_statement",
"has": { "kind": "function_declaration" },
"not": { "follows": { "stopBy": "neighbor", "kind": "comment", "regex": "^/\\*\\*[^/]" } }
}
}/// <reference types="x" /> directive (a /// comment before the first statement) whose x a static import of the file names. The transform reads x out of the comment and the comparison runs inside the walk over the imports' specifiers.{
"id": "reference-types-imported",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"all": [
{ "kind": "comment" },
{ "pattern": "$C" },
{
"regex": "^///[ \\t]*<reference[ \\t](?:[^>\\n]*[ \\t])?types[ \\t]*=[ \\t]*[\"'](?<NAME>[^\"'\\n]*)[\"'][^\\n]*?/>"
},
{
"not": {
"follows": {
"stopBy": "end",
"not": { "any": [{ "kind": "comment" }, { "kind": "hash_bang_line" }] }
}
}
},
{
"inside": {
"kind": "program",
"has": {
"stopBy": "end",
"kind": "string_fragment",
"pattern": "$S",
"inside": {
"kind": "string",
"inside": {
"any": [{ "kind": "import_statement" }, { "kind": "import_require_clause" }]
}
},
"compare": { "left": "T", "op": "eq", "right": "S" }
}
}
}
]
},
"transform": {
"T": {
"replace": {
"source": "$C",
"replace": "^///[ \\t]*<reference[ \\t](?:[^>\\n]*[ \\t])?types[ \\t]*=[ \\t]*[\"']([^\"'\\n]*)[\"'][\\s\\S]*$",
"by": "$1"
}
}
}
}
},
"violation": {
"anchor": "capture.NAME",
"code": "NO_REFERENCE_IMPORT",
"message": "No need to reference \"$T\", since it is imported."
}
}2. Grammar #
The accepted JSON, production by production. Each production says in one line what it is; open it to see its keys, one per line, each with its type and what it does. key? is optional, a nested object is indented under its key, and an object refuses any key it does not list. A key's name links to the card in section 3 that explains it. A | B are alternatives, Str a non-empty string, NAME a capture name ([A-Z0-9_]+, written without $); "list of T" is a JSON array and "map of K to V" an object used as a map. A note under a production is a rule the schema adds (at least one key, one form, per-language keys).
For tools and AI agents: every production is in this page's HTML, open or closed. The same grammar is published as a JSON Schema, rerail.policy.v1.schema.json, generated from src/ (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/ checks every data-zod annotation names an object in that file and that its documented keys match.
Documents and clauses
DocumentOne code rule: its identity, mode, scope and clauses
- format"rerail.policy/v1"Always rerail.policy/v1
- idStrThe code rule's id, which rerail-ignore comments name
- revisionInt≥1The document's revision number, starting at 1
- mode"log" | "steer" | "block"What Rerail does with a violation: log it, steer the agent, or block
- description?StringWhat the code rule asks for, in plain words
- scope?ScopeThe files the code rule reads; every file when omitted
- clauseslist of Clause, at least 1The clauses that report violations or define shared utilities and options
Clause ids are unique; at least one clause besides defines; $option references are resolved first.
ScopeThe repository-relative path globs a code rule or evidence relation reads
ClauseOne check in a document: an id, exactly one form, and its violation
- idStrThe clause's id, unique within the document
- violation?Violation required except on define, refused on defineWhere each violation points and what it says; required except on a define
- exactly one form
- forbid?QueryReports every witness of its query
- require_each?Reports each subject row that no evidence row matches
- budget?Counts evidence rows per group and reports groups past the limit
- llm?anyReserved for a future clause form
- define?DefineHolds utilities, options and config the other clauses share; reports nothing
ViolationWhere a clause's violation points, what it says, and how it may be fixed
- codeStrA short code naming the violation, such as SELECT_ONE
- anchorAnchorWhere the violation points: the witness, its start or end, the file, or a capture
- messageTemplateThe text reported, with $NAME interpolating captures and transform variables
- fix?Template, or:A suggested replacement for the witness, never applied
- each?NAMEReports one violation per distinct binding of this capture at a witness
- suppress?SuppressHonours other linters' suppression comments for this violation
- variables?empty objectReserved; only an empty object is accepted
SuppressWhich other linters' suppression comments drop the violation, one entry per linter
- eslint?Honours eslint-disable comments in TypeScript and Vue queries
- ruleslist of RuleName, 1 to 256The ESLint rules a directive may name; one naming no rule also suppresses
- noqa?Honours Ruff's noqa comments in Python queries
- nosec?Honours Bandit's nosec comments in Python queries
- codeslist of BanditTest, 1 to 256The Bandit tests, by id or name, a nosec comment may name
- oxlint?Honours oxlint and eslint disable directives in TypeScript queries
- ruleslist of OxlintRuleName, 1 to 256The Oxlint rule names a directive may name to suppress
- biome?Honours biome-ignore comments in TypeScript queries
- ruleslist of BiomeLintRule, 1 to 256The Biome lint rules a biome-ignore comment may name
- phpstan?Honours @phpstan-ignore comments in PHP queries
- identifierslist of PhpstanIdentifier, 1 to 256The PHPStan error identifiers a comment may name
- psalm?Honours @psalm-suppress doc comments in PHP queries
- issueslist of PsalmIssue, 1 to 256The Psalm issue types a tag may name, or their parent types
- suppressAll?BoolLets @psalm-suppress all count as a suppression
- phpcs?Honours phpcs:ignore and phpcs:disable comments in PHP queries
- clippy?Honours lint-level attributes such as allow and expect in Rust queries
- sqlfluff?Honours SQLFluff's noqa comments in SQL queries
- ruleslist of SqlfluffReference, 1 to 256The SQLFluff rule references a noqa entry may name or glob
At least one.
DefineA clause that reports nothing and holds shared utilities, options and config
- language?"python" | "typescript" | "javascript" | "php" | "blade" | "rust" | "sql" | "vue"The language whose queries see the shared utilities; required with utils
- utils?map of UtilId to UtilityNamed rules every query of the define's language can call with matches
- options?map of OptionName to OptionDeclTyped options declared once for the whole document
- config?map of OptionName to JsonValues set for declared options; an unset option takes its default
At least one of utils, options, config; language is required with utils.
OptionDeclOne typed option: its type, allowed values and default
- type"boolean" | "string" | "number" | "integer" | "array"The kind of value the option holds
- items?ScalarTypeThe type of each item of an array option; string when omitted
- enum?list of Json, at least 1The only values, or array items, the option accepts
- nullable?BoolAlso accepts null as the option's value
- defaultJsonThe value used when config leaves the option unset
- description?StringWhat the option controls, in plain words
OptionRefStands for an option's value and is replaced by it before validation
- "$option"OptionNameThe name of the option whose value replaces the reference
- cases?map of Text to JsonPicks a replacement by the option's value, keyed by its text
- default?JsonThe replacement when no case matches the option's value
- escape?"regex"Escapes the value as regex text
- map?StringA template applied to each array item, {} standing for the item
- join?StringJoins array items into one string with this separator
- template?StringText to embed the value in, {} standing for the value
- spread?trueSplices an array value into the array holding the reference
Anywhere but format, id, revision, options and config.
UtilityA named rule that matches calls in place, optionally taking captures as parameters
Queries
QueryWhat a forbid clause matches: an ast, regex or file query, or a select_when
- exactly one source
- regexRegexQueryRuns a Rust regex over the file's text or its path
- astAstQueryMatches a rule object on a language's syntax tree
- fileempty objectMatches every file in scope, with no span
- select_whenReports select's witnesses when exists has a witness in the workspace
RegexQueryA Rust regex over the file's text or path; named groups become captures
- source"file.content" | "file.path"Whether the regex reads the file's content or its path
- patternStrThe Rust regex; every match is a witness
A Rust regex; named groups are captures.
EvidenceA relation of rows from an AST query, projected to named fields for cross-file clauses
- evidenceWraps an AST query whose matches become rows
- asStrThe alias rows are known by in where and group_by operands
- paths?ScopePath globs limiting the files this relation's query reads
- queryAstQueryThe AST query whose matches are the rows
- projectmap of Str to ProjectionSourceThe fields each row carries, taken from captures or file metadata
- where?PredicateA predicate over projected alias.field operands that rows must pass
PredicateA test over projected alias.field operands that filters evidence rows
AstQueryA rule object matched on a language's syntax tree, with the options around it
- language"python" | "typescript" | "javascript" | "php" | "blade" | "rust" | "sql" | "vue"The language whose files the query reads and parses
- embedded?ViaRuns the query on the program a Blade or Vue file embeds
- ruleAstRuleThe rule object matched on every candidate node
- utils?map of UtilId to UtilityNamed rules local to this query, called with matches
- constraints?map of NAME to AstRuleRules each named capture's node must also match after the rule matched
- languageOptions?LanguageOptionsConfiguration the file cannot show, such as globals or the parser's settings
- when?WhenThe configurations the query applies to, gated on option values
- file?FileConditions on the file the query reads
- transform?map of NAME to TransformationVariables computed from captures for messages, fixes and comparisons
A via of the language's registry embeds; the query runs on that embedded program.
Rule objects and relations
AstRuleA rule object: terms that must all hold at a node, matched as ast-grep plus Rerail's terms
- atomic
- pattern?Str, or:A code snippet whose shape the node must have; $NAME binds a node
- kind?KindThe node's tree-sitter kind
- regex?StrA Rust regex the node's source text must match
- nthChild?NthChildThe node's 1-based position among its parent's named children
- capture?NAME inside parse onlyInside parse only: binds the parsed node under a name for the host rule
- composite
- all?list of AstRule, at least 1Every item matches, in written order
- any?list of AstRule, at least 1At least one item matches
- not?AstRuleThe rule does not match
- matches?UtilId, or a map of UtilId to a map of NAME to "$NAME"Calls a named utility, which matches as if written in place
- relational
- inside?RelationAn ancestor matches the relation's rule
- has?RelationA descendant matches the relation's rule
- precedes?RelationA later sibling matches the relation's rule
- follows?RelationAn earlier sibling matches the relation's rule
- Rerail
- compare?CompareCompares two captures: their texts, values, names, structures or positions
- count?CountHow many nodes one relation reaches that match its rule, within bounds
- resolves?ResolvesWhat the node's name resolves to: a global, an import, a binding, or nothing
- value?ValueTestTests what the node evaluates to, judged from its own source
- arguments?ArgumentsBinds a call's arguments to a signature's named slots
- binds?BindsThe node is a binding's name node and the binding passes the filters
- parse?ParseParses a literal's decoded value with a sub-grammar and matches a rule on it
- formatArgs?FormatArgsA format call's unused arguments or unfilled fields
- layout?LayoutTests the node's lines, first byte, and lines to a neighbour
- host?AstRule inside parse, or over an embedded programA rule matched on the host document's node that this node stands in
- embedded?AstRule over a host document whose language embeds one program; not inside parseA rule matched on the embedded program a host file's node holds
- calls?CallsThe call reaches, through the call graph, a definition the inner rule matches
- importTarget?ImportTarget javascript and typescript; not inside parseTests the import or require target a module specifier names
At least one key.
CallsA call-graph term: the callee reaches a definition the inner rule matches
RelationA rule matched on related nodes, with how far the walk goes and which field it enters
- stopBy?"neighbor" | "end" | AstRuleHow far the walk goes: the nearest node, every node, or up to a stop rule
- field?Str inside and has onlyRestricts has and inside to one grammar field
- and every AstRule key: pattern?, kind?, regex?, nthChild?, all?, any?, not?, matches?, inside?, has?, precedes?, follows?, compare?, count?, resolves?, value?, arguments?, binds?, parse?, capture?, formatArgs?, layout?, host?, embedded?, calls?, importTarget? Any AstRule key, applied to the related node
NthChildA node's position among its parent's named children, optionally counted over a rule
CountHow many nodes one relation reaches that match its rule, held between min and max
- exactly one relation, without field
- inside?RelationCounts matching ancestors
- has?RelationCounts matching descendants, descending below each match
- precedes?RelationCounts matching later siblings
- follows?RelationCounts matching earlier siblings
- bounds
- min?Int≥0The fewest matches allowed; 0 when omitted
- max?Int≥0The most matches allowed
- as?NAMEBinds the count plus offset as text, for the message only
- offset?IntA number added to the count that as binds
min ≥ 1 or a max; min ≤ max; offset needs as; as binds offset + the exact count as text, for the message only.
Comparing, values and arguments
CompareCompares two captures by text, value, name, structure, identity or list position
- leftNAMEThe capture on the left of the comparison
- op"lt" | "le" | "eq" | "ne" | "gt" | "ge"The operator: less, less or equal, equal, not equal, greater, greater or equal
- rightNAMEThe capture on the right of the comparison
- collation?"ordinal" | "fold-lower" | "fold-upper" | "lowercase-first" | "natural" | "natural-fold-lower"How two strings order: ordinal by default, case-folded or natural
- text?"source" | "unquoted" | "value" | "key" | "member" | "name" | "reference" | "reference-no-static-keys" | "tokens" | "structure" | "structure-hash" | "structure-parts" | "node" | "index"What is compared, the source text by default; the modes table lists every mode
- commute?list of Str, at least 1 text tokens onlyBinary operators whose operands may compare in either order, with tokens
Equality texts (tokens, structure*, node, reference*) take eq and ne and the ordinal collation only; index takes ordinal.
ValueTestPredicates over what an expression evaluates to, judged from its own source
- eq?Str | Num | Bool | nullThe value equals this, in the language's sense of equality
- type?TypeName, or a list of 1 to 1024The value's type name is this one or one listed
- constant?BoolWhether the whole value is known; alone, asks whether it is known
- startsWith?Str, or a list of 1 to 1024The decoded string starts with this text or one listed
- regex?StrThe decoded string matches this regex, backreferences allowed
- bitsAny?Int≥1The integer shares at least one bit with this mask
- gt?NumThe number is greater than this
- ge?NumThe number is greater than or equal to this
- lt?NumThe number is less than this
- le?NumThe number is less than or equal to this
- truthy?BoolWhether the value is truthy, as the language's bool or ToBoolean reads it
- follow?BoolReads a name's value through its binding, up to 16 names deep
- name?Bool true only with eq, startsWith or regexMakes eq, startsWith and regex test what the node names instead of its value
At least one predicate besides follow and name; type names are per language.
ArgumentsBinds a call's arguments to the named slots of a signature, as the language binds them
- signaturelist of up to 256: Ident, "/" or "*"The parameter slots in order; / ends positional-only slots, * starts keyword-only
- bind?map of NAME to SlotBinds the argument passed to each slot as a capture; an unpassed slot fails
- absent?list of Slot, at least 1Slots that must not be passed
- mode?"language" | "syntactic" | "estree"How arguments fill slots: the language's own rules, syntactic, or ESTree positions
bind or absent; slots are unique and in the signature; "/" before "*"; a bound slot is not absent.
Names and bindings
ResolvesWhether a reference resolves to a global, an import, a local binding, or nothing
- global?list of 1 to 1024 Ident, or trueA reference to a listed (or any) global or builtin that nothing local shadows
- import?list of 1 to 1024 Dotted, RustPath or PhpName, or trueA name or attribute chain whose qualified name, through imports, is listed
- imported?list of 1 to 1024 Dotted, RustPath or PhpNameAn import site whose imported qualified name is listed
- prefix?trueA listed name also matches every name under it
- relative?"never" | "text"Whether a relative import is named by its text without the dots
- seenBefore?trueThe node comes after an absolute import of a listed module
- directives?"eslint"Reads ESLint /* global */ comments as declaring or removing globals
- declared?Boolfalse asks only that nothing shadows the name, declared as a global or not
- scriptDeclarations?trueIn a script, a listed name bound at the top level counts as the global
- this?"global" | "globalScope"A this whose value is the global object, in ESLint no-eval or no-alert's sense
- sloppyFunctions?trueAlso a this owned by an ordinary function that is not strict
- binding?ResolvesBindingA reference to a file-local binding that passes the filters and sub-rules
- write?"last"Where ESLint's no-unused-vars reports a variable: its last write
- unresolved?true | "star"A reference with no binding, builtin or declared global; star: a star import may bind it
- beforeLocal?trueA read in a function before the function's own later binding of the name
The resolves cards say which keys combine.
Label: a letter, _ or a non-ASCII character, then those or digits; a name is at most 256 UTF-8 bytes. Dotted in python, PhpName in php and blade, RustPath in rust (import only).
ResolvesBindingFilters, sub-rules and lookup options for the binding a reference resolves to
- filters, as in Binds: form?, scope?, used?, referenced?, builtin?, directives?, nestedUse?, exported?, latest?, outer?, bindings? The binds filters, applied to the binding the reference reads
- sub-rules: site?, value?, annotation?, occurrence? AstRule Rules over the binding construct, its value, annotation or an occurrence
- lookup: stopAtGlobal?, only?, destructuredValue? Bool Which binding is read: its scope's global, the only one, or a destructured value
BindsMatches a binding's name node where a linter reports it, if the binding passes the filters
- form?Form, or a list of Form, at least 1The binding construct: assignment, import, parameter, definition and so on
- scope?ScopeKind, or a list of ScopeKind, at least 1The kind of scope that holds the binding
- used?BoolWhether some read resolves to the binding
- referenced?BoolWhether some reference, writes included, resolves to the binding
- builtin?BoolWhether it is a configured global's variable in a script
- directives?"eslint"Counts ESLint /* exported */ comments as exports
- nestedUse?BoolWhether the binding is read from another scope
- exported?BoolWhether the binding is explicitly re-exported or exported
- latest?BoolWhether this is the live binding of the name
- outer?BoolWhether the binding was made under global or nonlocal
- bindings?How many bindings of the name its scope holds
- site?AstRuleA rule over the construct that binds
- value?AstRuleA rule over the value the binding carries
- annotation?AstRuleA rule over the binding's annotation, without its colon
- occurrence?AstRuleA rule over some definition name or reference of the variable
- redefines?RedefinesThe binding shadows or redeclares an earlier one that passes these terms
RedefinesTerms the earlier binding, the one a binding shadows or redeclares, must pass
- filters, as in Binds: form?, scope?, used?, referenced?, builtin?, directives?, nestedUse?, exported?, latest?, outer?, bindings? The binds filters, applied to the shadowed binding
- site?AstRuleA rule over the construct that made the shadowed binding
- value?AstRuleA rule over the value the shadowed binding carries
- annotation?AstRuleA rule over the shadowed binding's annotation
- sameScope?BoolWhether the shadowed binding is in the same scope
- sameImport?BoolWhether both bindings import the same qualified name
- every?BoolEvery other definition must pass, not just some earlier one
Sub-grammars, layout and imports
ParseParses a literal's value with a sub-grammar and matches a rule on the tree
- grammar"percentFormat" | "braceFormat" | "annotation" | "eslintDirective" | "printfFormat" | "scanfFormat" | "phpDoc" | "rustFormat"The sub-grammar that parses the literal or comment
- and a rule over the parsed tree, with the AstRule keys: pattern?, kind?, regex?, nthChild?, all?, any?, not?, matches?, inside?, has?, precedes?, follows?, compare?, count?, resolves?, value?, arguments?, binds?, parse?, capture?, formatArgs?, layout?, host?, embedded?, calls?, importTarget? A rule matched on the parsed tree's root; each grammar admits some keys
Each grammar admits only some keys and kinds (see parse); none admits embedded or importTarget.
FormatArgsA format call's arguments no field reads, or fields no argument fills
- style"brace" | "percent" | "printf" | "scanf" | "rust"The format family: brace or percent, printf or scanf, or rust
- stringNAMEThe capture holding the format string
- argsNAMEThe capture holding the arguments passed after the format
- unused?AstRuleA rule matched on the arguments no field consumes
- missing?AstRuleA rule matched on the fields or placeholders no argument fills
unused or missing. brace and percent: python; printf and scanf: php and blade; rust: rust.
LayoutTests the node's line span, its start offset, and the lines between it and a neighbour
- span?LineCountThe line breaks inside the node, 0 on one line
- start?LineCountThe byte offset of the node's first byte, 0 at the file's start
- before?GapThe gap from the previous neighbour's end to the node's start
- after?GapThe gap from the node's end to the next neighbour's start
At least one.
LineCountA count of lines, exact or within bounds
GapThe lines between the node and a neighbour token, sibling or capture
- to"token" | "sibling" | "capture.NAME"The neighbour: the nearest token, the nearest named sibling, or a capture
- comments?BoolA comment counts as the neighbour instead of being passed over
- edge?BoolWith no neighbour, the file's start or end stands in
- breaks?LineCountThe line breaks between the two
- blank?LineCountThe lines between the two that no token or comment touches
breaks or blank; comments and edge not with a capture.
ImportTargetThe import or require target a module specifier names, and how it resolves
- via"import" | "require"Which visitor the rule stands for when no ancestor decides the style
- moduleType?ModuleType, or a list of ModuleType, at least 1The specifier's module type is this one or one listed
- builtin?BoolWhether the specifier is a Node built-in module
- resolved?BoolWhether the target has a file path
- failed?BoolWhether resolution failed
- extensionSpelled?BoolWhether the specifier spells the extension file-extension-in-import expects
- published?BoolWhether npm publishes the file the target names
- devDependency?BoolWhether the module is listed only in devDependencies
- declared?BoolWhether the importing package declares the module as a dependency
- aliased?BoolWhether the specifier starts with one of the tsconfig.json paths keys
TransformationOne transform variable: a single operation over a source's text
- sourceSrcThe capture, variable or regex group the text comes from
- default?StringThe text an unbound source reads as before the operation
- value?BoolReads the capture's value as a string instead of its source text
- replaceStrThe regex whose every match is replaced
- byStringThe replacement text, expanding $1 and ${name}
- sourceSrcThe capture, variable or regex group the text comes from
- default?StringThe text an unbound source reads as before the operation
- value?BoolReads the capture's value as a string instead of its source text
- startChar?IntThe first character kept; negative counts from the end
- endChar?IntThe character the substring stops before; negative counts from the end
- sourceSrcThe capture, variable or regex group the text comes from
- default?StringThe text an unbound source reads as before the operation
- value?BoolReads the capture's value as a string instead of its source text
- toCaseCaseThe case to convert the text to
- separatedBy?list of SeparatorWhere to split words, by default at every separator
Exactly one of these keys, holding the keys under it.
Query options
LanguageOptionsConfiguration the file cannot show, after ESLint's languageOptions and Ruff's settings
- typescript
- ecmaVersion?"latest" | 3 | 5 | 6 to 17 | 2015 to 2026The ECMAScript version whose builtin globals apply
- sourceType?"script" | "module" | "commonjs"Reads every file as a script, a module or CommonJS
- moduleSystem?"file" | "package-type"How a file's module system is decided: by the file, or by package.json type
- globals?map of Ident to "readonly" | "writable" | "off" | an alias, up to 16384Extra global names, each readonly, writable or off
- parserOptions?ESLint parser options
- ecmaFeatures?ESLint's ecmaFeatures flags
- globalReturn?BoolThe top level is a function scope
- impliedStrict?BoolEvery file is strict mode code
- ecmaFeatures?ESLint's ecmaFeatures flags
- jsxPragma?Ident | nullThe variable a JSX element reads, React by default; null for none
- jsxFragmentName?Ident | nullThe variable a JSX fragment reads
- lib?list of LibNametypescript-eslint lib names whose globals are declared
- python
- pythonVersion?"3.7" to "3.15"The Python version whose builtins and magic globals apply
- typingModules?list of up to 256 DottedExtra modules treated as typing, as Ruff's lint.typing-modules
- extendGenerics?list of up to 256 DottedExtra generic base classes, as Ruff's lint.pyflakes.extend-generics
WhenThe configurations a query applies to; otherwise it matches no file
- pythonVersion?Range of PyVersionThe configured Python version lies in this inclusive range
- ecmaVersion?Range of EcmaVersionThe configured ECMAScript version lies in this inclusive range
- sourceType?SourceType, or a list of SourceType, at least 1The source type, configured or the file's own, is this one or one listed
- ecmaFeatures?The query's ecmaFeatures flags equal these
- globalReturn?The query's globalReturn flag equals this
- impliedStrict?The query's impliedStrict flag equals this
- option?map of OptionName to ConditionEach named document option's value meets its condition
RangeAn inclusive version range with at least one bound
At least one; min not after max.
ConditionA test on an option's value: a value, a list of values, or in, includes or not
FileConditions on the file the query reads; otherwise the query skips the file
- path?TextCondTests the path the host sends
- declaration?Bool tsWhether the file is a TypeScript declaration file
- module?TextCondTests the Python module path, its names joined by dots
- moduleName?TextCondTests the last name of the Python module path
- package?TextCond pythonPython: tests the top package name; TypeScript: tests the nearest package.json
- inPackage?Bool pythonWhether the host sent a Python module path
- package?Package tsPython: tests the top package name; TypeScript: tests the nearest package.json
- jsxRuntime?list of "react" | "other" | "unknown" tsThe file's JSX runtime is one listed
- byteOrderMark?BoolWhether the file's text opens with a byte order mark
TextCondA regex test on a text
At least one.
PackageConditions on the file's nearest package.json
- bin?BoolWhether the file is one of the package's bin targets
- nodeVersion?Tests the package's configured Node version range
- exactly one of
- subsetOfSemverRangeThe configured Node range is a subset of this npm range
- notSubsetOfSemverRangeThe configured Node range is not a subset of this npm range
- dependsOn?list of StrThe package lists one of these names among its dependencies
- path?TextCondTests the file's path relative to the package's folder
- serverGraph?BoolWhether the file is in the Next.js App Router server module graph
- private?BoolWhether the package is private
- published?BoolWhether npm publishes the file
At least one.
3. Constructs #
One card per construct. Each gives the JSON shape; the semantics; how it composes with other terms; what it binds or reads (captures); how it behaves when a verdict is unknown; whether a rule using it is evaluated in a file with syntax errors (sound: witnesses outside error regions are reported and the file gets a source-partial failure; source-incomplete: the clause is not evaluated on the file); which languages take it; its limits; and one to three example rules with their source. Section 4 explains the verdict model and the syntax-error classification in full.
Document and queries
format · id · revision · mode · description · scope · clauses #
The document's envelope: identity, mode, file scope and clauses.
In the grammar: Document, Scope, Evidence
mode:logrecords violations without guidance;steersends them to the agent;blockis reserved and acts likesteerfor now.scope: repository-relative globs (globset).includeis non-empty;excludeapplies after. Without ascopethe 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. $optionreferences resolve before validation, except informat,id,revisionand 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.uses evaluation's matcher, so a host passes only the files the rule reads.inScope( path) - 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.sqlitecan't read a document with a newer key (codeRuleSchemais strict).
example:select-one: a scope that includes the API package and excludes tests and migrations.(whole document){
"format": "rerail.policy/v1",
"id": "example:select-one",
"revision": 1,
"mode": "steer",
"description": "Prefer lazy-row entry points for primary-key lookups in consumer code.",
"scope": { "include": ["api/app/**/*.py"], "exclude": ["**/tests/**", "**/migrations/**"] },
"clauses": [
{
"id": "select-one",
"forbid": { "regex": { "pattern": "(?m)txn\\.select_one\\(", "source": "file.content" } },
"violation": {
"anchor": "witness.span",
"code": "SELECT_ONE",
"message": "Prefer lazy-row entry points for primary-key lookups in consumer code."
}
}
]
}forbid · require_each · budget · llm #
A reporting clause holds exactly one form and a violation.
In the grammar: Document, Clause, Evidence
forbidreports every witness of its query. Its violation may take any anchor, a fix (astorfile.contentregex), andeach/suppress(astonly).require_eachreports each projectedsubjectrow lacking anevidencerow.budgetcounts projected evidence rows pergroup_byand reports groups pastcount.lte.llmis reserved.require_each,budgetandllmviolations take onlyanchor: "witness.span"and no fix.
Details: notes, shape, syntax errors, languages, limits
- Notes
- A
select_whenviolation reads itsselectside. An evidence relation wraps an AST query, aliases its rows withas, and projects non-empty fields from captures or file metadata. Its optionalwherecomparesalias.fieldoperands. - 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;
projectneeds at least one field.
require_each clause: every TypeScript program in scope is a subject and evidence, projected by its path. It demonstrates the structural evidence wrapper; a useful relation normally gives the two sides different path scopes or AST rules.{
"id": "program-has-evidence",
"require_each": {
"subject": {
"evidence": {
"as": "subject",
"query": { "ast": { "language": "typescript", "rule": { "kind": "program" } } },
"project": { "path": "file.path" }
}
},
"evidence": {
"evidence": {
"as": "proof",
"query": { "ast": { "language": "typescript", "rule": { "kind": "program" } } },
"project": { "path": "file.path" },
"where": { "eq": ["proof.path", "subject.path"] }
}
}
},
"violation": {
"code": "MISSING_EVIDENCE",
"anchor": "witness.span",
"message": "Program has no evidence."
}
}ast · regex · file · select_when #
A query has exactly one source. Cross-file clause forms use Evidence instead.
In the grammar: Query, RegexQuery, AstQuery
astmatches a rule object on the file's syntax tree or, withembedded, on the program embedded through thatvia.regexruns a Rust regex overfile.contentorfile.path. Each match is a witness; named groups are its captures.fileyields one witness perscopefile it reads (see Files), whatever its text.{ "forbid": { "file": {} } }says those files shouldn't exist;excludelists exceptions.select_whenreportsselect's witnesses ifexistsmatches anywhere in the workspace. The violation readsselect's captures.file.pathregex andfilewitnesses have no span: no fix, nocapture.NAMEanchor.filealso takes noeach.
Details: notes, shape, captures, syntax errors, files, limits
- Notes
- Named groups are
(?<NAME>…)or(?P<NAME>…). Messages, anchors andfile.contentfix templates read them. - A
file.pathmatch reports the path with no line, once per path; messages read the first match. existsis global by design.- ast options:
utils,constraints,transform,languageOptions,when,file.
- Named groups are
- 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 itslanguage:python,typescript,javascript,php,blade,rust,vue, orsql.regexandfile: 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 aregexorfilequery (at any depth) has them in scope. Never secret-like names (SENSITIVE_FILE_NAMES,SENSITIVE_FOLDER_NAMESinsrc/:engine/ path-boundary.ts .env,.npmrc,id_rsa,*.pem,*.tfstate, a path through.ssh/, …).
- 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-ignorecan't silence afile.pathorfilematch: it has no line.- Limits
maxAstPatternBytesandmaxRegexPatternBytescap pattern sizes: 16 KiB each in the bundled capabilities.
file.content regex anchored at the start of the file: a byte-order mark.{
"id": "bom",
"forbid": { "regex": { "source": "file.content", "pattern": "\\A\\x{FEFF}" } },
"violation": {
"anchor": "witness.span",
"code": "BOM",
"message": "The file starts with a byte-order mark."
}
}3 more examples
select_when over two file.path regexes: a .tsx file whose name is not kebab-case.{
"id": "tsx-kebab",
"forbid": {
"select_when": {
"select": {
"regex": { "source": "file.path", "pattern": "\\A(?:.*/)?[^/]*[A-Z_][^/]*$" }
},
"exists": { "regex": { "source": "file.path", "pattern": "(?i)tsx$" } }
}
},
"violation": {
"anchor": "witness.start",
"code": "FILE_NAME_CASING",
"message": "File name must be kebab-case"
}
}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.(whole document){
"format": "rerail.policy/v1",
"id": "example:pack-json-placement",
"revision": 1,
"mode": "steer",
"description": "Keep each pack's pack.json at the top of its pack folder.",
"scope": { "include": ["**/pack.json"], "exclude": ["packs/*/pack.json"] },
"clauses": [
{
"id": "misplaced",
"forbid": { "file": {} },
"violation": {
"anchor": "witness.span",
"code": "MISPLACED_PACK_JSON",
"message": "Move this pack.json to the top of its pack folder, packs/<name>/pack.json."
}
}
]
}no-irregular-whitespace: a regex query whose named group WS is the violation's anchor./checks/3 · ESLint conformance port{
"id": "before-first-token-1-terminator",
"forbid": {
"regex": {
"source": "file.content",
"pattern": "\\A[\\t\\n\\r ]*(?P<WS>[\\x{2028}\\x{2029}])"
}
},
"violation": {
"anchor": "capture.WS",
"code": "NO_IRREGULAR_WHITESPACE",
"message": "Irregular whitespace not allowed."
}
}Atomic terms
kind #
The node's tree-sitter kind: a named, concrete kind of the query's grammar.
In the grammar: AstRule
- Holds where the node has the named kind. Kinds compare by symbol id, so grammar aliases resolve.
- A kind the grammar lacks is a grammar gap: the rule compiles for each dialect that has it (TypeScript and TSX are two), failing only if none does.
- Abstract kinds (supertypes:
expression,statement,declaration,primary_expression,pattern) are refused; list their concrete kinds underany.
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 aregex.
no-ternary: the whole rule is one kind./checks/0 · ESLint conformance port{
"id": "ternary",
"forbid": { "ast": { "language": "typescript", "rule": { "kind": "ternary_expression" } } },
"violation": {
"anchor": "witness.span",
"code": "NO_TERNARY",
"message": "Ternary operator used."
}
}pattern #
A snippet in the query's language the node must match. $NAME binds one node, $$$NAME a run of nodes; $_ matches anything unbound.
In the grammar: AstRule
- ast-grep 0.45.3 matching (pinned
ast-grep-core, default smart strictness): ignoring trivia and comments, the trees must match. - A repeated metavariable (
f($A, $A)) requires equal text. - Object form: parses
contextwhole and matches its sub-node of kindselector. 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$Atas 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 anannotationtree. - Limits
- At most
maxAstPatternBytes. A pattern parsing to an ERROR is refused for that dialect.
console, named in the message.{
"id": "console-access",
"forbid": { "ast": { "language": "typescript", "rule": { "pattern": "console.$M" } } },
"violation": {
"anchor": "witness.span",
"code": "NO_CONSOLE",
"message": "Calls to 'console.$M' are not allowed."
}
}1 more example
$A.{
"id": "longhand-property",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"any": [
{ "pattern": { "context": "({ $A: $A })", "selector": "pair" } },
{ "pattern": { "context": "({ $A: $A } = x)", "selector": "pair_pattern" } }
]
}
}
},
"violation": {
"anchor": "witness.span",
"code": "OBJECT_SHORTHAND",
"message": "Expected property shorthand in object literal.",
"fix": "$A"
}
}regex #
A Rust regex (unanchored) that the node's source text must match.
In the grammar: AstRule
- Matches the node's whole source text; for a parsed tree's node, its decoded text.
- Named groups of a regex on the witness itself (not under a relation or negation) feed the violation:
capture.NAMEanchors,$NAMEin 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
regexcrate syntax: no look-around or backreferences (value.regexhas backreferences).
E742: a named group NAME read by the message./checks/0 · Ruff conformance port{
"id": "ambiguous-class-name",
"forbid": {
"ast": {
"language": "python",
"rule": {
"kind": "identifier",
"regex": "^(?<NAME>[lIO])$",
"inside": { "stopBy": "neighbor", "kind": "class_definition" },
"follows": { "stopBy": "neighbor", "regex": "^(?:class|def)$" }
}
}
},
"violation": {
"anchor": "witness.span",
"code": "E742",
"message": "Ambiguous class name: `$NAME`"
}
}value #
Predicates on the node's value, computed from its own source: literals, plus operators, conditionals and builtin constructors folded over them. Python follows CPython; TypeScript follows eslint-utils' getStaticValue.
In the grammar: AstRule, ValueTest
eq: the language's equality: Python==(1 == 1.0 == True), JavaScriptObject.is.type: the value's type name.constant:trueif the whole value is known,falseif not (a list with an unknown element is not).startsWith,regextest a decoded string;bitsAny,gt/ge/lt/lea number;truthytruthiness.name: true:eq,startsWithandregextest what the node names, not its value.follow: true: a name takes its binding's value (seeresolves.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 Pythonint.type: Pythontype(v).__name__(int,float,complex,str,bytes,bool,NoneType,ellipsis,tuple,list,set,dict). TypeScripttypeof, plusnull,array, andregexpfor a regex literal. Aboolis not anint.constantalone asks whether the value is known.startsWith,regex: Pythonstr, notbytes.regexallows 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 Pythonboolcounts.truthy: Pythonbool(v), JavaScript ToBoolean.namereads any node with a name, property names and bindings included: ESTree's name (TypeScript: identifiers with\uescapes decoded, private names without#), a string literal's value, PHP's symbol name.compare'snametext is the same. Nameless nodes fail. Only those three predicates may sit beside it; Python and Rust refuse it.- Example: ESLint's
id-lengthcountsIdentifier.namein 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)).followin PHP, as PHPStan reads it: a constant this file declares once takes its declaration's value. That coversconst, top-leveldefine, and constants of classes, interfaces, enums or traits declared here. Class constants resolve viaself,parent, a class name, orstaticif 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
getStaticValuefolds, 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,
countandcompare; beforeresolves. Often inconstraintson 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, sonot: { value }fails too. The main source of unknown verdicts. - Syntax errors
- sound (builtin names read as unknown in a broken file) · with
followsource-incomplete - Languages
- python typescript; inside
parse, the grammar's values (followrefused). - Limits
- Python: no
%formatting, complex division orfrozenset. TypeScript: nonew,Math's transcendental functions,Object.keys,Date,Symbolor callback methods. Array and object ToPrimitive is not folded.
no-template-curly-in-string : value.regex over the decoded string, so escapes read as the runtime reads them./checks/0 · ESLint conformance port{
"id": "template-curly-in-string",
"forbid": {
"ast": {
"language": "typescript",
"rule": { "kind": "string", "value": { "regex": "\\$\\{[^}]+\\}" } }
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_TEMPLATE_CURLY_IN_STRING",
"message": "Unexpected template string expression."
}
}2 more examples
ASYNC115: value with type and eq, in a utility the document's define shares, constraining the argument arguments binds in syntactic mode./checks/0 + /checks/1 · Ruff conformance port[
{
"id": "shared",
"define": {
"language": "python",
"utils": {
"number": {
"any": [
{
"any": [{ "kind": "integer" }, { "kind": "float" }],
"value": { "type": "int", "eq": 0 }
},
{
"kind": "parenthesized_expression",
"has": { "stopBy": "neighbor", "matches": "number" }
}
]
}
}
}
},
{
"id": "trio-sleep",
"forbid": {
"ast": {
"language": "python",
"utils": {
"target": {
"any": [{ "kind": "identifier" }, { "kind": "attribute" }],
"resolves": { "import": ["trio.sleep"] }
},
"callee-of-target": {
"any": [
{ "matches": "target" },
{
"kind": "parenthesized_expression",
"has": { "stopBy": "neighbor", "matches": "callee-of-target" }
}
]
}
},
"rule": {
"kind": "call",
"has": {
"stopBy": "neighbor",
"matches": "callee-of-target",
"precedes": {
"stopBy": "neighbor",
"any": [{ "kind": "argument_list" }, { "kind": "generator_expression" }]
}
},
"arguments": {
"signature": ["seconds"],
"bind": { "S": "seconds" },
"mode": "syntactic"
},
"all": [
{
"has": {
"stopBy": "neighbor",
"kind": "argument_list",
"count": {
"has": {
"stopBy": "neighbor",
"not": { "any": [{ "regex": "^[(),\\[\\]{}]$" }, { "kind": "comment" }] }
},
"min": 1,
"max": 1
}
}
}
]
},
"constraints": { "S": { "matches": "number" } }
}
},
"violation": {
"anchor": "witness.span",
"code": "ASYNC115",
"message": "Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`"
}
}
]class-literal-property-style , its unknown-key utility: constant: false with follow holds where a computed key does not fold./checks/0/forbid/ast/utils/unknown-key · ESLint conformance port{
"has": {
"stopBy": "neighbor",
"kind": "computed_property_name",
"has": {
"stopBy": "neighbor",
"not": { "kind": "comment" },
"value": { "constant": false, "follow": true }
}
}
}nthChild #
The node's 1-based position among its parent's named children, as a number or CSS's An+B.
In the grammar: AstRule, NthChild
- Counts the parent's named children, comments included; anonymous tokens and array holes are not children.
- Matches like ast-grep 0.45.3's
nth_child.rs:n / step >= 0 && n % step == 0ini32. ofRulecounts only siblings it matches;reversecounts 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, afterregex. With it: leads the ordered binding terms, soofRulecan compare what a sibling relation binds. - Captures
ofRuletests each sibling against a copy of the caller's bindings, so siblings never reject each other. The node's ownofRulebindings are kept.- Unknown
- An unknown
ofRuleverdict 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.
{
"kind": "identifier",
"nthChild": { "position": "2n+1", "ofRule": { "kind": "identifier" }, "reverse": true }
}1 more example
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./checks/0/forbid/ast/utils/tail · Ruff conformance port{
"params": ["LAST"],
"rule": {
"any": [
{
"not": {
"any": [
{ "kind": "block" },
{ "kind": "if_statement" },
{ "kind": "elif_clause" },
{ "kind": "else_clause" },
{ "kind": "for_statement" },
{ "kind": "while_statement" },
{ "kind": "try_statement" },
{ "kind": "except_clause" },
{ "kind": "finally_clause" },
{ "kind": "with_statement" },
{ "kind": "function_definition" },
{ "kind": "class_definition" },
{ "kind": "decorated_definition" },
{ "kind": "match_statement" },
{ "kind": "case_clause" }
]
},
"pattern": "$LAST"
},
{
"any": [
{ "kind": "block" },
{ "kind": "if_statement" },
{ "kind": "elif_clause" },
{ "kind": "else_clause" },
{ "kind": "for_statement" },
{ "kind": "while_statement" },
{ "kind": "try_statement" },
{ "kind": "except_clause" },
{ "kind": "finally_clause" },
{ "kind": "with_statement" },
{ "kind": "function_definition" },
{ "kind": "class_definition" },
{ "kind": "decorated_definition" },
{ "kind": "match_statement" },
{ "kind": "case_clause" }
],
"has": {
"stopBy": "neighbor",
"not": { "kind": "comment" },
"nthChild": {
"position": 1,
"reverse": true,
"ofRule": { "not": { "kind": "comment" } }
},
"matches": { "tail": { "LAST": "$LAST" } }
}
}
]
}
}layout #
The node's lines, its first byte, and the lines between it and a neighbour.
In the grammar: AstRule, Layout, LineCount, Gap
span: line breaks inside the node (0 on one line).start: its first byte's offset (0 at file start).- A gap runs from neighbour to node (
before) or node to neighbour (after), end to start. topicks 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'sgetTokenBefore/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.NAMEruns after the term binding it; a capture unbound there is refused (as forcompare). - 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
\rline endings, tree-sitter-python's comment token swallows the code after it.
no-unexpected-multiline: a line break between a tag and its template (before, to: "token"), anchored at a regex group./checks/2 · ESLint conformance port{
"id": "tagged-template",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"kind": "template_string",
"regex": "^(?P<PAREN>`)",
"all": [{ "layout": { "before": { "to": "token", "breaks": { "min": 1 } } } }],
"inside": { "stopBy": "neighbor", "kind": "call_expression" }
}
}
},
"violation": {
"anchor": "capture.PAREN",
"code": "NO_UNEXPECTED_MULTILINE",
"message": "Unexpected newline between template tag and template literal."
}
}2 more examples
curly (multi-line), its utilities: isCollapsedOneLiner and isOneLiner as span and before counts./checks/0/forbid/ast/utils · ESLint conformance port{
"using-declaration": {
"kind": "expression_statement",
"has": {
"stopBy": "neighbor",
"any": [
{ "kind": "assignment_expression", "has": { "stopBy": "neighbor", "regex": "^using$" } },
{
"kind": "await_expression",
"has": {
"stopBy": "neighbor",
"kind": "assignment_expression",
"has": { "stopBy": "neighbor", "regex": "^using$" }
}
}
]
}
},
"statement": { "all": [{ "not": { "kind": "comment" } }, { "not": { "regex": "^[{}]$" } }] },
"unsafe-if": {
"any": [
{ "kind": "if_statement", "not": { "has": { "kind": "else_clause" } } },
{
"kind": "if_statement",
"has": { "kind": "else_clause", "has": { "matches": "unsafe-if" } }
},
{
"any": [
{ "kind": "for_statement" },
{ "kind": "for_in_statement" },
{ "kind": "while_statement" },
{ "kind": "labeled_statement" },
{ "kind": "with_statement" }
],
"has": { "matches": "unsafe-if" }
}
]
},
"before-else": {
"any": [
{ "precedes": { "kind": "else_clause" } },
{
"inside": {
"any": [
{ "kind": "for_statement" },
{ "kind": "for_in_statement" },
{ "kind": "while_statement" },
{ "kind": "labeled_statement" },
{ "kind": "with_statement" }
],
"stopBy": "neighbor",
"matches": "before-else"
}
},
{
"inside": {
"kind": "else_clause",
"stopBy": "neighbor",
"inside": { "kind": "if_statement", "stopBy": "neighbor", "matches": "before-else" }
}
}
]
},
"one-statement-block": {
"kind": "statement_block",
"has": { "stopBy": "neighbor", "matches": "statement" },
"not": {
"has": {
"stopBy": "neighbor",
"all": [
{ "matches": "statement" },
{ "precedes": { "stopBy": "end", "matches": "statement" } }
]
}
}
},
"braces-necessary": {
"all": [
{ "matches": "one-statement-block" },
{
"any": [
{
"has": {
"stopBy": "neighbor",
"any": [
{
"any": [
{ "kind": "lexical_declaration" },
{ "kind": "function_declaration" },
{ "kind": "generator_function_declaration" },
{ "kind": "class_declaration" }
]
},
{ "matches": "using-declaration" }
]
}
},
{
"all": [
{ "has": { "stopBy": "neighbor", "matches": "unsafe-if" } },
{ "matches": "before-else" }
]
}
]
}
]
},
"requires-block": {
"all": [
{ "kind": "statement_block" },
{
"any": [
{ "not": { "matches": "one-statement-block" } },
{ "matches": "braces-necessary" }
]
}
]
},
"if-body": {
"all": [
{ "inside": { "kind": "if_statement", "stopBy": "neighbor" } },
{
"follows": {
"stopBy": { "not": { "kind": "comment" } },
"all": [{ "not": { "kind": "comment" } }, { "kind": "parenthesized_expression" }]
}
}
]
},
"one-liner": {
"any": [
{ "layout": { "span": { "eq": 0 } } },
{
"all": [
{
"has": {
"regex": "^;$",
"not": { "precedes": { "stopBy": "end", "not": { "kind": "comment" } } }
}
},
{
"not": {
"has": {
"all": [
{ "not": { "kind": "comment" } },
{ "not": { "regex": "^;$" } },
{ "layout": { "span": { "min": 1 } } }
]
}
}
},
{
"not": {
"has": {
"all": [
{ "not": { "kind": "comment" } },
{ "not": { "regex": "^;$" } },
{ "follows": { "stopBy": "end", "not": { "kind": "comment" } } },
{ "layout": { "before": { "to": "token", "breaks": { "min": 1 } } } }
]
}
}
}
]
}
]
},
"collapsed": {
"any": [
{ "kind": "empty_statement" },
{
"all": [
{ "layout": { "before": { "to": "token", "breaks": { "eq": 0 } } } },
{ "matches": "one-liner" }
]
}
]
}
}Before and after: sort-keys allowLineSeparatedGroups
Before, without layout, the variant compared keys across a blank line (ESLint's valid/99 fired; the port could not read trivia). After, the property rule adds one negated gap: a blank line before the previous named sibling starts a group.
{
"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
throughandchain) 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.chainbinds the definitions on the way (a.fetch -> b.pause) for messages: a shortest chain, first callee in file-path order on ties.- A callee the graph cannot name makes the term unknown, so
not: { calls }never claims an unproven absence.
{ "kind": "call",
"not": { "inside": { "kind": "await" } },
"inside": { "kind": "function_definition", "regex": "^async ", "stopBy": { "kind": "function_definition" } },
"calls": { "through": "sync", "chain": "CHAIN",
"has": { "stopBy": { "kind": "function_definition" }, "kind": "call",
"has": { "field": "function", "resolves": { "import": ["time.sleep"] } } } } }Details: notes, shape, composition, captures, unknown, syntax errors, languages, limits
- Notes
- Seeds: Python
function_definition; PHPfunction_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()itsm(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_, where dispatch is static. Names compare case-insensitively.creation_ expression - 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 (minusinsteadofexclusions), parent's, interface's (abstract yields to a later concrete one).$this->m(),static::m(),new staticare 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, ornew C(__invoke). Also constant callbacks ofcall_user_funcandcall_user_func_array.f(...)itself calls nothing. - PHP chains read
App\; a closure isSvc:: fetch -> App\ Repo:: pause {closure}.
- Seeds: Python
- Shape
- "calls": { "through"?: "sync" | "any", "chain"?: NAME, …Rule }
- Composition
- Late, at the call node. Not in
evidenceorselect_whenqueries; not nested (nocallsin 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-constantcall_user_funccallback). Also$this,self,static,parentin 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, anewexpression, or a variable every path assigns one.
- Python unknown: calls of a parameter, subscript, call result, lambda or
- 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(), overrides in non-workspace classes.C $c) { $c->m(); }
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, animport()orrequire()argument); the term does not find them. - The specifier is the node's
valuewithoutfollow, up to its first!. No known value, no target. viasets the style where no ancestor does:"import"forvisitImport's targets,"require"forvisitRequire's.- Every listed fact must hold. A fact that does not apply holds for neither
truenorfalse. - Files: held files over files on disk below the workspace root; nothing above it. Hosts set the root with
CodeRuleDslWorkspace..setRoot
{ "kind": "string",
"inside": { "field": "source", "any": [{ "kind": "import_statement" }, { "kind": "export_statement" }] },
"importTarget": { "via": "import", "builtin": false, "failed": true } }Details: notes, shape, composition, captures, unknown, syntax errors, languages, limits
- Notes
- Specifier: the plugin's
getStringIfConstant(no scope), converted byString(). - Style: from the first deciding ancestor, per the plugin's
getModuleStyle. A call to the identifierrequire(through parentheses) givesrequire; an import declarationimport(typeforimport type);export type … fromtype. Elsevia:"import"covers import/export sources andimport();"require"coversrequire()andrequire.resolve()arguments. moduleType: the type given, or one of those listed.relative(.,.., or starting./,../,.\,..\), elseabsolute(starting/or\), elsenode(a built-in),data(data:),http(http://,https://),npm(a letter, digit,_,~or-first, after an optional@scope/), elseunknown.nodeandnpmtargets have a module name: the first segment (two if scoped), minusnode:.builtin: a Node built-in per Node 26.5.0'smodule.isBuiltin(names with or withoutnode:, plusnode:-only ones).resolved: the target has a file path (the plugin'sfilePath): the resolved file, else arelativeorabsolutetarget's own path, or adata:orhttp(s):import's specifier. Modules the fallback or an alias maps tofalsehave none.failed: resolution failed (resolveError). A relative path to no file is bothresolvedandfailed.extensionSpelled(relative,absolute):file-extension-in-importat its defaultalways. The specifier must spell the named file's extension: a folder'sindexfile; if nothing is there, a same-named file beside it, try extensions first. TypeScript maps to the compiled extension:.tsand none to.js,.mtsto.mjs,.ctsto.cjs,.tsxto.jsunder a Reactjsxmode above, else.jsx; none withallow.Importing Ts Extensions published(no module name): negatedcheck-publishisPrivateFile. npm publishes the target file, per the importing package, read likefile.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-publishisDevPackage, withoutallowModules: the importing package lists the module indevDependencies, not independencies,peerDependenciesoroptionalDependencies.declared(module name): incheck-extraneous's dependency set. That is the importing package'snameand itsdependencies,devDependencies,peerDependencies,optionalDependencies. Add the same from the nearestpackage.jsonabove whoseworkspaces(array, or objectpackages) orpnpm-workspace.yamlpackagesclaim the folder (globs;!excludes).typeimports also count@typespackages (@types/scope__namefor@scope/name).aliased: a TypeScript file whose specifier starts with atsconfig.jsonpathskey, minus trailing/,\,*(hasTSAlias).- Importing package: the nearest
package.jsonparsing 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; plusimportfor theimportstyle,importandtypesfortype. - Main field
mainand main fileindex, only for therequirestyle ornode/npmtargets. - Extensions
.js,.json,.node,.mjs,.cjs. Withallowin a TypeScript file'sImporting Ts Extensions tsconfig.json:.js,.ts,.mjs,.mts,.cjs,.cts,.json,.node. - The plugin's Node built-in table as a
falsefallback. - TypeScript files (
.ts,.tsx,.mts,.cts):tsconfig.jsonpathsas aliases (keys and targets minus trailing/,\,*; targets relative to the config's folder). - Also the extension map, reversed, as
extensionAlias:.jsto.tsand.tsxifjsxisreact,react-jsx,react-jsxdevorreact-native; else.jsto.ts,.jsxto.tsx. Always.cjsto.cts,.mjsto.mts. None withallow.Importing Ts Extensions - Otherwise defaults:
package.jsondescription files,node_modulesfolders,exportsandimportsfields, symlinks resolved. import-styledata:andhttp(s):targets are not resolved.
- Conditions
tsconfig.json: get-tsconfig 4.14.3, as eslint-plugin-n reads it: the nearest at or above the file, JSON with comments. ItscompilerOptionsoverride those itextends(string or array, later over earlier), at most 8 bases deep. A base is a path (.jsonappended if it names no file) or a package in anode_modulesfolder at or above: itstsconfigfield's file, the given subpath, else itstsconfig.json.- Root: an absolute folder; the enforcer sets its repository's. Without one, and in a snapshot's evaluation (
CompiledCodeRuleDsl.), only held files resolve, so a bare specifier needs its package installed below the root or held.evaluate
- Specifier: the plugin's
- 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 neithertruenorfalse:extensionSpelledof a package,publishedof a named module,devDependencyordeclaredof a path, and the last three without an importingpackage.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,allowModulesorconvertPath. - No
jsconfig.jsonorbaseUrl(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.
- Default plugin options only: no
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:hastests one child;insideonly 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
stopByandfield. - Walk order:
hasin document order,insideoutwards from the parent,precedesforwards,followsbackwards 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
countover 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
hassound, negated too (it reads inside the witness) ·inside,precedes,followspositive 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
parsetoo (insidestops 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.
no-plusplus: has an anonymous token./checks/1 · ESLint conformance port{
"id": "update-decrement",
"forbid": {
"ast": {
"language": "typescript",
"rule": { "kind": "update_expression", "has": { "regex": "^--$" } }
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_PLUSPLUS",
"message": "Unary operator '--' used."
}
}2 more examples
default-case-last: precedes with stopBy: "end", any later case./checks/0 · ESLint conformance port{
"id": "not-last",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"kind": "switch_default",
"precedes": { "kind": "switch_case", "stopBy": "end" }
}
}
},
"violation": {
"anchor": "witness.span",
"code": "DEFAULT_CASE_LAST",
"message": "Default clause should be the last clause."
}
}no-with: a kindless token rule inside a statement./checks/0 · ESLint conformance port{
"id": "with-statement",
"forbid": {
"ast": {
"language": "typescript",
"rule": { "regex": "^with$", "inside": { "kind": "with_statement" } }
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_WITH",
"message": "Unexpected use of 'with' statement."
}
}stopBy #
How far a relation walks.
In the grammar: Relation
"neighbor"(default): the nearest node only: parent, children or adjacent sibling."end": every node on the axis.- A rule: walk to the first node the stop rule matches, and test that node too.
Details: notes, shape, captures, unknown, syntax errors, languages, limits
- Notes
- For
has, a pre-order walk that does not descend below a node the stop rule matches. - Shape
- "stopBy": "neighbor" | "end" | AstRule
- Captures
- A stop rule has its own environment: it neither reads nor binds the caller's metavariables.
- Unknown
- An unknown stop verdict ends the walk with the answer open.
- Syntax errors
- On
inside,precedesorfollowssource-incomplete (claims the nodes walked past do not stop it) · onhas, 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).
no-new: the stop rule not parenthesized_expression reads through any number of parentheses./checks/0 · ESLint conformance port{
"id": "new-statement",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"kind": "expression_statement",
"has": {
"kind": "new_expression",
"stopBy": { "not": { "kind": "parenthesized_expression" } }
}
}
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_NEW",
"message": "Do not use 'new' for side effects."
}
}1 more example
no-sparse-arrays: follows the previous token, stepping over comments./checks/0 · ESLint conformance port{
"id": "hole-comma",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"regex": "^,$",
"inside": { "kind": "array" },
"follows": { "regex": "^[\\[,]$", "stopBy": { "not": { "kind": "comment" } } }
}
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_SPARSE_ARRAYS",
"message": "Unexpected comma in middle of array."
}
}field #
Restricts has and inside to a grammar field.
In the grammar: Relation
insidetests 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. hasstarts at the child in that field (none: no match).neighbortests that child,endits subtree.- A stop rule on
hastests 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 onhas. - Shape
- "has" | "inside": { "field": Str, ... }
- Composition
- Only on
hasandinside. Refused onprecedes/follows(upstream'sFieldNotSupported), in acount's relation, in stop rules, and insideparse. - 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
ifbody isconsequence.
no-empty-function (allow: ["asyncFunctions"]): inside through the body field, and a value field in a negated inside./checks/4 · ESLint conformance port{
"id": "arrow-function",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"kind": "statement_block",
"regex": "^\\{\\s*\\}",
"inside": {
"field": "body",
"all": [
{ "kind": "arrow_function" },
{
"not": {
"inside": {
"field": "value",
"stopBy": { "not": { "kind": "parenthesized_expression" } },
"any": [{ "kind": "pair" }, { "kind": "public_field_definition" }]
}
}
},
{
"any": [
{
"has": {
"regex": "^async$",
"not": {
"any": [{ "kind": "identifier" }, { "kind": "property_identifier" }]
},
"pattern": "$$AS"
}
},
{ "regex": "^" }
]
}
]
}
},
"transform": {
"ASW": { "replace": { "source": "$AS", "replace": "^.+$", "by": "async " } }
}
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_EMPTY_FUNCTION",
"message": "Unexpected empty $ASWarrow function."
}
}2 more examples
Before and after: no-empty-function
{
"kind": "statement_block",
"regex": "^\\{\\s*\\}",
"inside": { "all": [{ "kind": "method_definition" }, "…"] }
}{
"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
allthreads 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 theallruns.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
anyruns after a sibling that binds a name any branch reads, even if another branch binds it. Anotruns after the sibling that binds any capture its rule would bind (directly or through a utility), and tests the bound value. - Captures
allandanybind what their matching items bind.notbinds nothing outward; acompareinside it may read what the samenotbinds.- Unknown
notof 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 implicitallwith the same rules.- Syntax errors
- as their items;
notover an outward relation source-incomplete - Languages
- both; inside
parsetoo.
example:use-normal-imports: any of two patterns./clauses/0{
"violation": {
"anchor": "witness.span",
"code": "USE_NORMAL_IMPORTS",
"message": "Use normal imports rather than import_module."
},
"forbid": {
"ast": {
"language": "python",
"rule": {
"any": [
{ "pattern": "importlib.import_module($MODULE)" },
{ "pattern": "import_module($MODULE)" }
]
}
}
},
"id": "use-normal-imports"
}1 more example
no-labels (allowSwitch): all with a negated has./checks/0 · ESLint conformance port{
"id": "labeled-statement",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"all": [
{ "kind": "labeled_statement" },
{
"not": { "has": { "stopBy": "neighbor", "any": [{ "kind": "switch_statement" }] } }
}
]
}
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_LABELS",
"message": "Unexpected labeled statement."
}
}Reuse and configuration
matches · utils #
Named rules: a query's utils maps ids to rules; matches names one, which matches at the node as if written in place.
In the grammar: AstQuery, AstRule
- Utilities may name each other and recurse through a relation (the parentheses idiom below).
- Ids may not contain
(,),,or=. matcheslooks in the query'sutils, 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
matchesbinds 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
comparemay read any caller's binding, undernottoo; 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.matchesis refused inside a parsed tree, except anannotation's. - Limits
- Recursion depth 256 or 1 MiB of stack, and one
maxVisitedNodesstep 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; ahastostopBy: end(fieldhastoo) 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 loneregex, 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).
SIM208: a recursive utility that reads through parentheses./checks/0 · Ruff conformance port{
"id": "double-negation",
"forbid": {
"ast": {
"language": "python",
"utils": {
"negation": {
"any": [
{ "kind": "not_operator" },
{
"kind": "parenthesized_expression",
"has": { "stopBy": "neighbor", "matches": "negation" }
}
]
}
},
"rule": { "kind": "not_operator", "has": { "stopBy": "neighbor", "matches": "negation" } }
}
},
"violation": {
"anchor": "witness.span",
"code": "SIM208",
"message": "Use the operand instead of `not (not ...)`"
}
}1 more example
no-eval (direct call): the utility binds $P, which the violation anchors at./checks/0 · ESLint conformance port{
"id": "direct-call",
"forbid": {
"ast": {
"language": "typescript",
"languageOptions": {},
"rule": { "kind": "call_expression", "has": { "matches": "direct-callee" } },
"utils": {
"direct-callee": {
"any": [
{ "kind": "identifier", "regex": "^eval$", "pattern": "$P" },
{ "kind": "parenthesized_expression", "has": { "matches": "direct-callee" } }
]
}
}
}
},
"violation": { "anchor": "capture.P", "code": "NO_EVAL", "message": "`eval` can be harmful." }
}define #
A clause that reports nothing and holds what the document's other clauses share: utilities, typed options and their configuration.
In the grammar: Clause, Define
- Shared
utilsare visible to everyastquery of the define's language, as if listed there. - Each compiles with the calling query's
languageOptions, Python options andtransform, soresolvesreads each caller's configuration. - Each id may be shared once per language; shared utilities may name only shared utilities.
optionsandconfig: 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;utilsneedslanguage. Noviolation; 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.
no-unused-vars as one document: a define of six shared TypeScript utilities its clauses call (matches: "u0"…)./clauses/1 · the worked example as built; this repository keeps a cut-down copy{
"id": "shared",
"define": {
"language": "typescript",
"utils": {
"name": {
"any": [
{ "kind": "identifier" },
{ "kind": "type_identifier" },
{ "kind": "property_identifier" },
{ "kind": "shorthand_property_identifier_pattern" },
{ "kind": "shorthand_property_identifier" },
{ "kind": "undefined" }
]
},
"u0": {
"resolves": {
"binding": {
"occurrence": {
"inside": {
"any": [
{ "kind": "assignment_expression" },
{ "kind": "augmented_assignment_expression" },
{ "kind": "update_expression" },
{ "kind": "for_in_statement" },
{ "has": { "regex": "^=$" }, "kind": "variable_declarator" },
{ "kind": "assignment_pattern" },
{ "kind": "object_assignment_pattern" },
{ "has": { "regex": "^=$" }, "kind": "required_parameter" },
{ "has": { "regex": "^=$" }, "kind": "optional_parameter" }
],
"stopBy": {
"not": {
"any": [
{ "kind": "array_pattern" },
{ "kind": "object_pattern" },
{ "kind": "pair_pattern" },
{ "kind": "object_assignment_pattern" },
{ "kind": "assignment_pattern" },
{ "kind": "rest_pattern" },
{ "kind": "parenthesized_expression" }
]
}
}
}
}
},
"write": "last"
}
},
"u1": {
"precedes": {
"any": [
{ "kind": "pair_pattern" },
{ "kind": "shorthand_property_identifier_pattern" },
{ "kind": "object_assignment_pattern" },
{ "kind": "rest_pattern" }
]
}
},
"u2": {
"inside": {
"inside": {
"has": { "kind": "rest_pattern", "not": { "matches": "u1" } },
"kind": "object_pattern"
},
"kind": "pair_pattern"
}
},
"u3": {
"inside": {
"has": { "kind": "rest_pattern", "not": { "matches": "u1" } },
"kind": "object_pattern"
}
},
"u4": {
"resolves": {
"binding": { "occurrence": { "inside": { "kind": "array_pattern" } } },
"write": "last"
}
},
"u5": {
"not": { "inside": { "any": [{ "kind": "enum_body" }, { "kind": "enum_assignment" }] } }
}
}
}
}1 more example
PIE796 (m12c): its two clauses (.py and stub readings) share the enum utilities through one define./checks/0 · Ruff conformance port{
"id": "shared",
"define": {
"language": "python",
"utils": {
"auto-call": {
"any": [
{
"kind": "call",
"has": {
"stopBy": "neighbor",
"not": { "follows": { "stopBy": "neighbor", "regex": "(?s)." } },
"matches": "enum-auto"
}
},
{
"kind": "parenthesized_expression",
"has": { "stopBy": "neighbor", "matches": "auto-call" }
}
]
},
"enum-auto": {
"any": [{ "kind": "identifier" }, { "kind": "attribute" }],
"resolves": { "import": ["enum.auto"] }
},
"enum-base": {
"any": [{ "kind": "identifier" }, { "kind": "attribute" }],
"resolves": { "import": ["enum.Enum"] }
}
}
}
}params #
A utility that takes captures as parameters and runs in its own frame, so it can recurse over a pair of nodes to any depth.
In the grammar: Utility
- The frame holds only the parameters, each bound to the caller's capture (a node, a node list, a parsed tree's exported text and host range) or unbound.
- Bindings to a parameter the caller left unbound return under the caller's name. Other captures stay in the frame, fresh at each level.
- Pass every parameter. Calling a parameterized utility by bare id, or a plain one with an object, is refused.
- Each distinct call (utility and mapping) is planned once: at most 1,024 per query and grammar.
Details: shape, composition, captures, unknown, syntax errors, languages, limits
- Shape
- "utils": { "pair": { "params": ["X", "Y"], "rule": AstRule } } "matches": { "pair": { "X": "$R", "Y": "$L" } }
- Composition
- A call binds the caller's names for the parameters its utility binds and reads those for the parameters it reads, so a sibling comparing a returned name runs after it.
- Captures
- Comparisons inside read only the frame; reading a caller's other capture is refused. Violations see returned captures, not frame captures (
$YPbelow in a message is an error). The query'stransformvariables are unreadable in a frame. - Unknown
- A call's verdict depends on every capture it passes; unknown inside is unknown to the caller, under
nottoo. - 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.
no-self-assign over array destructuring to any depth, from native/src/code_rule_dsl/compile/params_tests.rs (self_assign): pair walks up one array per level and returns the mirror on the left, pinned by index and node. About 1 KB, where the ESLint suite's port spells out three levels of arrays and objects in 206,909 bytes over three documents.{
"language": "typescript",
"rule": {
"kind": "identifier",
"pattern": "$R",
"matches": { "pair": { "X": "$R", "Y": "$L" } },
"compare": { "left": "L", "op": "eq", "right": "R", "text": "name" }
},
"utils": {
"pair": {
"params": ["X", "Y"],
"rule": {
"pattern": "$X",
"any": [
{
"inside": {
"field": "right",
"kind": "assignment_expression",
"has": { "field": "left", "pattern": "$Y" }
}
},
{
"all": [
{
"inside": { "kind": "array", "matches": { "pair": { "X": "$XP", "Y": "$YP" } } }
},
{
"inside": {
"stopBy": "end",
"kind": "assignment_expression",
"has": {
"field": "left",
"has": {
"stopBy": "end",
"pattern": "$Y",
"compare": { "left": "Y", "op": "eq", "right": "X", "text": "index" },
"inside": {
"pattern": "$YPC",
"compare": { "left": "YPC", "op": "eq", "right": "YP", "text": "node" }
}
}
}
}
}
]
}
]
}
}
}
}1 more example
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./checks/0/forbid/ast/utils/in-range + /checks/0/forbid/ast/utils/comments-0 · Ruff conformance port[
{
"params": ["T", "LAST"],
"rule": {
"kind": "comment",
"all": [
{ "layout": { "before": { "to": "capture.T", "breaks": { "min": 1 } } } },
{ "not": { "layout": { "before": { "to": "capture.LAST", "breaks": { "min": 1 } } } } }
]
}
},
{
"params": ["T", "LAST"],
"rule": {
"all": [
{
"not": {
"has": {
"stopBy": "end",
"matches": { "in-range": { "T": "$T", "LAST": "$LAST" } }
}
}
}
]
}
}
]options · config · $option · when.option #
Typed options with defaults, references to their values, and a gate on option values. A port that differs by configuration becomes one document; each variant sets its options.
In the grammar: Define, OptionDecl, OptionRef, When, Condition
- Names: a letter or
_, then letters, digits,_,-,.. Declare each once per document; an unset option takes itsdefault. configsets declared options: a variant's patch, written byapplyCodeRuleConfig(indocument, options) code-rule-options.ts.$optionmay appear anywhere butformat,id,revisionand the defines'optionsandconfig. It resolves before validation.- A
clausesitem 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(anddefault), 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: truesplices 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 forcases. - 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 itsconfig. A repository sets a Rerail rule's options with theconfigrule 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
nullwithcases). Clauses a reference drops are not compiled. Numbers are JavaScript numbers in the engine and zod: compared by value (1equals1.0;integeradmits3.0).caseskeys them byString(n):1.0selects"1",0.00001"0.00001",1.5e-7"1.5e-7",-0"0". A"1.0"key is never selected (not refused).map,joinandtemplateprint numbers the same way. Integral numbers inenum,defaultorconfigmust be safe integers (|n| ≤ 9007199254740991):9007199254740993, 253 or1e21is refused, as a parsed JavaScript document may have rounded it.$optionis a reserved key.
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./checks/0 + /checks/1 · Ruff conformance port[
{
"id": "options",
"define": {
"options": {
"dummy-variable-rgx": {
"type": "string",
"default": "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$",
"description": "Names of variables that may be unused."
}
}
}
},
{
"id": "unused-variable",
"forbid": {
"ast": {
"language": "python",
"utils": {
"display": {
"any": [
{ "kind": "tuple" },
{ "kind": "expression_list" },
{ "kind": "list" },
{ "kind": "set" },
{
"kind": "parenthesized_expression",
"has": { "stopBy": "neighbor", "matches": "display" }
}
]
},
"chain-all-tuple": {
"kind": "assignment",
"has": {
"stopBy": "neighbor",
"any": [
{ "kind": "pattern_list" },
{ "kind": "list_pattern" },
{ "kind": "tuple_pattern", "has": { "stopBy": "neighbor", "regex": "^,$" } }
]
},
"any": [
{
"has": {
"stopBy": "neighbor",
"kind": "assignment",
"matches": "chain-all-tuple"
}
},
{ "not": { "has": { "stopBy": "neighbor", "kind": "assignment" } } }
]
},
"chain-display-value": {
"kind": "assignment",
"any": [
{
"has": {
"stopBy": "neighbor",
"kind": "assignment",
"matches": "chain-display-value"
}
},
{
"not": { "has": { "stopBy": "neighbor", "kind": "assignment" } },
"has": { "stopBy": "neighbor", "matches": "display" }
}
]
},
"chain-top": {
"kind": "assignment",
"not": { "inside": { "stopBy": "neighbor", "kind": "assignment" } }
},
"unpacked": {
"any": [
{
"inside": {
"stopBy": { "kind": "assignment" },
"any": [
{ "kind": "pattern_list" },
{ "kind": "list_pattern" },
{ "kind": "tuple_pattern", "has": { "stopBy": "neighbor", "regex": "^,$" } }
]
},
"all": [
{
"inside": {
"stopBy": "end",
"matches": "chain-top",
"any": [
{ "not": { "matches": "chain-display-value" } },
{ "not": { "matches": "chain-all-tuple" } }
]
}
}
]
},
{
"binds": { "form": "named" },
"inside": {
"stopBy": "end",
"matches": "chain-top",
"all": [
{ "matches": "chain-all-tuple" },
{ "not": { "matches": "chain-display-value" } }
]
}
},
{
"binds": { "form": "with" },
"inside": { "stopBy": "end", "kind": "tuple", "matches": "with-target-top" }
}
]
},
"with-target-top": {
"inside": {
"stopBy": "neighbor",
"any": [
{ "kind": "as_pattern_target" },
{ "kind": "parenthesized_expression", "matches": "with-target-top" }
]
}
}
},
"rule": {
"kind": "identifier",
"pattern": "$N",
"any": [
{
"binds": {
"form": [
"assignment",
"annotated",
"augmented",
"named",
"with",
"match",
"typeAlias"
],
"scope": ["function", "lambda"],
"latest": true,
"outer": false,
"used": false
},
"not": {
"any": [
{
"regex": "^(?:__tracebackhide__|__traceback_info__|__traceback_supplement__|__debuggerskip__)$"
},
{ "matches": "unpacked" }
]
}
},
{ "binds": { "form": "except", "used": false } }
],
"not": { "regex": { "$option": "dummy-variable-rgx" } }
}
}
},
"violation": {
"anchor": "witness.span",
"code": "F841",
"message": "Local variable `$N` is assigned to but never used"
}
}
]2 more examples
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./clauses/0 + /clauses/2 · the worked example as built; this repository keeps a cut-down copy[
{
"id": "options",
"define": {
"options": {
"strict-checking": { "type": "boolean", "default": false },
"src": { "type": "array", "default": ["src"] },
"allowed-modules": { "type": "array", "default": [] }
}
}
},
{
"id": "stdlib-module-shadowing-3.7-3.7",
"forbid": {
"ast": {
"language": "python",
"when": { "pythonVersion": { "max": "3.7" } },
"file": {
"module": {
"regex": "(?:^|\\.)(?:macpath)(?:\\.__init__)?$",
"notRegex": {
"$option": "allowed-modules",
"cases": { "[]": "[^\\s\\S]" },
"default": {
"$option": "allowed-modules",
"escape": "regex",
"join": "|",
"template": "(?:^|\\.)(?:{})(?:\\.__init__)?$"
}
}
},
"moduleName": { "notRegex": "\\." }
},
"rule": { "kind": "module" }
}
},
"violation": {
"anchor": "witness.span",
"code": "A005",
"message": "Module shadows a Python standard-library module"
}
}
]cases with spread), a clause gated by when.option, and a config setting the variant.[
{
"id": "options",
"define": {
"options": {
"ignorePattern": { "type": "string", "nullable": true, "default": null },
"mode": { "type": "string", "enum": ["all", "none"], "default": "all" }
},
"config": { "ignorePattern": "^_" }
}
},
{
"id": "unused-name",
"forbid": {
"ast": {
"language": "python",
"when": { "option": { "mode": { "not": "none" } } },
"rule": {
"kind": "identifier",
"all": [
{ "binds": { "form": "assignment", "scope": "function", "used": false } },
{
"$option": "ignorePattern",
"cases": { "null": { "kind": "identifier" } },
"default": { "not": { "regex": { "$option": "ignorePattern" } } }
}
]
}
}
},
"violation": { "code": "UNUSED", "anchor": "witness.span", "message": "unused" }
}
]constraints #
Per-capture rules checked after the query's rule matched.
In the grammar: AstQuery
- The node a metavariable captured must also match the constraint's rule.
- Single-node captures only.
- An unbound metavariable is not checked: it passes.
Details: notes, shape, composition, captures, unknown, syntax errors, languages
- Notes
- ast-grep's
constraints. - Shape
- "constraints": { NAME: AstRule } (on the ast query)
- Composition
- After the rule matched, before
transform. Typicallyvalue,resolvesor amatches. - 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.
no-unneeded-ternary: a repeated $B constrained to a boolean literal through a utility, and $T constrained by a negation./checks/1 · ESLint conformance port{
"id": "same-values",
"forbid": {
"ast": {
"language": "typescript",
"rule": { "kind": "ternary_expression", "pattern": "$T ? $B : $B" },
"utils": { "bool-lit": { "any": [{ "kind": "true" }, { "kind": "false" }] } },
"constraints": {
"B": { "matches": "bool-lit" },
"T": { "not": { "kind": "identifier" } }
}
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_UNNEEDED_TERNARY",
"message": "Unnecessary use of boolean literals in conditional expression."
}
}Comparing and counting
compare #
Compares two captures by text, value, name, structure, identity or list position.
In the grammar: AstRule, Compare
- Both sides must be bound: a single-node capture, a parsed tree's exported capture, or a transform variable. An unbound side fails.
- A transform variable compares as text:
sourceorunquotedonly. textpicks what is compared (defaultsource); the table below lists every mode.- Equality modes (
tokens,structure,structure-hash,structure-parts,node,reference,reference-no-static-keys) take onlyeq/ne, under the ordinal collation. indexcompares 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
notorcount, it may read what thatnot/countbinds. - Captures
- Reads
leftandright; 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 withoutdefault.- Syntax errors
- sound (reads only captured nodes) · negated, over a capture an outward relation binds under
anysource-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 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 get: 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 |
sort-vars: the default source text with gt, comparing a declarator's name with an earlier one's inside a follows./checks/0 · ESLint conformance port{
"id": "sort-vars",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"kind": "variable_declarator",
"all": [
{
"has": {
"stopBy": "neighbor",
"kind": "identifier",
"pattern": "$NAME",
"not": { "follows": { "stopBy": "neighbor", "regex": "^=$" } }
}
},
{
"follows": {
"stopBy": "end",
"kind": "variable_declarator",
"has": {
"stopBy": "neighbor",
"kind": "identifier",
"pattern": "$PREV",
"not": { "follows": { "stopBy": "neighbor", "regex": "^=$" } },
"compare": {
"left": "PREV",
"op": "gt",
"right": "NAME",
"collation": "ordinal"
}
}
}
}
]
}
}
},
"violation": {
"anchor": "witness.span",
"code": "SORT_VARS",
"message": "Variables within the same declaration block should be sorted alphabetically."
}
}9 more examples
no-self-assign (props: true), object members: key pairs the properties, reference compares the members, each: "L" reports every pair./checks/8 · ESLint conformance port{
"id": "obj-member",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"any": [{ "kind": "member_expression" }, { "kind": "subscript_expression" }],
"pattern": "$R",
"all": [{ "pattern": "$X1" }],
"inside": {
"field": "value",
"stopBy": { "not": { "kind": "parenthesized_expression" } },
"kind": "pair",
"pattern": "$PR1",
"not": { "precedes": { "kind": "spread_element", "stopBy": "end" } },
"inside": {
"kind": "object",
"inside": {
"field": "right",
"stopBy": { "not": { "kind": "parenthesized_expression" } },
"kind": "assignment_expression",
"has": {
"field": "left",
"kind": "object_pattern",
"has": {
"kind": "pair_pattern",
"pattern": "$PL1",
"all": [
{ "compare": { "left": "PL1", "op": "eq", "right": "PR1", "text": "key" } }
],
"has": {
"field": "value",
"any": [{ "kind": "member_expression" }, { "kind": "subscript_expression" }],
"pattern": "$L",
"compare": { "left": "L", "op": "eq", "right": "R", "text": "reference" }
}
}
}
}
}
}
},
"transform": { "RN": { "replace": { "source": "$R", "replace": "\\s+", "by": "" } } }
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_SELF_ASSIGN",
"message": "'$RN' is assigned to itself.",
"each": "L"
}
}no-self-compare: tokens over two operands found through parentheses by utilities./checks/0 · ESLint conformance port{
"id": "self-compare",
"forbid": {
"ast": {
"language": "typescript",
"utils": {
"left": {
"any": [
{ "not": { "kind": "parenthesized_expression" }, "pattern": "$L" },
{
"kind": "parenthesized_expression",
"has": { "stopBy": "neighbor", "not": { "kind": "comment" }, "matches": "left" }
}
]
},
"right": {
"any": [
{ "not": { "kind": "parenthesized_expression" }, "pattern": "$R" },
{
"kind": "parenthesized_expression",
"has": { "stopBy": "neighbor", "not": { "kind": "comment" }, "matches": "right" }
}
]
}
},
"rule": {
"kind": "binary_expression",
"has": {
"stopBy": "neighbor",
"regex": "^(===|==|!==|!=|>|<|>=|<=)$",
"follows": { "stopBy": "end", "not": { "kind": "comment" }, "matches": "left" },
"precedes": { "stopBy": "end", "not": { "kind": "comment" }, "matches": "right" }
},
"compare": { "left": "L", "op": "eq", "right": "R", "text": "tokens" }
}
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_SELF_COMPARE",
"message": "Comparing to itself is potentially pointless."
}
}B033: structure-hash finds a set element equal, as Python hashes it, to a later one./checks/0 · Ruff conformance port{
"id": "duplicate-value",
"forbid": {
"ast": {
"language": "python",
"utils": {
"literal": {
"any": [
{ "kind": "string", "regex": "^[rRuUbB]{0,2}['\"]" },
{
"kind": "concatenated_string",
"not": {
"has": {
"stopBy": "neighbor",
"kind": "string",
"not": { "regex": "^[rRuUbB]{0,2}['\"]" }
}
}
},
{ "kind": "integer" },
{ "kind": "float" },
{ "kind": "true" },
{ "kind": "false" },
{ "kind": "none" },
{ "kind": "ellipsis" }
]
},
"literal-p": {
"any": [
{ "matches": "literal" },
{
"kind": "parenthesized_expression",
"has": { "stopBy": "neighbor", "matches": "literal-p" }
}
]
},
"dup-slot": {
"any": [
{
"inside": { "stopBy": "neighbor", "kind": "set" },
"count": {
"follows": {
"stopBy": "end",
"matches": "literal-p",
"pattern": "$F",
"compare": { "left": "F", "op": "eq", "right": "V", "text": "structure-hash" }
},
"min": 1
}
},
{
"inside": {
"stopBy": "neighbor",
"kind": "parenthesized_expression",
"matches": "dup-slot"
}
}
]
}
},
"rule": { "matches": "literal", "pattern": "$V", "all": [{ "matches": "dup-slot" }] }
}
},
"violation": {
"anchor": "witness.span",
"code": "B033",
"message": "Sets should not contain duplicate item `$V`"
}
}no-self-assign over arrays: index pairs elements by position, and bounds the spread before the element./checks/1 · ESLint conformance port{
"id": "arr-id",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"kind": "identifier",
"pattern": "$R",
"all": [{ "pattern": "$X1" }],
"inside": {
"stopBy": { "not": { "kind": "parenthesized_expression" } },
"kind": "array",
"not": {
"has": {
"kind": "spread_element",
"pattern": "$SP1",
"all": [
{ "compare": { "left": "SP1", "op": "lt", "right": "X1", "text": "index" } }
]
}
},
"inside": {
"field": "right",
"stopBy": { "not": { "kind": "parenthesized_expression" } },
"kind": "assignment_expression",
"has": {
"field": "left",
"kind": "array_pattern",
"has": {
"kind": "identifier",
"pattern": "$L",
"compare": { "left": "L", "op": "eq", "right": "R", "text": "name" },
"all": [
{ "compare": { "left": "L", "op": "eq", "right": "R", "text": "index" } }
]
}
}
}
}
},
"transform": { "RN": { "replace": { "source": "$R", "replace": "\\s+", "by": "" } } }
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_SELF_ASSIGN",
"message": "'$RN' is assigned to itself.",
"each": "L"
}
}no-useless-rename: name equates 'a' and a./checks/1 · ESLint conformance port{
"id": "import-commented",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"all": [
{ "kind": "import_specifier" },
{
"has": {
"stopBy": "neighbor",
"pattern": "$K",
"any": [{ "kind": "identifier" }, { "kind": "string" }],
"precedes": { "regex": "^as$", "stopBy": "end" }
}
},
{
"has": {
"stopBy": "neighbor",
"pattern": "$V",
"any": [{ "kind": "identifier" }, { "kind": "string" }],
"follows": { "regex": "^as$", "stopBy": "end" }
}
},
{ "compare": { "left": "K", "op": "eq", "right": "V", "text": "name" } },
{ "has": { "kind": "comment", "stopBy": "neighbor" } }
]
},
"transform": { "VT": { "substring": { "source": "$V", "value": true } } }
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_USELESS_RENAME",
"message": "Import $VT unnecessarily renamed."
}
}operator-assignment: reference-no-static-keys, positive and negated./checks/2 · ESLint conformance port{
"id": "replaceable-commutative",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"kind": "assignment_expression",
"all": [
{
"has": {
"stopBy": "neighbor",
"not": { "kind": "comment" },
"pattern": "$L",
"precedes": { "stopBy": "end", "regex": "^=$" }
}
},
{
"has": {
"stopBy": { "not": { "kind": "parenthesized_expression" } },
"kind": "binary_expression",
"all": [
{
"has": {
"stopBy": "neighbor",
"not": { "kind": "comment" },
"pattern": "$B",
"precedes": {
"stopBy": "end",
"regex": "^(?:\\*|&|\\^|\\|)$",
"pattern": "$$OP"
}
}
},
{
"has": {
"stopBy": "neighbor",
"not": { "kind": "comment" },
"pattern": "$BR",
"follows": { "stopBy": "end", "regex": "^(?:\\*|&|\\^|\\|)$" }
}
}
]
}
},
{
"not": {
"compare": {
"left": "L",
"op": "eq",
"right": "B",
"text": "reference-no-static-keys"
}
}
},
{
"compare": {
"left": "L",
"op": "eq",
"right": "BR",
"text": "reference-no-static-keys"
}
}
]
}
}
},
"violation": {
"anchor": "witness.span",
"code": "OPERATOR_ASSIGNMENT",
"message": "Assignment (=) can be replaced with operator assignment ($OP=)."
}
}Before and after: Ruff's structural ports
| 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:
PIE796, the counted comparison (a fragment: it calls a utility of its query)./checks/1/forbid/select_when/select/ast/rule/all/1 · Ruff conformance port{
"count": {
"follows": {
"stopBy": "end",
"matches": { "member": { "X": "$U" } },
"compare": { "left": "U", "op": "eq", "right": "V", "text": "structure" }
},
"min": 1
}
}Before and after: no-dupe-else-if (commute)
covers utility.{ "pattern": "$X", "compare": { "left": "X", "op": "eq", "right": "C", "text": "tokens" } }b || a now covers a || b./checks/0/forbid/ast/utils/covers/any/0 · ESLint conformance port{
"pattern": "$X",
"compare": {
"left": "X",
"op": "eq",
"right": "C",
"text": "tokens",
"commute": ["||", "&&"]
}
}collation #
How two strings order under compare.
In the grammar: Compare
ordinal(default): UTF-16 code units, as JavaScript's<.fold-lower/fold-upper: aftertoLowerCase()/toUpperCase().lowercase-first: after swapping the case of ASCII letters.natural: the natural-compare package's order, digit runs by number (ESLint sort-keysnatural).natural-fold-lower:naturalaftertoLowerCase().
Details: shape, composition, languages
- Shape
- "collation": "ordinal" | "fold-lower" | "fold-upper" | "lowercase-first" | "natural" | "natural-fold-lower"
- Composition
- Applies to text modes, and to
valuewhere both values are strings. Equality modes andindexaccept onlyordinal. - Languages
- both.
fold-lower, in utilities that bind both sides.{
"id": "named-imports",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"kind": "import_specifier",
"all": [
{ "matches": "unsorted" },
{
"not": {
"follows": {
"stopBy": "end",
"kind": "import_specifier",
"matches": "unsorted-before"
}
}
}
]
},
"utils": {
"unsorted": {
"pattern": "$A",
"precedes": { "stopBy": "end", "kind": "import_specifier", "pattern": "$B" },
"compare": { "left": "B", "op": "lt", "right": "A", "collation": "fold-lower" }
},
"unsorted-before": {
"pattern": "$C",
"precedes": { "stopBy": "end", "kind": "import_specifier", "pattern": "$D" },
"compare": { "left": "D", "op": "lt", "right": "C", "collation": "fold-lower" }
}
}
}
},
"violation": {
"anchor": "witness.span",
"code": "ORDERED_IMPORTS",
"message": "Named imports must be alphabetized ($B before $A)."
}
}count #
Counts the nodes one relation reaches that match its rule.
In the grammar: AstRule, Count
- Exactly one relation, plus
min(default 0) and/ormax; needsmin ≥ 1or amax. - The relation walks as usual, but
haskeeps descending below a match, so every matching descendant counts. - No
fieldin 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
compareinside 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
hassound · overinside,precedes,followssource-incomplete (a count is an absence claim too) - Languages
- both; inside
parsetoo. - Limits
- A message cannot show a relation's match count. The
counttransform counts a$$$capture's nodes.
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./checks/0 · ESLint conformance port{
"id": "too-many-classes",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"not": { "kind": "comment" },
"all": [
{
"inside": {
"kind": "program",
"count": {
"has": {
"stopBy": "end",
"any": [
{ "kind": "class_declaration" },
{ "kind": "abstract_class_declaration" }
]
},
"min": 2
},
"stopBy": "neighbor"
}
},
{ "not": { "follows": { "stopBy": "end", "not": { "kind": "comment" } } } }
]
}
}
},
"violation": {
"anchor": "witness.start",
"code": "MAX_CLASSES_PER_FILE",
"message": "File has too many classes. Maximum allowed is 1."
}
}1 more example
no-duplicate-case: a follows count whose rule compares with tokens./checks/0 · ESLint conformance port{
"id": "duplicate-case",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"kind": "switch_case",
"all": [
{
"has": {
"stopBy": "neighbor",
"not": { "kind": "comment" },
"matches": "t",
"precedes": { "regex": "^:$", "stopBy": "end" }
}
},
{
"count": {
"follows": {
"stopBy": "end",
"kind": "switch_case",
"all": [
{
"has": {
"stopBy": "neighbor",
"not": { "kind": "comment" },
"matches": "u",
"precedes": { "regex": "^:$", "stopBy": "end" }
}
}
],
"compare": { "left": "U", "op": "eq", "right": "T", "text": "tokens" }
},
"min": 1
}
}
]
},
"utils": {
"t": {
"any": [
{ "not": { "kind": "parenthesized_expression" }, "pattern": "$T" },
{
"kind": "parenthesized_expression",
"has": {
"stopBy": "neighbor",
"all": [{ "not": { "kind": "comment" } }, { "not": { "regex": "^[()]$" } }],
"matches": "t"
}
}
]
},
"u": {
"any": [
{ "not": { "kind": "parenthesized_expression" }, "pattern": "$U" },
{
"kind": "parenthesized_expression",
"has": {
"stopBy": "neighbor",
"all": [{ "not": { "kind": "comment" } }, { "not": { "regex": "^[()]$" } }],
"matches": "u"
}
}
]
}
}
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_DUPLICATE_CASE",
"message": "Duplicate case label."
}
}Names and bindings
Two keys read the file's scope table, built on first use once per file revision and reading (Python version and typing settings, TypeScript block scoping, stub files). resolves asks what a reference resolves to; binds asks about a binding's name node.
Python follows Ruff 0.16.9's semantic model; TypeScript, eslint-scope and typescript-eslint 8.70.1's scope manager. Resolution is lexical and per file: no flow, types or other files.
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,importedandthiscombine; any may match.seenBeforegoes only besideimported.binding(withwrite),unresolved(withdirectives) andbeforeLocalstand alone. Put other terms in their ownresolvesunderall/any.not resolvesalso 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)referencesinput;(time).sleepis the chaintime.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/writerun 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
maxVisitedNodesonce 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.
subprocess.run or os.path.join; a local function of the same name does not match).{
"language": "python",
"rule": { "pattern": "$F($$$)" },
"constraints": { "F": { "resolves": { "import": ["subprocess.run", "os.path.join"] } } }
}global · declared · directives · scriptDeclarations #
A reference to a global or builtin name that no visible file-local binding shadows.
In the grammar: Resolves
global: [names]: a listed, unshadowed reference. WithlanguageOptions, it must also be a declared global or builtin.global: true: any unshadowed declared global or builtin. Refusesdeclared.declared: false(needsglobalandlanguageOptions): only require that nothing shadows the name (ESLint's no-alert).directives: "eslint"(TypeScript): also read/* global a, b: off */comments, as ESLint'sparseStringConfigdoes.offin any comment wins.scriptDeclarations: true(TypeScript, needsglobalandlanguageOptions): in ascript, a listed top-level binding is the global itself, so its references match.
Details: notes, shape, languages, limits
- Notes
- Declared globals: TypeScript's
ecmaVersionbuiltins, CommonJS,globals,lib; Python's builtins and magic globals ofpythonVersion(Ruff's tables). WithoutlanguageOptions: ESLint's latest module globals, Ruff's 3.10 builtins. directivesdeclare or remove names withlanguageOptions. Without, they only remove a listed name (off).scriptDeclarationsfollows eslint-scope's global scope; parameters and block bindings still shadow.
- Declared globals: TypeScript's
- Shape
- "global": [Ident]¹⁻¹⁰²⁴ | true, "declared"?: Bool, "directives"?: "eslint", "scriptDeclarations"?: true
- Languages
- python typescript:
global,declared. typescript only:directives,scriptDeclarations. In aneslintDirectiveparse,globalalone reads an item's text as a declared global. - Limits
- In a Python scope declaring a name
global, it never matchesglobal(Ruff'shas_builtin_binding).readonlyandwritableare not told apart.
no-unsafe-function-type: a type reference to the global Function, not a local type of that name./checks/0 · ESLint conformance port{
"id": "function-type",
"forbid": {
"ast": {
"language": "typescript",
"rule": { "kind": "type_identifier", "resolves": { "global": ["Function"] } }
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_UNSAFE_FUNCTION_TYPE",
"message": "The `Function` type accepts any function-like value.\nPrefer explicitly defining any function parameters and return type."
}
}2 more examples
no-alert: declared: false for shadowing only, directives for /* global */, and this: "globalScope", in one clause./checks/0 · ESLint conformance port{
"id": "alert",
"forbid": {
"ast": {
"language": "typescript",
"languageOptions": {},
"rule": { "kind": "call_expression", "has": { "matches": "callee" } },
"utils": {
"global-obj": {
"any": [
{ "kind": "identifier", "resolves": { "global": ["window"], "declared": false } },
{
"kind": "identifier",
"resolves": { "global": ["globalThis"], "directives": "eslint" }
},
{ "kind": "this", "resolves": { "this": "globalScope" } },
{ "kind": "parenthesized_expression", "has": { "matches": "global-obj" } }
]
},
"callee": {
"any": [
{
"kind": "identifier",
"pattern": "$NAME",
"resolves": { "global": ["alert", "confirm", "prompt"], "declared": false }
},
{
"kind": "member_expression",
"all": [
{
"has": {
"kind": "property_identifier",
"regex": "^(alert|confirm|prompt)$",
"pattern": "$NAME"
}
},
{ "has": { "matches": "global-obj" } }
]
},
{
"kind": "subscript_expression",
"all": [
{
"has": {
"any": [
{ "kind": "string" },
{
"kind": "template_string",
"not": { "has": { "kind": "template_substitution" } }
}
],
"has": {
"kind": "string_fragment",
"regex": "^(alert|confirm|prompt)$",
"pattern": "$NAME"
}
}
},
{ "has": { "matches": "global-obj" } }
]
},
{ "kind": "parenthesized_expression", "has": { "matches": "callee" } }
]
}
}
}
},
"violation": { "anchor": "witness.span", "code": "NO_ALERT", "message": "Unexpected $NAME." }
}no-eval, references to eval: scriptDeclarations makes var eval in a script the global itself./checks/1 · ESLint conformance port{
"id": "eval-reference",
"forbid": {
"ast": {
"language": "typescript",
"languageOptions": {},
"rule": {
"all": [
{ "any": [{ "kind": "identifier" }, { "kind": "shorthand_property_identifier" }] },
{
"resolves": {
"global": ["eval"],
"directives": "eslint",
"scriptDeclarations": true
}
},
{ "pattern": "$G" },
{
"not": {
"inside": { "kind": "call_expression", "has": { "matches": "direct-callee" } }
}
},
{
"not": {
"inside": {
"kind": "parenthesized_expression",
"inside": { "kind": "call_expression", "has": { "matches": "direct-callee" } }
}
}
}
]
},
"utils": {
"direct-callee": {
"any": [
{ "kind": "identifier", "regex": "^eval$", "pattern": "$P" },
{ "kind": "parenthesized_expression", "has": { "matches": "direct-callee" } }
]
}
}
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_EVAL",
"message": "`eval` can be harmful."
}
}import · prefix #
A name or attribute chain whose qualified name, through the file's imports and definitions, is listed.
In the grammar: Resolves
import a.bbindsatoa;import m as x,xtom;from m import x as y,ytom.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: trueis Ruff'sresolve_: an import, a qualified definition or an unshadowed builtin.qualified_ name(..). is_some() prefix: true: a listed name also matches names under it (picklematchespickle.loads, notpickles), forimportandimported. Refused withimport: 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. UnderlanguageOptions., a listedtypingModules 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'sNameResolver(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'suseandextern crateitems;self::/super::normalise tocrate::(see Rust).prefixmatches at a::. Refused: dotted or PHP names, listed paths startingself::/super::,imported. - TypeScript: import resolution out of scope.
- python: dotted names;
- 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).
B022: the callee resolves to contextlib.suppress through any import form or alias./checks/0 · Ruff conformance port{
"id": "useless-contextlib-suppress",
"forbid": {
"ast": {
"language": "python",
"utils": {
"suppress": {
"any": [
{
"any": [{ "kind": "identifier" }, { "kind": "attribute" }],
"resolves": { "import": ["contextlib.suppress"] }
},
{
"kind": "parenthesized_expression",
"has": { "stopBy": "neighbor", "matches": "suppress" }
}
]
}
},
"rule": {
"kind": "call",
"has": {
"stopBy": "neighbor",
"kind": "argument_list",
"not": {
"has": {
"stopBy": "neighbor",
"not": {
"any": [
{ "kind": "keyword_argument" },
{ "kind": "dictionary_splat" },
{ "kind": "comment" },
{ "regex": "^[(),]$" }
]
}
}
},
"follows": { "stopBy": "neighbor", "matches": "suppress" }
}
}
}
},
"violation": {
"anchor": "witness.span",
"code": "B022",
"message": "No arguments passed to `contextlib.suppress`. No exceptions will be suppressed and therefore this context manager is redundant"
}
}1 more example
Before and after: BLE001's logger candidate
Before, "has a qualified name" was approximated by enumerating the file's import forms. After, import: true is exactly Ruff's test:
$L.exception($$$).{
"any": [
{ "resolves": { "import": ["logging", "flask.current_app.logger"] } },
{ "regex": "^_?log", "not": { "resolves": { "import": true } } }
]
}imported · relative · seenBefore #
An import site: a dotted_name an import statement imports, whose qualified name is listed.
In the grammar: Resolves
import a.bandimport a.b as cimporta.b. Infrom m import x as y,mnamesmandxnamesm.x.from __future__ import ximports__future__.x.- Reads no bindings and builds no scope table.
relative: "text": a relative import is its text without dots (from .pdb import set_traceimportspdb.set_trace). Default"never": relative modules name nothing.seenBefore: true(top-level module names only, alone besideimported): 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 andtypevalues.- Shape
- "imported": [Dotted | PhpName]¹⁻¹⁰²⁴, "relative"?: "never" | "text", "seenBefore"?: true
- Syntax errors
- Positive
imported-only: sound (a statement broken before the name imports nothing) · negated, besideglobal/import/this, or withseenBefore: source-incomplete - Languages
- python. php and blade support
imported(withprefix): auseclause's (or@use's) name, alias aside, matches what it imports (a group's prefix plus the clause's name), compared asimportcompares PHP names. They refuserelative(use namespace\A;does not parse) andseenBefore(auseonly aliases). - Limits
- No level-aware reading of relative import sites.
seenBeforeplaces deferred annotations, string annotations and a stub's class bases where they are written.
F407: __future__ imports, relative ones by text./checks/0 · Ruff conformance port{
"id": "future-feature-not-defined",
"forbid": {
"ast": {
"language": "python",
"utils": {
"known-feature": {
"kind": "dotted_name",
"regex": "^(nested_scopes|generators|division|absolute_import|with_statement|print_function|unicode_literals|barry_as_FLUFL|generator_stop|annotations)$"
},
"future-import": {
"any": [
{ "kind": "future_import_statement" },
{
"kind": "import_from_statement",
"has": {
"stopBy": "neighbor",
"kind": "relative_import",
"has": {
"stopBy": "neighbor",
"kind": "dotted_name",
"resolves": { "imported": ["__future__"], "relative": "text" }
}
}
}
]
}
},
"rule": {
"any": [
{ "kind": "dotted_name", "not": { "matches": "known-feature" }, "pattern": "$NAME" },
{
"kind": "aliased_import",
"has": {
"stopBy": "neighbor",
"kind": "dotted_name",
"not": { "matches": "known-feature" },
"pattern": "$NAME"
}
},
{ "kind": "wildcard_import", "pattern": "$NAME" }
],
"inside": { "stopBy": "neighbor", "matches": "future-import" }
}
}
},
"violation": {
"anchor": "witness.span",
"code": "F407",
"message": "Future feature `$NAME` is not defined"
}
}1 more example
ASYNC109 (trio and anyio): gated by seenBefore and by when.pythonVersion; the define shares the override utility with the document's other clause./checks/0 + /checks/2 · Ruff conformance port[
{
"id": "shared",
"define": {
"language": "python",
"utils": {
"override": {
"any": [{ "kind": "identifier" }, { "kind": "attribute" }],
"resolves": {
"import": ["typing.override", "_typeshed.override", "typing_extensions.override"]
}
}
}
}
},
{
"id": "async-function-with-timeout-trio-anyio",
"forbid": {
"ast": {
"language": "python",
"when": { "pythonVersion": { "max": "3.10" } },
"rule": {
"all": [
{
"any": [
{
"kind": "identifier",
"regex": "^timeout$",
"inside": { "stopBy": "neighbor", "kind": "parameters" }
},
{
"any": [
{ "kind": "default_parameter" },
{ "kind": "typed_parameter" },
{ "kind": "typed_default_parameter" }
],
"inside": { "stopBy": "neighbor", "kind": "parameters" },
"has": {
"stopBy": "neighbor",
"kind": "identifier",
"regex": "^timeout$",
"not": { "follows": { "stopBy": "end", "regex": "^[=:]$" } }
}
}
],
"inside": {
"stopBy": "neighbor",
"kind": "parameters",
"inside": {
"stopBy": "neighbor",
"kind": "function_definition",
"has": { "stopBy": "neighbor", "regex": "^async$" },
"not": {
"inside": {
"stopBy": "neighbor",
"kind": "decorated_definition",
"has": {
"stopBy": "neighbor",
"kind": "decorator",
"has": { "stopBy": "neighbor", "matches": "override" }
}
}
}
}
}
},
{ "resolves": { "imported": ["trio", "anyio"], "seenBefore": true } }
]
}
}
},
"violation": {
"anchor": "witness.span",
"code": "ASYNC109",
"message": "Async function definition with a `timeout` parameter"
}
}
]this · sloppyFunctions #
The node is a this whose value is the global object.
In the grammar: Resolves
"global"(ESLint's no-eval): a top-levelthisin ascriptorcommonjsfile, outside functions (arrows aside), field initializers, static blocks and namespaces."globalScope"(ESLint's no-alert): athisin ascript's eslint-scope global scope. Never in a module or CommonJS file.sloppyFunctions: true(with"global"): also athisowned by a non-strict ordinary function.
Details: notes, shape, languages, limits
- Notes
"global"underglobalReturn: only where the top level is not strict."globalScope": not enclosed by a function (arrows included), class, static block, namespace, enum, catch clause orwith; from ES2015, nor by a block,switchor loop declaringlet/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'sisDefaultThisBinding).
this term: the this of this.eval, in a script or a sloppy function.{
"language": "typescript",
"languageOptions": { "sourceType": "script" },
"rule": {
"kind": "this",
"resolves": { "this": "global", "sloppyFunctions": true },
"inside": {
"kind": "member_expression",
"stopBy": "neighbor",
"has": { "kind": "property_identifier", "regex": "^eval$" }
}
}
}binding #
A reference to a file-local binding that passes the filters and sub-rules.
In the grammar: Resolves, ResolvesBinding, Binds
- Reads the binding the reference resolves to (Python's scope-table pick, TypeScript's lexical declaration); never a global, builtin or Ruff's
globalplaceholder.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'sonly_binding). stopAtGlobal: true(Python): where the scope declares the nameglobal, read that scope'sglobalbinding instead of the module's.destructuredValue: false(TypeScript): destructured names have novalue.- 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;
globalreads continue in the module. site: Python's assignment,for,with/exceptpattern, import item,def/class, parameter node,globalstatement, 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.onlyignores form; TypeScript: the name's one declaration in its scope.stopAtGlobalis Ruff'slookup_symbol;destructuredValue, ESLint'sReferenceTracker.
- Python reads the latest binding the read sees; function bodies see the whole module;
- 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(withwrite). Runs in the ordered group beforecountandcompare, which read what its sub-rules bind. - Captures
- Sub-rules share the caller's environment, as
hasdoes, 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 astrue;falserefused.latest,outer: Python, php, blade. PHP flow: some path carries the binding to its scope's end; it writes the global or enclosing variable viaglobal, a by-referenceuseor=& $GLOBALS['x']on every path (unknown on some).destructuredValue,occurrence: TypeScript, php, blade. PHP: the array-literal element alist()target's key selects (also read byvalue.follow); every$xnaming 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, whichbinds: { used: false }matches.
- Limits
- Flow-insensitive: after
x = 1thenif c: x = [2], a later read sees[2]. ESLint'sReferenceTrackerfollows every write; a binding value models only the effectively constant case.
no-obj-calls: a global object reached through a constant alias (value), not through a destructured one (destructuredValue: false)./checks/0 · ESLint conformance port{
"id": "call",
"forbid": {
"ast": {
"language": "typescript",
"languageOptions": {},
"rule": {
"all": [
{ "any": [{ "kind": "call_expression" }, { "kind": "new_expression" }] },
{ "not": { "has": { "kind": "template_string" } } },
{ "has": { "matches": "callee" } }
]
},
"utils": {
"global-obj": {
"any": [
{
"kind": "identifier",
"resolves": {
"global": ["globalThis", "window", "self", "global"],
"directives": "eslint"
}
},
{ "kind": "parenthesized_expression", "has": { "matches": "global-obj" } },
{
"kind": "identifier",
"resolves": {
"binding": { "destructuredValue": false, "value": { "matches": "global-obj" } }
}
}
]
},
"callee": {
"any": [
{
"kind": "identifier",
"resolves": {
"global": ["Atomics", "JSON", "Math", "Reflect", "Intl", "Temporal"],
"directives": "eslint"
},
"pattern": "$NAME"
},
{
"kind": "member_expression",
"all": [
{
"has": {
"kind": "property_identifier",
"regex": "^(Atomics|JSON|Math|Reflect|Intl|Temporal)$",
"pattern": "$NAME"
}
},
{ "has": { "matches": "global-obj" } }
]
},
{
"kind": "subscript_expression",
"all": [
{
"has": {
"any": [
{ "kind": "string" },
{
"kind": "template_string",
"not": { "has": { "kind": "template_substitution" } }
}
],
"has": {
"kind": "string_fragment",
"regex": "^(Atomics|JSON|Math|Reflect|Intl|Temporal)$",
"pattern": "$NAME"
}
}
},
{ "has": { "matches": "global-obj" } }
]
},
{ "kind": "parenthesized_expression", "has": { "matches": "callee" } }
]
}
}
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_OBJ_CALLS",
"message": "'$NAME' is not a function."
}
}1 more example
ASYNC220, utility p-wait-arg: os.P_WAIT directly, through a binding's value, or through a site, read with stopAtGlobal./checks/0/forbid/ast/utils/p-wait-arg · Ruff conformance port{
"any": [
{ "matches": "p-wait-value" },
{
"kind": "identifier",
"resolves": {
"binding": { "stopAtGlobal": true, "value": { "matches": "p-wait-value" } }
}
},
{
"kind": "identifier",
"pattern": "$PW",
"resolves": {
"binding": {
"stopAtGlobal": true,
"site": {
"kind": "assignment",
"all": [
{
"has": {
"field": "left",
"has": {
"stopBy": "neighbor",
"kind": "identifier",
"pattern": "$UT",
"compare": { "left": "UT", "op": "eq", "right": "PW", "text": "tokens" },
"not": {
"precedes": {
"stopBy": "end",
"kind": "identifier",
"pattern": "$UU",
"compare": { "left": "UU", "op": "eq", "right": "PW", "text": "tokens" }
}
}
}
}
},
{
"has": {
"field": "right",
"has": { "stopBy": "neighbor", "matches": "p-wait-value", "pattern": "$UV" }
}
},
{ "compare": { "left": "UV", "op": "eq", "right": "UT", "text": "index" } }
]
}
}
}
},
{
"kind": "parenthesized_expression",
"has": { "stopBy": "neighbor", "matches": "p-wait-arg" }
}
]
}Before and after: ports in the Ruff suite
| 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
delunbound), 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 countslanguageOptions' declared globals and, withdirectives,/* global */comments. - Shape
- "unresolved": true | "star", "directives"?: "eslint"
- Composition
- Alone in its
resolves,directivesaside. - Syntax errors
- source-incomplete
- Languages
- python typescript sql;
"star"Python only. sql'strue: aSELECTqualifier 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.
F822: an __all__ entry naming nothing./checks/0 · Ruff conformance port{
"id": "undefined-export",
"forbid": {
"ast": {
"language": "python",
"rule": {
"any": [{ "kind": "string" }, { "kind": "concatenated_string" }],
"pattern": "$S",
"resolves": { "unresolved": true }
}
}
},
"violation": {
"anchor": "witness.span",
"code": "F822",
"message": "Undefined name $S in `__all__`"
}
}2 more examples
no-undef (typeof: true) with /* global */ comments./checks/0 · ESLint conformance port{
"id": "undef",
"forbid": {
"ast": {
"language": "typescript",
"languageOptions": { "jsxPragma": null },
"rule": {
"pattern": "$NAME",
"any": [
{ "kind": "identifier" },
{ "kind": "type_identifier" },
{ "kind": "property_identifier" },
{ "kind": "shorthand_property_identifier_pattern" },
{ "kind": "shorthand_property_identifier" },
{ "kind": "undefined" }
],
"resolves": { "unresolved": true, "directives": "eslint" }
}
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_UNDEF",
"message": "'$NAME' is not defined."
}
}F405: "star", in code and inside string annotations (parse: annotation), one violation per name (each)./checks/0 · Ruff conformance port{
"id": "undefined-local-with-import-star-usage",
"forbid": {
"ast": {
"language": "python",
"utils": {
"star-in-string": {
"any": [{ "kind": "string" }, { "kind": "concatenated_string" }],
"parse": {
"grammar": "annotation",
"has": {
"stopBy": "end",
"any": [
{
"kind": "identifier",
"resolves": { "unresolved": "star" },
"capture": "NAME"
},
{ "matches": "star-in-string" }
]
}
}
}
},
"rule": {
"any": [
{
"any": [{ "kind": "identifier" }, { "kind": "string" }],
"pattern": "$NAME",
"resolves": { "unresolved": "star" }
},
{ "matches": "star-in-string" }
]
}
}
},
"violation": {
"anchor": "capture.NAME",
"code": "F405",
"message": "`$NAME` may be undefined, or defined from star imports",
"each": "NAME"
}
}beforeLocal · write #
beforeLocal (Python, F823): a read before the function binds the name. write: "last" (TypeScript): where ESLint's no-unused-vars reports a variable.
In the grammar: Resolves
beforeLocal: a function's first load of a name that a later binding in the same function shadows (Ruff'sundefined_local).write: "last": the last write reference from the variable's own function scope, else the first definition's name. Abindingbeside it filters the variable.
Details: notes, shape, syntax errors, languages
- Notes
beforeLocalskips 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 adef's annotations are in Ruff's type scope and never count.writeincludes a declarator's initializer, afor … in/ofdeclaration 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
beforeLocalpython ·writetypescript
F823, whole./checks/0 · Ruff conformance port{
"id": "undefined-local",
"forbid": {
"ast": {
"language": "python",
"rule": { "kind": "identifier", "pattern": "$N", "resolves": { "beforeLocal": true } }
}
},
"violation": {
"anchor": "witness.span",
"code": "F823",
"message": "Local variable `$N` referenced before assignment"
}
}1 more example
no-unused-vars (args: "all", vars: "local"), its parameter term: an unused parameter reported where ESLint reports it./checks/0 · ESLint conformance port{
"id": "unused-args",
"forbid": {
"ast": {
"language": "typescript",
"languageOptions": { "jsxPragma": null },
"rule": {
"pattern": "$NAME",
"any": [
{ "kind": "identifier" },
{ "kind": "type_identifier" },
{ "kind": "property_identifier" },
{ "kind": "shorthand_property_identifier_pattern" },
{ "kind": "shorthand_property_identifier" },
{ "kind": "undefined" }
],
"all": [
{
"all": [
{
"resolves": {
"write": "last",
"binding": {
"scope": [
"module",
"function",
"class",
"block",
"namespace",
"typeParameters"
],
"directives": "eslint",
"form": "parameter",
"used": false,
"exported": false
}
}
}
]
}
]
}
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_UNUSED_VARS",
"message": "'$NAME' is defined but never used."
}
}binds #
A binding's name node, where a linter reports it, whose binding passes the filters and sub-rules.
In the grammar: AstRule, ResolvesBinding, Binds, Redefines
- Name node: Python's identifier, import alias, or unaliased import's
dotted_name(os.path); TypeScript's declared identifier, type name, enum member, shorthand pattern or private name (#a). form: the binding construct.scope: the kind of scope holding it. Values in Notes.used: some read resolves to it (or the same-scope binding it shadows).referenced: any reference, writes included.nestedUse: a read from another scope.exported: exported or re-exported.latest/outer(Python): the live binding; made underglobal/nonlocal.bindings: how many bindings of the name its scope holds.redefines: the binding shadows (Python) or redeclares (TypeScript) one that passes; withsameScope,sameImport,every.
Details: notes, shape, composition, captures, unknown, syntax errors, languages, limits
- Notes
form:assignment,annotated,annotation(barex: 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'squery; each language refuses the other's.usedfollows Ruff (reads copied) and ESLint'sisUsedVariablewith typescript-eslint's marks.referencedinherits nothing.exported: Python, an explicit re-export or__all__entry; TypeScript, anexportor (withdirectives) an/* exported */comment.builtin(TypeScript): a configured global's variable in a script.bindingscounts inside the capturewithin.redefines: Python, the one before it on the same fork or an enclosing scope's passes, withsameScopeandsameImport; TypeScript, an earlier definition passes (ESLint's no-redeclare), or witheveryeach 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.
bindsstarts from the binding, so it has noonlyorstopAtGlobal. - Captures
- Sub-rules (
site,value,annotation,occurrence, the shadowed binding's) share the environment and may bind.bindings.withinreads a capture. - Unknown
used,referenced,nestedUse,exportedare three-valued: where a read may be unseen, an unused binding'susedis unknown, so neitherused: falsenor its negation matches. That covers:- Python: an assignment, walrus,
withname, match capture,typealias or parameter in a function callinglocals(); a module binding under an__all__whose value may list names no string spells. - TypeScript: bindings a direct
eval(...)orwithcan see; a private name'snestedUse.
- Python: an assignment, walrus,
- 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 witheveryeach does and one reaches. They refusesameImport,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.
F402: a loop variable (form, site) that redefines an import./checks/0 · Ruff conformance port{
"id": "import-shadowed-by-loop-var",
"forbid": {
"ast": {
"language": "python",
"rule": {
"kind": "identifier",
"pattern": "$NAME",
"binds": {
"form": "for",
"site": { "kind": "for_statement" },
"redefines": { "form": "import" }
}
}
}
},
"violation": {
"anchor": "witness.span",
"code": "F402",
"message": "Import `$NAME` shadowed by loop variable"
}
}5 more examples
F401: an unused, unexported, live import; Ruff's allowed-unused-imports setting is a document option that a $option reference splices in./checks/0 + /checks/1 · Ruff conformance port[
{
"id": "options",
"define": {
"options": {
"pyflakes.allowed-unused-imports": {
"type": "array",
"items": "string",
"default": [],
"description": "Modules whose imports may be unused, with their submodules and members."
}
}
}
},
{
"id": "unused-import",
"forbid": {
"ast": {
"language": "python",
"rule": {
"any": [{ "kind": "dotted_name" }, { "kind": "identifier" }],
"pattern": "$NAME",
"binds": {
"form": "import",
"scope": ["module", "function"],
"latest": true,
"used": false,
"exported": false,
"outer": false
},
"not": {
"any": [
{
"inside": {
"stopBy": "end",
"any": [
{ "kind": "future_import_statement" },
{
"kind": "import_from_statement",
"has": {
"stopBy": "neighbor",
"kind": "relative_import",
"has": {
"stopBy": "neighbor",
"kind": "dotted_name",
"regex": "^__future__$"
}
}
}
]
}
},
{
"$option": "pyflakes.allowed-unused-imports",
"cases": { "[]": [] },
"default": [
{
"kind": "dotted_name",
"resolves": {
"imported": { "$option": "pyflakes.allowed-unused-imports" },
"prefix": true
}
},
{
"kind": "identifier",
"inside": {
"stopBy": "neighbor",
"kind": "aliased_import",
"has": {
"stopBy": "neighbor",
"kind": "dotted_name",
"resolves": {
"imported": { "$option": "pyflakes.allowed-unused-imports" },
"prefix": true
}
}
}
}
],
"spread": true
}
]
}
}
}
},
"violation": {
"anchor": "witness.span",
"code": "F401",
"message": "`$NAME` imported but unused"
}
}
]no-redeclare (builtinGlobals): builtin./checks/1 · ESLint conformance port{
"id": "redeclared-builtin",
"forbid": {
"ast": {
"language": "typescript",
"languageOptions": { "jsxPragma": null },
"rule": {
"pattern": "$NAME",
"any": [
{ "kind": "identifier" },
{ "kind": "type_identifier" },
{ "kind": "property_identifier" },
{ "kind": "shorthand_property_identifier_pattern" },
{ "kind": "shorthand_property_identifier" },
{ "kind": "undefined" }
],
"all": [
{ "binds": { "scope": ["global", "module", "function", "block"], "builtin": true } }
]
}
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_REDECLARE",
"message": "'$NAME' is already defined as a built-in global variable."
}
}Before and after: the Pyflakes usedness ports
| 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) |
F842 as committed./checks/0 · Ruff conformance port{
"id": "unused-annotation",
"forbid": {
"ast": {
"language": "python",
"rule": {
"kind": "identifier",
"pattern": "$N",
"binds": {
"form": "annotation",
"scope": ["function", "lambda"],
"latest": true,
"used": false
},
"not": { "regex": "^(?:_+|_+[a-zA-Z0-9_]*[a-zA-Z0-9])$" }
}
}
},
"violation": {
"anchor": "witness.span",
"code": "F842",
"message": "Local variable `$N` is annotated but never used"
}
}$LOOP.{
"kind": "identifier",
"inside": { "kind": "for_statement", "pattern": "$LOOP", "stopBy": "end" },
"binds": { "form": "augmented", "bindings": { "within": "LOOP", "max": 1 } }
}binds term (367 bytes where the structural port had 2,111): reported at a member's last declaration. MEMBERS spelled out.{
"kind": "private_property_identifier",
"pattern": "$N",
"binds": { "used": false },
"not": {
"inside": {
"stopBy": "neighbor",
"any": [{ "kind": "public_field_definition" }, { "kind": "method_definition" }],
"precedes": {
"stopBy": "end",
"any": [{ "kind": "public_field_definition" }, { "kind": "method_definition" }],
"has": { "stopBy": "neighbor", "kind": "private_property_identifier", "pattern": "$N" }
}
}
}
}arguments #
A call's arguments bound to a signature's named slots, as the language binds them.
In the grammar: AstRule, Arguments
- Holds on a Python
call, TypeScriptcall_expressionornew_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'sfind_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'snode.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:**kwpasses nothing. PHP reads nikic/PHP-Parser'sargsas written (named arguments as keywords, unpacked as starred, parentheses unwrapped): a call PHP refuses (positional after named) is still read;f(...)is no call.
- Python:
- Shape
- "arguments": { "signature": [Ident | "/" | "*"], "bind"?: { NAME: Slot }, "absent"?: [Slot]⁺, "mode"?: "language" | "syntactic" | "estree" }
- Composition
- An atomic term (after
pattern,kind,regex), often withconstraintson the bound capture. - Captures
- Binds the
bindnames. - Unknown
- In
languagemode: a slot a starred argument (*args,...args) or**kwmay fill, or one passed twice, and any verdict depending on it (no violation, negated or not).syntacticandestree: 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.
estreehas no "every argument equals the parameter at its index" form (usecompare text: index). Bandit counts**kwas passing nothing where alanguage-mode port is unknown.
radix, missing parameters: absent on the first slot./checks/0 · ESLint conformance port{
"id": "missing-parameters",
"forbid": {
"ast": {
"language": "typescript",
"languageOptions": {},
"rule": {
"kind": "call_expression",
"has": { "matches": "callee" },
"arguments": { "signature": ["string", "radix"], "absent": ["string"] }
},
"utils": {
"callee": {
"any": [
{
"any": [
{
"kind": "identifier",
"resolves": { "global": ["parseInt"], "directives": "eslint" }
},
{
"kind": "member_expression",
"all": [
{ "has": { "kind": "property_identifier", "regex": "^parseInt$" } },
{ "has": { "matches": "number-obj" } }
]
},
{
"kind": "subscript_expression",
"all": [
{
"has": {
"any": [
{ "kind": "string" },
{
"kind": "template_string",
"not": { "has": { "kind": "template_substitution" } }
}
],
"value": { "eq": "parseInt" }
}
},
{ "has": { "matches": "number-obj" } }
]
}
]
},
{ "kind": "parenthesized_expression", "has": { "matches": "callee" } }
]
},
"number-obj": {
"any": [
{
"kind": "identifier",
"resolves": { "global": ["Number"], "directives": "eslint" }
},
{ "kind": "parenthesized_expression", "has": { "matches": "number-obj" } }
]
}
}
}
},
"violation": { "anchor": "witness.span", "code": "RADIX", "message": "Missing parameters." }
}2 more examples
requests.get without a timeout (Bandit's B113; keyword-only after *).{
"pattern": "requests.get($$$)",
"arguments": { "signature": ["url", "params", "*", "timeout"], "absent": ["timeout"] }
}estree binds a of super((a)), and name compares it with the parameter.{
"kind": "call_expression",
"has": { "kind": "super", "stopBy": "neighbor" },
"arguments": {
"signature": ["a", "b"],
"bind": { "A": "a" },
"absent": ["b"],
"mode": "estree"
},
"inside": {
"kind": "method_definition",
"stopBy": "end",
"has": {
"kind": "formal_parameters",
"has": { "kind": "required_parameter", "has": { "kind": "identifier", "pattern": "$P" } }
}
},
"compare": { "left": "A", "op": "eq", "right": "P", "text": "name" }
}Sub-grammars
A string literal or comment can be a small language of its own: a printf-style format, a str.format template, a type in a string, an ESLint directive.
parse decodes the literal, parses it with a sub-grammar, and matches a rule of that grammar's kinds on the tree's root. Each parsed node maps back to its source bytes, so a capture inside the tree can anchor a violation, carry a fix range, or be enumerated by each.
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; PHPprintfFormat,scanfFormat,phpDoc; RustrustFormat; TypeScripteslintDirective. 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
formatroot holding oneerrornode, valued by Ruff's message (PHP'sValueErrormessage forprintfFormat, rustc's forrustFormat).
Details: notes, shape, composition, captures, unknown, syntax errors, languages, limits
- Notes
percentFormat(Ruff'scformat, F50x):formatholdsliteralandplaceholder. A placeholder holdsmapping_key,flags,width,precision(each with astar),length,conversion; its value is its key, or the index of the positional value it consumes.braceFormat(Ruff'sformat, F52x):formatholdsliteralandfield. A field holdsfield_name(headauto,indexorname, thenattribute,element_index; valued by the argument it reads),conversion,format_spec(nested fields, orfill,align,sign,alternate,zero,width,grouping,precision,type,spec_error).printfFormat(PHP 8.5.11'sphp_formatted_printwith every value passed; oracle: the runtime and PHPStan 2.2.16'sPrintfFormatParser):formatholdsliteralandplaceholder. A placeholder (%[argnum$]) holds[flags] [width] [.precision] [l]conversion argnum,flags(-,+, space,0,'and its padding byte),width,precision(each with astarfor*or*N$),length(l) andconversion. Values are indexes from 0, after the format: a placeholder's, of the value it consumes; a star's, of the value it reads; anargnum'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'sValidateFormatforsscanf/fscanfwith no variable passed; oracle: the runtime with 0–6 variables):formatholdsliteralandplaceholder. A placeholder holdssuppress(*),argnum,width,lengthandconversion(asetchild 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 (acommentPHP lexes asT_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, withoutNode:- A
php_docroot holdsphp_doc_textandphp_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, theassert_tag_*values,generic_tag_valuefor 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_typeandconst_typeover constant expressions (const_expr_integer,const_expr_string,const_fetchvaluedClass::NAME, …). name,variableanddescriptionleaves 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 aninvalid_type), valued by the parser's message. - Each node maps to its exact source in the comment; nesting past 256 levels is unknown.
- A
rustFormat(rustc 1.99.0'srustc_parse_format: templates offormat_args!and the macros built on it):formatholdsliteralandfield. A field, valued by the argument it names (an explicit index or a name; none for{}), holdsauto(zero width),indexorname, then itsformat_spec(valued by its text) with thetypeof the trait it names (?,x?,e,x, ...). It reads astring_literalorraw_string_literaldecoded 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 aformatroot holding oneerrornode 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 insideLiteral[...]), parsed asparse_type_annotationdoes: anexpression_statementroot with Python's kinds. A string Ruff refuses (F722) is oneERRORnode. Each semantic syntax error Ruff's checker reports there (awaitoutside an async function, a walrus rebinding a comprehension variable…) is anERRORchild after the expression, valued by Ruff's message. Its names are forward references in the scope table.eslintDirective(TypeScript comments ESLint reads as directives): adirectiveroot valued by its label; anitemper listed name (valued by the name); aglobalitem'ssetting; adescriptionafter--. 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,countand the relations (insidestops at the parsed root). Also:hostin a text grammar's rule;pattern,matches,resolves(notbinding) and a nestedparse: annotationin anannotationrule;resolveswithglobalalone in aneslintDirectiverule. Other keys,fieldandnthChildamong them, are refused. - Captures
- The tree has its own environment. Its
captures export to the host as span captures: decoded text plus host range. - Unknown
- A literal whose value is not known is unknown: no violation, negated or not. Python: an f-string with a field, a
tstring, 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,rustFormatsound (the literal or comment alone) ·annotationsource-incomplete (reads the scope table)- Languages
percentFormat,braceFormat,annotation: python, on astringorconcatenated_string.eslintDirective: typescript, on acomment.printfFormat,scanfFormat: php and blade, on astring,encapsed_string,heredocornowdoc, 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 doccomment; in a template only the embedded PHP's, never Blade's{{-- --}}.rustFormat: rust, on astring_literalorraw_string_literal.- A grammar of another family is refused, naming its counterpart (
percentFormaton PHP namesprintfFormat,annotationnamesphpDoc) or what PHP lacks (str.formattemplates; types held in strings). ecmaRegexis refused: see Not included.
- Limits
parseholds on the literal node only, not through parentheses.braceFormatreads one level of nested fields, as Ruff'sFormatSummary. 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'sparse. Strings nested in a type definition are read eight levels deep.
F501: a % format the grammar refuses (an error node that is not an unsupported character)./checks/0 · Ruff conformance port{
"id": "percent-format-invalid-format",
"forbid": {
"ast": {
"language": "python",
"utils": {
"plain-string": {
"any": [
{ "kind": "string", "not": { "regex": "^[A-Za-z]*[fFbBtT][A-Za-z]*['\"]" } },
{
"kind": "concatenated_string",
"not": {
"has": { "kind": "string", "regex": "^[A-Za-z]*[fFbBtT][A-Za-z]*['\"]" }
}
}
]
},
"format-string": {
"any": [
{
"matches": "plain-string",
"parse": {
"grammar": "percentFormat",
"has": {
"kind": "error",
"not": { "value": { "regex": "^unsupported format character" } }
}
}
},
{ "kind": "parenthesized_expression", "has": { "matches": "format-string" } }
]
}
},
"rule": { "pattern": "$S % $R" },
"constraints": { "S": { "matches": "format-string" } }
}
},
"violation": {
"anchor": "witness.span",
"code": "F501",
"message": "`%`-format string has invalid format string"
}
}3 more examples
F521: the same for .format templates./checks/0 · Ruff conformance port{
"id": "string-dot-format-invalid-format",
"forbid": {
"ast": {
"language": "python",
"utils": {
"plain-string": {
"any": [
{ "kind": "string", "not": { "regex": "^[A-Za-z]*[fFbBtT][A-Za-z]*['\"]" } },
{
"kind": "concatenated_string",
"not": {
"has": { "kind": "string", "regex": "^[A-Za-z]*[fFbBtT][A-Za-z]*['\"]" }
}
}
]
},
"format-string": {
"any": [
{
"matches": "plain-string",
"parse": { "grammar": "braceFormat", "has": { "kind": "error" } }
},
{ "kind": "parenthesized_expression", "has": { "matches": "format-string" } }
]
}
},
"rule": { "pattern": "$S.format($$$A)" },
"constraints": { "S": { "matches": "format-string" } }
}
},
"violation": {
"anchor": "witness.span",
"code": "F521",
"message": "`.format` call has invalid format string"
}
}no-redeclare: a /* global */ item that a var of the script redeclares, anchored inside the comment at capture.N./checks/1 · ESLint conformance port{
"id": "global-comment-syntax",
"forbid": {
"ast": {
"language": "typescript",
"languageOptions": { "jsxPragma": null },
"rule": {
"kind": "comment",
"parse": {
"grammar": "eslintDirective",
"value": { "regex": "^globals?$" },
"has": {
"kind": "item",
"capture": "N",
"not": { "any": [{ "has": { "kind": "setting", "regex": "^off$" } }] }
}
},
"all": [
{
"inside": {
"stopBy": "end",
"kind": "program",
"has": {
"stopBy": "end",
"kind": "identifier",
"binds": { "scope": ["global"] },
"pattern": "$V",
"compare": { "left": "V", "op": "eq", "right": "N" }
}
}
}
]
}
}
},
"violation": {
"anchor": "capture.N",
"code": "NO_REDECLARE",
"message": "'$N' is already defined by a variable declaration."
}
}@param typed mixed (written for this page; a smoke test runs it stateless and through the workspace): the identifier is the tag value's own type, so array<mixed> does not match, and the violation anchors on the type inside the comment.{
"language": "php",
"rule": {
"kind": "comment",
"parse": {
"grammar": "phpDoc",
"has": {
"kind": "php_doc_tag",
"value": { "eq": "@param" },
"has": {
"kind": "param_tag_value",
"has": { "kind": "identifier_type", "value": { "eq": "mixed" }, "capture": "T" }
}
}
}
}
}capture #
Inside parse only: binds the parsed node under a name and exports it to the host rule.
In the grammar: AstRule
- The exported capture is text with a source span.
- Messages and fix templates interpolate its decoded text.
capture.NAMEanchors,fix.range,eachandlayoutgaps read its host range.- A host
comparereads it as text (source,unquoted);namecompares its text with a node's name.
Details: notes, shape, composition, captures, syntax errors, languages
- Notes
- Its source range sits under a reserved name: a NUL, then the name.
- Shape
- "capture": NAME
- Composition
- An atomic term of the tree's rule. Refused outside
parse. - Captures
- Binds; exported through nested
parses and utilities to the host. - Syntax errors
- sound
- Languages
- every grammar.
% format, one violation per key (each), anchored inside the string.{
"language": "python",
"rule": {
"kind": "string",
"parse": {
"grammar": "percentFormat",
"has": {
"kind": "placeholder",
"stopBy": "end",
"has": { "kind": "mapping_key", "capture": "K" }
}
}
}
}host #
A rule matched on the node of the other document a node stands in: the comment or string a parse holds, or the file's own node behind a node of an embedded program (ast.embedded, embedded).
In the grammar: AstRule
- In a parsed tree: a rule of the host language, matched on the comment or string the
parseholds. - 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
parseor enclosingembeddedbegan (fresh underast.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 theembeddedrule.
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
bin/*global b, a*/ var a;toa, andeachover the item reaches every item the file admits. - It nests: a host rule's own
parsemay holdhost. - A program node's host span is the file bytes its program range was copied or written from. The host rule may use
embeddedto reach the program again. - Where both apply, the innermost context decides: a
parseon a program's node makeshostinside 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
parseand outside a rule over an embedded program, in anannotationtree, and informatArgs' 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
parseor enclosingembeddedbegan: not a sibling relation's, and none underast.embedded. - Syntax errors
- positive: as its rule alone · negated (under
notorcount, or theparsenegated) 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).
/* global */ items (compile/parse/host_tests.rs pins the shape): an item is reported where it is a declared global (resolves on the item), where a var or function of the file declares it (host), or where an earlier comment lists it.{
"language": "typescript",
"languageOptions": { "sourceType": "script" },
"rule": {
"kind": "comment",
"parse": {
"grammar": "eslintDirective",
"value": { "regex": "^globals?$" },
"has": {
"kind": "item",
"capture": "N",
"not": { "has": { "kind": "setting", "regex": "^off$" } },
"any": [
{ "resolves": { "global": true } },
{
"host": {
"inside": {
"kind": "program",
"has": {
"stopBy": "end",
"any": [{ "kind": "variable_declarator" }, { "kind": "function_declaration" }],
"has": { "field": "name", "pattern": "$V" },
"compare": { "left": "V", "op": "eq", "right": "N", "text": "name" }
}
}
}
},
{
"host": {
"follows": {
"stopBy": "end",
"kind": "comment",
"parse": {
"grammar": "eslintDirective",
"value": { "regex": "^globals?$" },
"has": {
"kind": "item",
"capture": "M",
"not": { "has": { "kind": "setting", "regex": "^off$" } },
"host": { "compare": { "left": "M", "op": "eq", "right": "N" } }
}
}
}
}
}
]
}
}
}
}ast.embedded · embedded #
A file whose language embeds another (a Blade template's PHP, a Vue component's script) is two documents: its own tree, and the program built through each via its registry entry declares (embeds). ast.embedded queries the program; embedded reaches it from a node of the file's own tree.
In the grammar: AstQuery, AstRule
ast.embeddednames aviaof the language'sembeds:compiled-viewforblade,script-blockforvue. Another is refused.- Its
rule,utilsandconstraintsuse 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.hostleads 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.embeddedcompiles once per dialect the via produces.embeddedreads the language's one via; a language embedding through several would needast.embeddedto choose (none does today).
- Shape
- "ast": { "language": "blade" | "vue", "embedded": Via, "rule": AstRule, ... } "embedded": AstRule
- Composition
embeddedholds only on the file's own nodes: in a query withoutast.embeddedwhose language embeds a program, or under ahostreached from the program. Refused in a rule over the program (reach the host node withhostfirst), on a language that embeds none, and insideparse. Utilities and constraints may hold it.- Captures
embedded's rule runs in a fresh environment over the program: host captures are invisible, as inparse. Its bindings export into the host rule's environment, spanning the file bytes their program nodes stand for, where a later host term (acompare, the violation) reads them. Ahostrule inside reads the host's captures as they stood whenembeddedbegan.comparewithtext: "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).
v-html directive whose value calls a function: from the directive in the component's tree, embedded reaches the program's call.{
"language": "vue",
"rule": {
"kind": "directive_attribute",
"has": { "stopBy": "neighbor", "kind": "directive_name", "regex": "^v-html$" },
"embedded": { "kind": "call_expression" }
}
}2 more examples
:title="title", Vue 3.4's same-name shorthand): embedded exports V from the program, and the host rule compares it with the directive's argument. The same comparison may run inside the program rule as host: { compare: ... } where ARG is bound before embedded begins.{
"language": "vue",
"rule": {
"kind": "directive_attribute",
"has": { "stopBy": "neighbor", "kind": "directive_value", "pattern": "$ARG" },
"embedded": { "kind": "identifier", "pattern": "$V" },
"compare": { "left": "ARG", "op": "eq", "right": "V" }
}
}alert called from the template, not from a script block: the query runs on the program, and host places the call in the template.{
"language": "vue",
"embedded": "script-block",
"rule": {
"kind": "call_expression",
"has": { "field": "function", "regex": "^alert$" },
"host": { "inside": { "stopBy": "end", "kind": "template_element" } }
}
}formatArgs #
A format call's arguments no field consumes, or its fields no argument fills. Covers Ruff's Pyflakes rules (F504, F505, F507, F522, F523, F524), PHP's printf family as PHP 8.5.11 and PHPStan's argument.sprintf read it, and Rust's format macros as rustc 1.99.0 binds them.
In the grammar: AstRule, FormatArgs
string: a capture bound before, holding the template's plainstrliteral (through parentheses).args: a capture bound before: a.formatcall'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 asparse'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/fscanfwith variables passed.string: the format, read asprintfreads it and parsed byscanfFormat.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$sleaves, which PHPStan's count does not see. Both are PHP 8.5.11ValueErrors.- With no variable the call returns an array: nothing is missing or unused.
- printf (php)
stringcaptures the format (theargumenta pattern's$Fcaptures, or its expression, through parentheses), parsed byprintfFormat. 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 (aconst, a top-leveldefine, a class constant throughself,parent, a class name, orstaticwhere 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 avsprintf,vprintforvfprintfcall passes.missing: placeholders and stars that number a value past the last passed (PHP'sArgumentCountError).unused: values (argumentorarray_nodes) past the highest any field numbers, which PHP ignores and PHPStan counts. A value a numbered placeholder skips (element_ initializer %2$swith two values) is not unused.$$$Amatches no node after a comma, so a call passing the format alone needs its own pattern (sprintf($F)).- Oracle: PHPStan 2.2.16's
PrintfParametersRuleandPrintfover fixtures (Array Parameters Rule 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 orstd::/core::/alloc::path. stringcaptures the template, a string literal token of the macro's token tree, parsed byrustFormat.argscaptures 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
resolvesreads. - A positional argument after a named one, and a template rustc refuses, are unknown.
- A
- Composition
- Ordered with the relations: it reads
stringandargs, and binds whatunusedbinds andmissingexports. - Unknown
- Any style: a template the grammar refuses; a string that is no plain
strliteral (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.printfandscanf: 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-timedefinemay 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).
- Any style: a template the grammar refuses; a string that is no plain
- 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.
F504, whole: a dict pair no placeholder names./checks/0 · Ruff conformance port{
"id": "percent-format-extra-named-arguments",
"forbid": {
"ast": {
"language": "python",
"rule": {
"pattern": "$S % $R",
"formatArgs": {
"style": "percent",
"string": "S",
"args": "R",
"unused": { "kind": "pair" }
}
}
}
},
"violation": {
"anchor": "witness.span",
"code": "F504",
"message": "`%`-format string has unused named argument(s)"
}
}1 more example
argument.sprintf as one query (written for this page; compile/format_args/php.rs checks the same shape against PHPStan's reports): sprintf('%s %s', $a) misses a value, sprintf('%s', $a, $b) passes one no placeholder reads.{
"language": "php",
"rule": {
"all": [
{
"any": [
{ "pattern": "sprintf($F)" },
{ "pattern": "sprintf($F, $$$A)" },
{ "pattern": "printf($F)" },
{ "pattern": "printf($F, $$$A)" }
]
},
{
"any": [
{
"formatArgs": {
"style": "printf",
"string": "F",
"args": "A",
"missing": { "any": [{ "kind": "placeholder" }, { "kind": "star" }] }
}
},
{
"formatArgs": {
"style": "printf",
"string": "F",
"args": "A",
"unused": { "kind": "argument" }
}
}
]
}
]
}
}Before and after: format-string ports
| 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;$$$NAMEfrom its first node to its last, de-indented) or a named regex group of the witness. replace: every regex match replaced byby(expands$1and${name}).substring: charactersstartChartoendChar, negative from the end.convert: to a case, splitting words atseparatedBy(default: all separators).toPrecision: ECMAScript'sNumber., exactly, of the number the source's text denotes. TypeScript, JavaScript and Vue only.prototype. toPrecision( digits) 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.
toPrecisionreads a numeric literal as written (0x,0o,0b, legacy octal and_separators included) or avalue's text.digitsis a number, or the text of another capture or variable read as an integer.- ESLint's
no-loss-of-precisioncompares a literal withvalue.toPrecision(n)for itsnsignificant digits:9223372036854775808is exact at 19 digits thoughStringgives9223372036854776000. toPrecisiongives the empty text where the source is no number (a BigInt, a name) or the digits no integer from 1 to 100, andString(x)for an infinite or NaN number. Other families' numbers are not laid out as ECMAScript's, so compiling it there is refused.lineuses the linter's terminators.countskips comments and Python line continuations.default: ""withreplace: "^$"supplies a fallback. Withoutdefault, an unbound source gives the empty text and runs no operation, as upstream.value: trueconversions: JavaScriptString(v)('\x61'isa,1e2is100); Pythonstr(v)(1e2is100.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
rulein the ast query. Messages and fixes interpolate variables as$NAME. Acomparereads 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,linesandcountread a capture's node (a variable is no source for them) and take novalue. - Unknown
- With
value, an unknown value reads as an unbound source where there is adefault. 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.
capitalized-comments: replace then convert, and a fix that replaces only the regex group L (fix.range)./checks/0 · ESLint conformance port{
"id": "comment-case",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"all": [
{ "kind": "comment" },
{ "pattern": "$CM" },
{ "regex": "^(?://|/\\*)[\\s*]*(?<L>\\p{Ll})" },
{
"not": {
"regex": "^(?://|/\\*)\\s*(?:eslint|jshint\\s+|jslint\\s+|istanbul\\s+|globals?\\s+|exported\\s+|jscs)"
}
},
{ "not": { "regex": "^(?://|/\\*)[\\s*]*(?:[^:/?#\\s*]\\**)+:\\**/\\**/\\**[^?#*]" } }
]
},
"transform": {
"LT": {
"replace": {
"source": "$CM",
"replace": "^(?://|/\\*)[\\s*]*(\\p{Ll})[\\s\\S]*$",
"by": "${1}"
}
},
"C": { "convert": { "source": "$LT", "toCase": "upperCase" } }
}
}
},
"violation": {
"anchor": "witness.span",
"code": "CAPITALIZED_COMMENTS",
"message": "Comments should not begin with a lowercase character.",
"fix": { "template": "$C", "range": "capture.L" }
}
}2 more examples
max-params: count of a $$$ capture in the message, and replace steps that build the number./checks/7 · ESLint conformance port{
"id": "function-type",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"all": [
{ "kind": "function_type" },
{
"has": {
"stopBy": "neighbor",
"kind": "formal_parameters",
"count": {
"has": {
"any": [{ "kind": "required_parameter" }, { "kind": "optional_parameter" }]
},
"min": 2
}
}
},
{ "pattern": "$F" },
{
"any": [
{
"has": {
"field": "parameters",
"pattern": {
"context": "function f($$$P) {}",
"selector": "formal_parameters"
}
}
},
{ "has": { "field": "parameter", "pattern": "$PB" } }
]
}
]
},
"transform": {
"NN": { "count": { "source": "$$$P" } },
"NC": { "replace": { "source": "$NN", "replace": "^0$", "by": "" } },
"NB": { "replace": { "source": "$PB", "replace": "(?s)^.+$", "by": "1" } }
}
}
},
"violation": {
"anchor": "witness.start",
"code": "MAX_PARAMS",
"message": "Function has too many parameters ($NC$NB). Maximum allowed is 1."
}
}default), and unified-signatures' "the one on line N" (line).{
"language": "typescript",
"rule": {
"kind": "function_signature",
"pattern": "$O",
"any": [{ "has": { "kind": "type_annotation", "pattern": "$R" } }, { "regex": "^" }]
},
"transform": {
"RT": { "replace": { "source": "$R", "default": "", "replace": "^$", "by": "none" } },
"L": { "line": { "source": "$O" } }
}
}Query options
languageOptions #
Configuration the file cannot show, set per ast query. Modelled on ESLint's languageOptions and Ruff's settings.
In the grammar: AstQuery, LanguageOptions
- TypeScript keys follow ESLint (
ecmaVersion,sourceType,globals, …); Python keys follow Ruff (pythonVersion,typingModules,extendGenerics). - Omitted keys take ESLint's defaults (
"latest", the file's own source type, no globals) and Ruff's. - Without
sourceType, each file has its own: from its extension, its nearestpackage.jsontype, then its syntax (see Notes). resolves.global,unresolved,binds.builtinandvalue's builtin names read these options;resolves.this,declaredandscriptDeclarationsrequire 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-levelthisis the global object),module(refused before ES2015) orcommonjs(addsrequire,module,exports,global).- Omitted
sourceType:.cjsand.ctsarecommonjs;.mjs,.mts,.tsxand declaration filesmodule. A.js,.jsxor.tsfile follows thetypeof its nearestpackage.json(the host'spackage, seefile; Node runs.tsfiles like.jsones). With no validtype, Node's syntax detection decides; with nopackage.json,module(ESLint's default). Omitting it withecmaVersionbefore ES2015 is refused. - Syntax detection makes a module of a file with a top-level
importorexportdeclaration that type stripping keeps,import.meta, a top-levelawaitoutside functions, or a top-levelconst,letorclassbindingrequire,module,exports,__dirnameor__filename. Declarations that do not count:import type,export type, an exported interface, type alias ordeclare,import x = require(…)andexport =. Anything else iscommonjs. moduleSystem: default"file"(the reading above); refused besidesourceType."package-type"uses only the extension and the nearestpackage.json'stype, as eslint-plugin-n's configs do:.mjsand.mtsaremodule,.cjsand.ctscommonjs, any other filemoduleonly undertype: "module", elsecommonjs(files without a manifest included). No syntax is read. The query'swhen.sourceTypegates on the same.globals: names mapped toreadonly,writableoroff, with ESLint's aliases; at most 16,384.parserOptions.:ecmaFeatures globalReturn(the top level is a function scope) andimpliedStrict.jsxPragma(default"React";nullfor none) andjsxFragmentName: the first JSX element (fragment) whose scope sees a variable of that name reads it. Base-ESLint ports setnull.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.offremoves 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'slint.typing-modules) andextendGenerics(lint.): which strings are type definitions, and what the typing-aware readings see.pyflakes. extend-generics
- 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 everyresolvesof 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
libmeans none, not typescript-eslint's parser default. Among conflicting/* global */comments,offwins.
no-redeclare: lib: ["esnext"] makes lib globals builtins for binds.builtin./checks/1 · ESLint conformance port{
"id": "redeclared-builtin",
"forbid": {
"ast": {
"language": "typescript",
"languageOptions": { "lib": ["esnext"] },
"rule": {
"pattern": "$NAME",
"any": [
{ "kind": "identifier" },
{ "kind": "type_identifier" },
{ "kind": "property_identifier" },
{ "kind": "shorthand_property_identifier_pattern" },
{ "kind": "shorthand_property_identifier" },
{ "kind": "undefined" }
],
"all": [
{
"all": [
{
"binds": {
"scope": ["global", "module", "function", "block"],
"site": { "not": { "kind": "function_signature" } }
}
},
{
"not": {
"all": [
{ "binds": { "scope": "function" } },
{
"inside": {
"kind": "class_static_block",
"stopBy": {
"any": [
{ "kind": "function_declaration" },
{ "kind": "generator_function_declaration" },
{ "kind": "function_expression" },
{ "kind": "generator_function" },
{ "kind": "arrow_function" },
{ "kind": "method_definition" },
{ "kind": "class_static_block" }
]
}
}
}
]
}
}
]
},
{ "binds": { "builtin": true } }
]
}
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_REDECLARE",
"message": "'$NAME' is already defined as a built-in global variable."
}
}1 more example
jQuery.{
"language": "typescript",
"languageOptions": {
"ecmaVersion": 5,
"sourceType": "script",
"globals": { "jQuery": "readonly" }
},
"rule": { "pattern": "$F($$$)" },
"constraints": { "F": { "resolves": { "global": ["Reflect", "JSON", "jQuery"] } } }
}when #
The configurations a query applies to.
In the grammar: AstQuery, When, Range
- Ranges are inclusive, in
languageOptions' spellings: at least one bound,minnot aftermax. sourceTypetakes one value or a list. EachecmaFeaturesflag must equal the query's.- Keys read the query's own
languageOptions, else the defaults. Without a configuredsourceType,sourceTypereads 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 replaceslanguageOptionsper case. - Languages
pythonVersionpython ·ecmaVersion,sourceType,ecmaFeaturestypescript ·optionboth.- Limits
- Gates only on the query's configured version and features, and on its configured source type or else the file's.
strict (global): one clause per mode, this one gated on scripts and CommonJS without impliedStrict./checks/4 · ESLint conformance port{
"id": "global-missing-explicit",
"forbid": {
"ast": {
"language": "typescript",
"languageOptions": {},
"when": {
"sourceType": ["script", "commonjs"],
"ecmaFeatures": { "impliedStrict": false }
},
"rule": {
"all": [
{ "regex": "." },
{ "not": { "kind": "comment" } },
{ "not": { "matches": "us-stmt" } },
{ "matches": "in-program" },
{
"not": {
"follows": {
"stopBy": "end",
"all": [{ "regex": "." }, { "not": { "kind": "comment" } }]
}
}
}
]
},
"utils": {
"us-lit": {
"any": [
{ "kind": "string", "regex": "^['\"]use strict['\"]$" },
{
"kind": "parenthesized_expression",
"has": { "stopBy": "neighbor", "matches": "us-lit" }
}
]
},
"us-stmt": {
"kind": "expression_statement",
"has": { "stopBy": "neighbor", "matches": "us-lit" }
},
"in-program": { "inside": { "kind": "program", "stopBy": "neighbor" } }
}
}
},
"violation": {
"anchor": "witness.start",
"code": "STRICT",
"message": "Use the global form of 'use strict'."
}
}1 more example
no-redeclare: the ES3 builtin list, gated by ecmaVersion.max, with each over a directive's items./checks/2 · ESLint conformance port{
"id": "global-comment-builtin-es3",
"forbid": {
"ast": {
"language": "typescript",
"languageOptions": { "jsxPragma": null },
"when": { "ecmaVersion": { "max": 3 } },
"rule": {
"kind": "comment",
"parse": {
"grammar": "eslintDirective",
"value": { "regex": "^globals?$" },
"has": {
"kind": "item",
"capture": "N",
"regex": "^(?:Array|Boolean|Date|Error|EvalError|Function|Infinity|Math|NaN|Number|Object|RangeError|ReferenceError|RegExp|String|SyntaxError|TypeError|URIError|constructor|decodeURI|decodeURIComponent|encodeURI|encodeURIComponent|escape|eval|hasOwnProperty|isFinite|isNaN|isPrototypeOf|parseFloat|parseInt|propertyIsEnumerable|toLocaleString|toString|undefined|unescape|valueOf)$",
"not": { "has": { "kind": "setting", "regex": "^off$" } }
}
}
}
}
},
"violation": {
"anchor": "capture.N",
"code": "NO_REDECLARE",
"message": "'$N' is already defined as a built-in global variable.",
"each": "N"
}
}file #
Conditions on the file the query reads.
In the grammar: AstQuery, File, TextCond, Package
- Every key must hold, or the query does not apply to the file: its grammar matcher is skipped, on both paths.
jsxRuntimeis checked once the file is read. pathtests the path the host sends. Python keys read the module path; TypeScript'spackagereads the nearestpackage.json.- Missing input satisfies neither value: no module path fails every Python text condition; no manifest fails
bin: trueandbin: falsealike (see Notes).
Details: notes, shape, languages, limits
- Notes
declaration: typescript-eslint'sisDefinitionFile(.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.moduleis the names joined by.;moduleNamethe last name (__init__for a package's__init__.py);packagethe 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
packagereads the file's nearestpackage.jsonat 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'sbintargets (the string, or each string value of the object), resolved against the manifest's folder as eslint-plugin-n'sisBinFiledoes: the file's path, that path without.js, or without.jsand a last/index.nodeVersion.subsetOf/nodeVersion.notSubsetOf: the configured Node range is / is not a subset of the given npm range (node-semver'ssubset). The configured range is eslint-plugin-n's:engines.node, else theversionof thedevEngines.runtimeentry namednode, else>=16.0.0; a range that does not parse falls through to the next.dependsOn: the manifest lists one of the names independencies,devDependencies,peerDependenciesoroptionalDependencies.path: the file's path relative to the manifest's folder.private: the manifest'sprivateistrue.serverGraph: the manifest namesnextand 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'sapp/(elsesrc/app/, which Next ignores beside a rootapp/), outside private_folders.- Graph edges: static imports,
export … from, andimport()orrequire()of a string, never passing a module whose directive prologue holds"use client". Specifiers resolve relative to the importer with Next's extensions andindexfiles, then through the package'stsconfig.json(elsejsconfig.json)pathsandbaseUrlover relativeextends(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'sgetNpmignorereads it. The file is left out where the manifest'sfilesarray leaves it out, or where the.npmignorein the manifest's folder (else, without afilesarray, its.gitignore) ignores it.filesentries: each non-empty string, a leading!or/dropped and the path normalized, matches as/<entry>and/<entry>/**in the npmignorepackage's gitignore semantics. A!entry matches as<entry>and<entry>/**at any depth. An entry with braces or an extglob goes throughglobrex, numeric and alphabetic brace sequences expanded.- Ignore files use the
ignorepackage, 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. Withoutfilesor an ignore file, every file below the manifest is published. - Host input: the host sends the manifest as the file's
package, itsnodeVersionthe configured range, which must be a valid npm range (apackagewith any other is malformed). It adds the file's ownserverGraphmembership where a code rule reads it, its ownpublished, andprivate: truewhere the manifest is private. A snapshot or workspace file without one reads thepackage.json,.npmignore,.gitignore,tsconfig.jsonandjsconfig.jsonentries and sources of its file set. - Missing input: without a module path no Python text condition holds,
notRegexincluded. Without a manifest, neither value ofbin,private,publishedorserverGraphholds, nordependsOnorpackage.path;serverGraphalso needs a manifest namingnext. A host that sends a file nopublishedgives it neither value. jsxRuntime: the file's JSX runtime is one listed, from the first evidence found:- A
@jsxImportSourcepragma naming React or a React wrapper (react,@emotion/react,theme-ui,@theme-ui/core, or a subpath) givesreact; one naming another JSX library (preact,solid-js,vue,@stencil/core,hono,@builder.io/qwik,inferno,mithril,hyperapp, or a subpath) givesother. - Else a
@jsxpragma namingReactorReact.…givesreact. Each pragma comes from the first comment that has one, read without its//,/*and*/as Babel reads a comment's value (/*@jsx React*/namesReact). Any other pragma is no evidence (Emotion's@jsx jsxrenders React). - Else a top-level static import of
horcreateElementfrom@stencil/core,preact,vue,solid-js,solid-js/h,hyperappormithrilgivesother. - Else the
compilerOptions.of thejsxImportSource tsconfig.json(elsejsconfig.json) beside the nearestpackage.json, through relativeextends, where it names React's or another library's as a pragma would. - 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) givereact, the reverseother; both, neither or no manifest giveunknown.
- A
- 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,byteOrderMarkboth ·declaration,package(aPackage),jsxRuntimetypescript ·module,moduleName,package(aTextCond),inPackagepython- Limits
- A name with a dot qualifies nothing, and
modulecannot tell a dotted name from two names. No key reads the names between the first and the last. packagereads the nearest manifest only (no workspace or monorepo root);jsxRuntimereads no bundler's settings.- A project configuration's relative
extendsis followed only to anothertsconfig*.jsonorjsconfig*.json(the names hosts route), never to a package's: through the first 16 entries of anextendsarray, at most 8 bases deep. - On disk, no configuration (the package's own
tsconfig.jsonincluded) 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
extendsincluded, and reports no failure.serverGraphresolves no import through thepathsorbaseUrlit would set,jsxRuntimereads nojsxImportSourcefrom it, and thejsconfig.jsonbeside an unparsabletsconfig.jsondoes not replace it. serverGraphroots take Next's default page extensions (.tsx,.ts,.jsx,.js;next.config'spageExtensionsis not read); an.mjs,.cjs,.mtsor.ctsmodule is still reached by import. A configuration'spathsmaps 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: truenorserverGraph: falseholds for the package's files. - That walk is not cancelled with an audit.
- A name with a dot qualifies nothing, and
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)./checks/0 · Ruff conformance port{
"id": "implicit-namespace-package",
"forbid": {
"ast": {
"language": "python",
"file": { "inPackage": false },
"rule": {
"kind": "module",
"not": {
"any": [
{
"has": {
"stopBy": "neighbor",
"kind": "comment",
"layout": { "start": { "eq": 0 } },
"regex": "^#!"
}
},
{
"regex": "(?m)^# /// script(\\r\\n|\\r|\\n)(#( [^\\r\\n]*)?(\\r\\n|\\r|\\n))*# ///(\\r?$|\\r)"
}
]
}
}
}
},
"violation": {
"anchor": "file.start",
"code": "INP001",
"message": "File is part of an implicit namespace package. Add an `__init__.py`."
}
}2 more examples
A005, one of its per-version clauses: a module name matched by module, dotted stems excluded by moduleName.notRegex, gated by when.pythonVersion./checks/2 · Ruff conformance port{
"id": "stdlib-module-shadowing-3.7-3.7",
"forbid": {
"ast": {
"language": "python",
"when": { "pythonVersion": { "max": "3.7" } },
"file": {
"module": { "regex": "(?:^|\\.)(?:macpath)(?:\\.__init__)?$" },
"moduleName": { "notRegex": "\\." }
},
"rule": { "kind": "module" }
}
},
"violation": {
"anchor": "witness.span",
"code": "A005",
"message": "Module shadows a Python standard-library module"
}
}no-unused-vars: the clause for ordinary files (declaration: false); a twin clause reads declaration files./checks/0 · ESLint conformance port{
"id": "unused-args",
"forbid": {
"ast": {
"language": "typescript",
"file": { "declaration": false },
"languageOptions": {},
"rule": {
"pattern": "$NAME",
"any": [
{ "kind": "identifier" },
{ "kind": "type_identifier" },
{ "kind": "property_identifier" },
{ "kind": "shorthand_property_identifier_pattern" },
{ "kind": "shorthand_property_identifier" },
{ "kind": "undefined" }
],
"all": [
{
"all": [
{
"resolves": {
"write": "last",
"binding": {
"scope": [
"module",
"function",
"class",
"block",
"namespace",
"typeParameters"
],
"directives": "eslint",
"form": "parameter",
"used": false,
"exported": false
}
}
}
]
}
]
}
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_UNUSED_VARS",
"message": "'$NAME' is defined but never used."
}
}Violations
anchor · message · code #
Where a violation points, and what it says.
In the grammar: Clause, Violation
anchor: the whole witness, its start or end, byte 0 of the file (file.start), or a capture (capture.NAME).message:$NAMEand$$$NAMEinterpolate 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$.variablesis reserved: only{}is accepted.
Details: notes, shape, composition, captures
- Notes
- Anchors:
file.startis byte 0 whatever the witness, for file-level diagnostics such as INP001. A capture is an ast metavariable, a witness-levelregex's or regex query's named group, or a parsed tree's export. - No span: a
file.pathmatch or afilequery's file names the file with no line, unless anchored atfile.start. It takes nocapture.NAMEanchor. - 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.
- Anchors:
- 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 onlywitness.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.
F823: $N in the message./checks/0 · Ruff conformance port{
"id": "undefined-local",
"forbid": {
"ast": {
"language": "python",
"rule": { "kind": "identifier", "pattern": "$N", "resolves": { "beforeLocal": true } }
}
},
"violation": {
"anchor": "witness.span",
"code": "F823",
"message": "Local variable `$N` referenced before assignment"
}
}2 more examples
INP001: file.start./checks/0 · Ruff conformance port{
"id": "implicit-namespace-package",
"forbid": {
"ast": {
"language": "python",
"file": { "inPackage": false },
"rule": {
"kind": "module",
"not": {
"any": [
{
"has": {
"stopBy": "neighbor",
"kind": "comment",
"layout": { "start": { "eq": 0 } },
"regex": "^#!"
}
},
{
"regex": "(?m)^# /// script(\\r\\n|\\r|\\n)(#( [^\\r\\n]*)?(\\r\\n|\\r|\\n))*# ///(\\r?$|\\r)"
}
]
}
}
}
},
"violation": {
"anchor": "file.start",
"code": "INP001",
"message": "File is part of an implicit namespace package. Add an `__init__.py`."
}
}${F} sits before name characters in the message and the fix.{
"id": "prefer-async",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"pattern": "$F($$$ARGS)",
"has": { "kind": "identifier", "regex": "^read", "stopBy": "neighbor" }
}
}
},
"violation": {
"code": "PREFER_ASYNC",
"anchor": "witness.span",
"message": "Prefer ${F}Async.",
"fix": "${F}Async($$$ARGS)"
}
}each #
One violation per distinct binding of a capture at a witness.
In the grammar: Violation
- The rule is rematched, excluding ranges already bound, until nothing new binds or
maxWitnessesis 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),unusedbindings 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
anyskip 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
nthChildprobes 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.
no-irregular-whitespace: each over a regex group, one violation per run./checks/0 · ESLint conformance port{
"id": "in-token",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"any": [{ "kind": "comment" }, { "kind": "regex" }],
"regex": "(?P<WS>[\\x0B\\x0C\\x{85}\\x{FEFF}\\x{A0}\\x{1680}\\x{180E}\\x{2000}-\\x{200B}\\x{202F}\\x{205F}\\x{3000}]+|[\\x{2028}\\x{2029}])"
}
}
},
"violation": {
"anchor": "capture.WS",
"code": "NO_IRREGULAR_WHITESPACE",
"message": "Irregular whitespace not allowed.",
"each": "WS"
}
}1 more example
W605 in format specs: an alternation that walks every escape, naming the group only where the escape is invalid./checks/1 · Ruff conformance port{
"id": "invalid-escape-sequence-format-spec",
"forbid": {
"ast": {
"language": "python",
"rule": {
"kind": "format_specifier",
"regex": "\\\\[\\n\\r\\\\'\"abfnrtv0-7xNuU]|(?<BS>\\\\(?:[^\\n\\r\\\\'\"abfnrtv0-7xNuU]|$))",
"inside": {
"stopBy": "neighbor",
"kind": "interpolation",
"inside": {
"stopBy": "neighbor",
"kind": "string",
"not": { "has": { "stopBy": "neighbor", "kind": "string_start", "regex": "[rR]" } }
}
}
}
}
},
"violation": {
"anchor": "capture.BS",
"code": "W605",
"message": "Invalid escape sequence",
"each": "BS"
}
}Before and after: E712
Before: "rule": { "kind": "comparison_operator", "has": { "stopBy": "neighbor", "matches": "flagged-bool-operand" } }, one violation per comparison (21 diagnostics on the fixture). After: bind the operand ("pattern": "$OP" in the has) and add "each": "OP": res == True != False gives two violations at one span with two ids, as Ruff does (26 diagnostics, exactly Ruff's). PIE810, SIM101, SIM109, SIM220/221 and T100 follow the same recipe.
fix #
A suggested replacement, never applied.
In the grammar: Violation
- ast query: ast-grep's
TemplateFixover metavariables and transform variables, indentation rules kept. file.contentregex: the template substitutes named groups.rangereplaces 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
forbidover an ast orfile.contentregex query;rangeonly 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.
no-unneeded-ternary: a template over two metavariables./checks/4 · ESLint conformance port{
"id": "invert-eq",
"forbid": {
"ast": {
"language": "typescript",
"rule": { "kind": "ternary_expression", "pattern": "$L == $R ? false : true" }
}
},
"violation": {
"anchor": "witness.span",
"code": "NO_UNNEEDED_TERNARY",
"message": "Unnecessary use of boolean literals in conditional expression.",
"fix": "$L != $R"
}
}1 more example
no-div-regex: a fix that replaces only the =, a regex group./checks/0 · ESLint conformance port{
"id": "leading-equals-regex",
"forbid": {
"ast": { "language": "typescript", "rule": { "kind": "regex", "regex": "^/(?<EQ>=)" } }
},
"violation": {
"anchor": "witness.span",
"code": "NO_DIV_REGEX",
"message": "A regular expression literal can be confused with '/='.",
"fix": { "template": "[=]", "range": "capture.EQ" }
}
}suppress #
Honours another linter's suppression comments as that linter does, opt-in per violation. Rerail's own rerail-ignore needs no opt-in.
In the grammar: Violation, Suppress
suppressnames, 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
oxlintandbiome, over its anchored span. - A suppressed violation is dropped before the witness budget counts it.
- Without
suppress, onlyrerail-ignoresilences a rule.
Details: notes, shape, composition, rerail's comment, limits
- Notes
eslint(TypeScript, Vue): ESLint 10.11.0'sapplyDisableDirectives. Blockeslint-disable/eslint-enablefrom where they stand,eslint-disable-lineon its line,eslint-disable-next-lineon the line after the comment ends; each names one ofrulesor 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-lineandeslint-disable-next-line, read as its processor reads them. An HTML-like comment (<!--, or-->first on its line) is a//comment whose-lineand-next-linedirectives count; ESLint parses it as a line comment in a script, not at all in a module.oxlint(TypeScript): Oxlint 1.88.0'sDisableDirectivesBuilderandDisableDirectives::(oxc tagcontains oxlint_v1.88.0), defaultrespect. ReadsEslint Disable Directives oxlint-andeslint-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:enablecloses the first opendisablenaming no rule,enable Xthe first opendisable X. Unclosed blocks run to end of file, soenable Xinside adisableleaves X disabled. A block covers a violation whose anchored span overlaps it, even one starting before thedisable.-linecovers one starting on the comment's first line, before it;-next-lineone starting after it on its last line, or on the next line.rulesare oxlint's names without a plugin; a listed name counts by its part after the last/(react-hooks/andrules-of-hooks foo/rules-of-hooksboth namerules-of-hooks;canonical/no-re-exportdoes not nameexport).biome(TypeScript): Biome 2.5.15's JavaScript analyzer (tag@biomejs/biome@2.5.15:biome_suppression'sparse_,suppression_ comment biome_analyze'sto_analyzer_suppressionsandSuppressions). A comment line (each block-comment line past its*s) startsbiome-ignore,biome-ignore-all,biome-ignore-startorbiome-ignore-end, then white-space-separated categories,:and a reason:// biome-ignore lint/.correctness/ useHookAtTopLevel: intentional lintnames every lint rule,lint/<group>a group,lint/<group>/<rule>one.lint/names an instance and never matches here (a violation reports no instance). A line with no reason or<group>/ <rule>( value) :, an unclosed(, or a category Biome lacks (lint/) is refused whole and suppresses nothing; a group or rule the JavaScript analyzer lacks (a CSS rule) is refused alone. No HTML-like comments:correctness/ useHookAtTopLevell biome-ignoreafter<!--or-->does nothing.biome-ignorecovers 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-allcovers the file if before the first token (a shebang aside), else nothing.biome-ignore-startcovers violations it holds whole, up to the closingbiome-ignore-endof the same category (the latest open one) or end of file.rulesare full names,lint/<group>/<rule>, of Biome 2.5.15's JavaScript lint rules.noqa(Python): Ruff 0.16.9'scheck_noqa: a file-level# ruff: noqaor# flake8: noqaon its own line, then# noqaor# noqa: CODESon the violation's line via Ruff'sNoqaMapping(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,B102names B102). It counts on the violation's line and the witness's first line carrying one (Bandit'slinerangeof the visited node). The lists join, so a blanket comment beside a naming one names only those tests. Suppresses when naming one ofcodes, or blanket.phpstan(PHP): PHPStan 2.2.16's@phpstan-ignorenaming one ofidentifierson the line it applies to, and@phpstan-ignore-line/-next-line.psalm(PHP): Psalm 6.19.1's@psalm-suppressin 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 ofissuesor itsConfig::parent;getParentIssueType allcounts only withsuppressAll: true(Psalm'sdisableSuppressAll="false").phpcs(PHP): PHP_CodeSniffer 4.0.4'sphpcs:ignore(alone on its line, it ignores the next),phpcs:disable/phpcs:enableranges andphpcs:ignoreFile, also as@phpcs:, naming one ofsniffs, its sniff, category or standard, or nothing.version: 3reads as PHP_CodeSniffer 3.13.6, with its.exceptrules and the@coding/Standards Ignore Line Start/End/Fileannotations 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,notoverclippy; other cfg names are false). Drops the violation when the level in force for one oflintsisalloworexpect. Lints are spelled as in attributes:clippy::double_parens, a group such asclippy::all, orwarnings. The narrower attribute wins, then the later on one item; aforbidis never lowered. Withmsrv("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'snoqa(core/rules/noqa.py) in--and/* */comments:-- noqaor-- noqa: LISTon the violation's line;-- noqa: disable=LIST/enable=LISTranges from their line on;allnames every rule.rulesare the keys of the rule'sRulePack.reference_map: code, legacy aliases, groups and name (["AL05", "L025", "aliasing", "aliasing.). An entry matches by name or as a case-sensitiveunused", "all", "core"] fnmatchglob.
- 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.
@operator silences run-time warnings, not a linter: no suppression. The engine reads these comments nowhere else. A ported rule carriessuppress, 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# noqaor# nosectoo) silences the port, unless the repository sets the rule'ssuppressionssetting toignore(docs/rule-scoping.md). A pack'shonourmakesSuppression Comments: false ignoreits rules' starting value. - Python:
- Rerail's comment
rerail-ignoreis Rerail's own suppression comment, separate fromsuppress. Every code rule answers to it, with nothing in the rule document. It is always honoured; the repository'ssuppressionssetting 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 startsrerail-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-. Ids match a code rule's documentcyclomatic- typescript, acme:no-print -- generated idexactly, case-sensitively. No id silences nothing;rerail-ignoredandrerail-ignore-nextare 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
<?phpbefore 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 xcoversb();). - 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 xon its own line of an argument list coversa(), not later arguments;x = 1; // rerail-ignore xin 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 xcovers the declaration;m(n, // rerail-ignore xthe call.
- Before the file's first code (only comments, white space, a shebang and PHP's
- 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_eachevidencerows,budgetrows (it silences the group's violation only at the row it is reported at), aselect_when'sexistsside, or acallsseed.suppressis refused onrequire_eachandbudget. - Languages: TypeScript, TSX, JavaScript, Python, PHP and Blade (its
{{-- --}}and<!-- -->comments and its embedded PHP's).
- Syntax: a comment node of the file's grammar (never string text). After its opener (
- Limits
- Not modelled:
- unused-directive reports (
UnusedPsalmSuppress, SQLFluff'swarn_unused_ignores); - Bandit's string tests reading
# nosecover the string's parent node lines (anosecport reads its witness's lines); /* eslint rule: off */configuration comments; oxlint'srespect;Eslint Disable Directives: false - Biome's instance suppressions (
use) and configuration (Exhaustive Dependencies( dep) biome.json); - Ruff's range suppressions, RUF100, code redirects (a port lists old codes itself);
phpcs:setproperty changes; a Psalm class tag reaching the class's trait methods;- SQLFluff's
disable_noqaanddisable_noqa_except.
- unused-directive reports (
strict valid/13, where /* eslint-disable rule-to-test/strict */ removes the report (the harness renames the rule, so both names are listed).{
"id": "function-missing",
"forbid": {
"ast": {
"language": "typescript",
"rule": {
"kind": "function_declaration",
"not": {
"has": {
"kind": "statement_block",
"has": { "kind": "expression_statement", "regex": "^['\"]use strict['\"]" }
}
}
}
}
},
"violation": {
"code": "STRICT",
"anchor": "witness.start",
"message": "Use the function form of 'use strict'.",
"suppress": { "eslint": { "rules": ["strict", "rule-to-test/strict"] } }
}
}4. Unknown and soundness #
How a rule decides when the file alone cannot, and which rules still run on files with syntax errors.
The three-valued model #
A term's verdict is true, false or unknown. Unknown means the file alone cannot decide; see the sources below.
- At the root, unknown is no violation.
- Under
notit 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:
valueon an expression it cannot decode.comparein thevalue,member, structural and transform-of-value readings.argumentsinlanguagemode past a starred argument or**kw.- The usage filters of
bindsandresolves.bindingunderlocals(), a dynamic__all__, a directevalorwith. parseandformatArgson literals they cannot read.nthChild.ofRuleover 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
resolvesrules incomplete there (13web/srcfiles, 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 |
|---|---|---|
|
|
|
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, awithin, what anotwould bind, a transform variable's roots, or a gap's target. - Late terms read everything bound before them.
anyreads what any branch reads, even where another branch binds the name.matchesbinds what its utility binds, inside another utility too. A call binds and reads its parameters by the caller's names.hostreads the host captures as they stood when itsparse, or theembeddedaround it, began (an atomic term): not a sibling relation's, and none underast.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:
hasin document order,insideoutwards,precedesforwards,followsbackwards. A later sibling's comparison sees only that binding. Enumerate witheach, or move the comparison into the relation's rule so the walk goes on to the first node whose captures compare. - An explicit
allis a sequence. Write the binder first; the compiler refuses the reverse. - Negation reads. A
notnaming a capture a sibling binds tests the bound value; anotwith a fresh name binds nothing outward. - A frame's captures are its own. A message cannot name
$YPbound 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
kindunless 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
allorder 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.
allgoes on past an unknown term; terms mentioning its would-be captures stay open. eachreaches 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
importresolution. An unconditional Pythondelunbinds by order (Ruff'sDeletion); 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
ReferenceTrackerfollows every write; Rerail models only the effectively constant case. resolvesbinds 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 aglobalmatch. Python 2'sprint >> xis a keyword token, not aprintreference. Notebook builtins are not modelled; onlyunresolvedknows__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 directevalorwithcan see. - Python only:
latest,outer. TypeScript only:occurrence,builtin,redefines.every,directiveson a filter,write,file.declaration,lib, private forms. onlycounts the bindings the table records (Ruff also counts a builtin it materialized).bindshas noonly;stopAtGlobalisresolves.binding's alone.redefinescompares forks by the innermost branch; ports approximate Ruff'sTYPE_CHECKINGdominance and@overloadby qualified name. Anonlocalbinding redefines nothing.- Private names are bindings of
bindsonly. tree-sitter-typescript 0.23 does not parseaccessorfields. - Python identifiers are NFKC-normalized wherever names are resolved or compared, as in CPython and Ruff (
𝒞isC,F821_28). Kept as written: string contents (__all__entries,**{…}keys, format fields), violations, spans,regex,source/tokenstext, and names a code rule lists. - ast-grep's matcher compares pattern identifiers and twice-bound metavariables by source text, so
$X == $Xmisses𝒞 == C; usecomparewithstructure. ECMAScript and PHP names compare as written. Identifier tests use Rust's current Unicode tables, newer thanesutils'. - Declared TypeScript globals are ESLint's configured globals plus, with
lib, typescript-eslint's lib variables (none withoutlib).readonly/writableare not told apart. Among conflicting/* global */comments,offwins; ESLint takes the last. sloppyFunctionsmodels strictness, not which calls bindthis.parserOptionsreads onlyglobalReturnandimpliedStrict.resolves.importedreads a relative module by text only.seenBeforeplaces deferred annotations, string annotations and a stub's class bases where written. A dotted name qualifies nothing;file.modulecannot 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, transcendentalMath,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. valueholds only on expressions.keyis ESLint core's (a nameless key equals no key).memberis unknown wherever a key does not fold, so "both unknown" is never equal (typescript-eslint'shasOverloadSignaturescalls it equal).tokenscompares leaf texts, not token types. JavaScript orders follow the relational operators whileeqstaysObject.is(null <= 0but noteq); no loose-equality mode.argumentshas no variadic slot and ignores arguments without a slot.estreehas no "every argument equals the parameter at its index" form.- The structural modes are Python's.
structure-partsreads 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 (
²). JSX text has no value.
Sub-grammars, layout, transforms and suppression
parseholds on literal nodes only and reads onlystrliterals; f-strings with a field,tstrings and bytes are unknown.braceFormatreads one level of nested fields. Where Ruff and CPython differ, the grammars follow Ruff. Pythonrepatterns are not parsed;ecmaRegexis 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 (witheach, one per capture). Type comments and doctests are not read. Three differences in semantic syntax errors of complex strings remain. eslintDirectivehas no items foreslintandeslint-envcomments; a setting lies outside its item's range; an item'sresolvestakesglobalalone.- A
hostrule 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.
embeddedreads a language's onevia, and its rule sees no host capture. Compare host and program captures inside ahostrule, or in the host rule afterembeddedexports them.layoutreads lines, not columns; a token is a tree-sitter leaf;blanktreats the file's edge as a token; a layout on a parsed node is false. curly'sisCollapsedOneLinerends a body before a trailing;, whichspancounts.- With bare
\rline endings, tree-sitter-python's comment token swallows the code after it (a grammar gap). transformhas norewrite, string form, length or arithmetic. Variables compare as text only;valueconverts 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).countcounts a$$$capture's named nodes, not its comments or line continuations; it cannot count a relation's matches.nthChildandindexcount tree-sitter's named children and commas;fieldnames tree-sitter's fields.suppressis 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/extendGenericsbuild a table per setting.whengates on configured values only.file.inPackagereads 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
transformis not readable in a frame. - Every query reaching a shared utility compiles it; its
allorder 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.
caseskeys numbers by JavaScript'sString(n)(1.0selects"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
usingdeclarations and misparsesf<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 asrcembeds none of its expressions. - An older Rerail breaks on a newer catalog row (the strict
codeRuleSchema), since one~/.rerail/rerail.sqliteserves 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/); --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/, 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.
ecmaRegexis refused ("not supported yet").- The TypeScript
parseplumbing it would need exists in a minimal form, built foreslintDirective.
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
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$incompare,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(ormaxVisitsPerNodeper 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
maxWitnesseswitnesses (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, ormaxFilesof 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/.