UNPKG

pi-lens

Version:

Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo

130 lines (103 loc) • 4.84 kB
--- name: pi-lens-write-tree-sitter-rule description: Use when writing a new pi-lens tree-sitter query rule YAML file — covers schema, S-expression syntax, capture names, predicates, and gotchas --- # Writing a pi-lens tree-sitter Rule Drop path: `rules/tree-sitter-queries/<language>/<id>.yml` Language dir is **lowercase**: `typescript` `javascript` `tsx` `python` `go` `rust` `java` `csharp` `kotlin` `php` `ruby` `cpp` `c` `css` Project rules merge with built-ins (both run). To disable a language's built-ins: rename dir to `<lang>-disabled/`. ## Minimal template ```yaml id: no-eval severity: error inline_tier: blocking # blocking | warning | review language: typescript # lowercase, inferred from dir if omitted message: "eval() is dangerous — use a safer alternative" query: | (call_expression function: (identifier) @FN (#eq? @FN "eval")) metavars: [FN] has_fix: false ``` ## S-expression query syntax ```scheme ; Node with field (call_expression function: (identifier) @NAME arguments: (arguments) @ARGS) ; Alternatives — ALL branches must use the SAME capture names [ (function_declaration name: (identifier) @FN) (arrow_function) @FN ] ; Predicates (inline) (#eq? @NAME "fetch") ; exact match (#match? @NAME "^on[A-Z]") ; regex match ``` ## Predicates via YAML (faster — runs in WASM) ```yaml predicates: - type: eq # eq | match | any-of var: "@FN" value: "dangerousMethod" - type: match var: "@NAME" value: "^(get|set)[A-Z]" ``` ## inline_tier | Value | Effect | |---|---| | `blocking` | Blocks agent turn — 🔴 injected immediately | | `warning` | Advisory only | | `review` | Low-priority suggestion | ## Gotchas ``` ❌ Mixed capture names in [...] alternatives — zero matches, no error [ (fn_decl name: (id) @NAME) (method body: (block) @BODY) ] → split into two separate [...] blocks ❌ Field value as alternative right: [(identifier) (call_expression)] ← INVALID → use separate query blocks instead ❌ @lowercase captures — convention is @UPPER_SNAKE @fn → @FN ✅ Find node kinds: use --debug-query CST in ast-grep playground or https://tree-sitter.github.io/tree-sitter/playground ✅ Schema + autocomplete: rules/tree-sitter-queries/rule-schema.json ✅ Docs: docs/custom-rules.md ``` ## Hard-won gotchas (verified, not theoretical) ``` ❌ `_` matches NAMED nodes only — never anonymous tokens (operators, punctuation) To match/capture an operator, use the literal token string: (binary_expression operator: _ @OP) ← captures NOTHING for `==` (binary_expression ["==" "!="] @OP) ← correct (anonymous tokens) Anonymous-token alternation `["==" "!="]` works as a CHILD; as a FIELD value it may not compile under the WASM grammar — prefer the child form. ❌ The bundled WASM grammar ≠ the playground / @ast-grep/napi grammar (version skew) Real example: WASM tree-sitter-typescript wraps the for-loop condition in an `expression_statement` and exposes NO `condition:` field: (for_statement condition: (binary_expression ...)) ← 0 matches (no such field) (for_statement (expression_statement (binary_expression ...))) ← correct Always verify field names / nesting against the WASM grammar, not the playground. ❌ A query that fails to compile returns 0 matches SILENTLY (no error thrown). So "0 matches" can mean "wrong query", not "nothing to find". → Probe incrementally: start `(target_kind) @X`, confirm >0, then add one layer (field, child, predicate) at a time until it narrows correctly. ✅ Verify with the production path, not just the playground: client.runQueryOnFile(queryDef, filePath, "typescript") (TreeSitterClient compiles via the same WASM the runner uses.) ✅ For rule-bug fixes, also add a real-runner regression test through treeSitterRunner.run() via makeRealRunnerCtx (tests/support/real-runner-ctx.ts) — runQueryOnFile bypasses dispatch (skip_test_files, tiers, delta, rule cache). Template: tests/clients/dispatch/runners/tree-sitter-skip-test-files.test.ts. (These test paths exist only in a pi-lens source checkout — the installed npm package ships no tests/ directory.) ✅ JS files also run typescript/ rules (shared grammar) — one rule in rules/tree-sitter-queries/typescript/ covers BOTH .ts and .js. No -js copy needed. ✅ Match a header-field child precisely without a field name by relying on direct children: for_statement's children are lexical_declaration (init), expression_statement (condition), update_expression (increment), statement_block (body) — so a binary_expression nested in the condition's expression_statement is unambiguous, and the body's nodes are inside statement_block (not matched). ```