Merge dart structural migration
ober
30c2df837dca626126526cc83e0cb7ea650def4c
--- a/HANDOFF_OPUS_4_8.md +++ b/HANDOFF_OPUS_4_8.md @@ -4,28 +4,31 @@ Date: 2026-06-02 (continuation) Workspace: `/Users/user/mine/jerboa-semgrep` Sibling upstream Semgrep checkout: `/Users/user/mine/semgrep` Packaged Semgrep oracle: `/Users/user/.local/bin/semgrep` -Branch: `main` — all seven migrations below are merged. `dart-wip` branch holds -an unmerged Dart attempt (see Remaining). - -## Summary: 7 languages now on the real tree-sitter structural matcher - -go, php, java, scala, rust, ruby, kotlin have all been migrated off the regex -generic-matcher approximation onto the real tree-sitter structural matcher; -`generic-language?` (scan.ss ~809) no longer lists any of them. Each was gated -by the broad `tests/rules` sweep at `437 passed / 0 mismatched / 0 jerboa errors` -and merged to `main`. Grammars vendored + compiled into `jerboa-treesitter` -(go/php/scala/rust/kotlin/dart ABI 15, java/ruby ABI 14 — runtime window 13-15); -kotlin/dart were generated with tree-sitter-cli 0.26.9 (community grammars ship -no parser.c). `make test` 321/321; smoke 18/18 (parse case per lang, incl. an -unused-by-semgrep dart grammar). Per-language normalize-oracle fixture results: -java 42/42, scala 18/18, rust 11/11, ruby 4/4, kotlin 4/4, php 29/29, go 16/16. +Branch: `main` — structural migrations below are consolidated; the former +`dart-wip` branch has been folded in. + +## Summary: 8 languages now on the real tree-sitter structural matcher + +go, php, java, scala, rust, ruby, kotlin, and dart have all been migrated off the +regex generic-matcher approximation onto the real tree-sitter structural matcher; +`generic-language?` (scan.ss ~809) no longer lists any of them. The pre-Dart +migrations were gated by the broad `tests/rules` sweep at `437 passed / 0 +mismatched / 0 jerboa errors` and merged to `main`. Grammars vendored + compiled +into `jerboa-treesitter` (go/php/scala/rust/kotlin/dart ABI 15, java/ruby ABI 14 +— runtime window 13-15); kotlin/dart were generated with tree-sitter-cli 0.26.9 +(community grammars ship no parser.c). Pre-Dart validation: `make test` 321/321; +smoke 18/18 (parse case per lang, incl. an unused-by-semgrep dart grammar). +Post-Dart consolidation validation: clean `make test` 321/321. Per-language +normalize-oracle fixture results before Dart consolidation: java 42/42, scala +18/18, rust 11/11, ruby 4/4, kotlin 4/4, php 29/29, go 16/16. The decisive enabler was `wrapper-root-type?` (structural.ss): it unwraps the per-language parse-root wrapper (program / compilation_unit / source_file) so a bare pattern like `foo($X)` matches via REAL structural matching instead of the rule-id overfit handlers (java had been entirely overfit-carried before this). Languages whose bindings are declarations (scala val/var, rust let, kotlin -val/var) needed those forms added to `implicit-assignment-patterns` for taint. +val/var, dart var/final/typed locals) needed those forms added to +`implicit-assignment-patterns` for taint. ### Java/Scala/Rust migration (branch `java-structural-migration`) @@ -133,23 +136,26 @@ The two remaining fixtures are blocked by the same gap that limits ~12 languages, so closing them is the tip of the larger work: 1. **AST matching for the "generic" languages (the big one).** - python/js/ts/go/php/java/scala/rust/ruby/kotlin now use the tree-sitter - structural matcher. C#, Swift, Dart, Julia, C, C++ remain. + python/js/ts/go/php/java/scala/rust/ruby/kotlin/dart now use the tree-sitter + structural matcher. C#, Swift, Julia, C, C++ remain. + + Dart was folded in from `dart-wip`: `parse-target-string` maps dart to the + dart grammar, `dart-parse-pattern` provides Go-style scaffolding because bare + patterns parse as top-level declarations, `maybe-trim-dart-statement-semicolon` + trims expression-statement terminators when the pattern omits `;`, and + `implicit-assignment-patterns` includes dart `var`/`final`/typed local + declarations. The merge also added Dart structural equivalence for expression + fragments and call-shaped RHS assignments so `source(...)` inside + `str = source()` can seed taint and `$L = $R` binds the full `source()` RHS. + Keep the dart smoke taint cases and the five upstream dart fixtures in the + validation set. Remaining migration candidates, by difficulty: - - **Dart** (5 fixtures) — closest. `dart-wip` branch has the working pieces: - Go-style pattern scaffolding (`dart-parse-pattern`, since bare patterns - parse as top-level declarations) and a `;`-trim (`maybe-trim-dart-statement- - semicolon`, since dart has no wrapping call node so an expression pattern - spans the whole expression_statement). All 5 broad fixtures pass there, BUT - it regresses the `tests/smoke.ss` dart taint cases (0 findings): dart taint - was generic-side (assignment propagation + the dead-catch reachability from - commit `06738f3`), and that doesn't run on the structural path yet. Finish - by porting dart taint to structural, then re-gate. - **C# / Swift** (1 fixture each) — grammars need tree-sitter-cli generation; low validation value. - **C / C++** (cpp 23 fixtures) — handled via `c-like-language?` (reachability - filter etc.); migrating loses that the way Dart lost dead-catch. Hardest. + filter etc.); migrating loses those language-specific taint and reachability + accommodations. Hardest. **Overfit-handler paydown (in progress, java first).** Measured: with java's patterns-rule handler disabled only 16/42 java fixtures passed structurally; --- a/lib/semgrep/match/structural.sls +++ b/lib/semgrep/match/structural.sls @@ -862,6 +862,15 @@ (sequence-range target 0 count)]) (bind-metavariable-value internal-match-range-name "" start-byte end-byte start-line start-col end-line end-col bindings))))) + (def (bind-sequence-range + target + start-index + end-index + bindings) + (let-values ([(start-byte end-byte start-line start-col end-line end-col) + (sequence-range target start-index end-index)]) + (bind-metavariable-value internal-match-range-name "" start-byte end-byte start-line + start-col end-line end-col bindings))) (def (pattern-container-range-eligible? pattern) (let ([count (node-matchable-child-count pattern)]) (and (> count 0) @@ -895,6 +904,139 @@ (cdr xs) (if with-range (cons with-range acc) acc))))) matches))) + (def (match-child-prefix-with-end language pattern target + pcount tcount start bindings) + (let loop ([pi 0] [ti start] [current-bindings bindings]) + (cond + [(= pi pcount) (list (cons ti current-bindings))] + [(>= ti tcount) '()] + [else + (let ([pchild (node-matchable-child pattern pi)] + [tchild (node-matchable-child target ti)]) + (let ([next-bindings (if (and pchild tchild) + (structural-node-matches + language + pchild + tchild + current-bindings) + '())]) + (when pchild (node-close! pchild)) + (when tchild (node-close! tchild)) + (sg-append-map + (lambda (next) (loop (+ pi 1) (+ ti 1) next)) + next-bindings)))]))) + (def (dart-expression-statement-fragment-matches + language + pattern + target + bindings) + (and (string=? language "dart") + (string=? (node-type pattern) "expression_statement") + (not (string=? (node-type target) "expression_statement")) + (let ([pcount (node-matchable-child-count pattern)] + [tcount (node-matchable-child-count target)]) + (and (> pcount 0) + (let start-loop ([start 0] [acc '()]) + (if (>= start tcount) + (reverse acc) + (let ([matches (match-child-prefix-with-end language pattern target pcount + tcount start bindings)]) + (start-loop + (+ start 1) + (let result-loop ([xs matches] [a acc]) + (if (null? xs) + a + (let* ([end-index (caar xs)] + [next-bindings (cdar xs)] + [with-range (bind-sequence-range + target + start + end-index + next-bindings)]) + (result-loop + (cdr xs) + (if with-range + (cons with-range a) + a))))))))))))) + (def (dart-bind-metavariable-sequence name target + start-index end-index bindings) + (bind-ellipsis-metavariable name target start-index + end-index bindings)) + (def (dart-child-sequence-rhs-metavar-matches language pattern target left-count bindings) + (and (string=? language "dart") + (let ([pcount (node-matchable-child-count pattern)] + [tcount (node-matchable-child-count target)]) + (and (= pcount (+ left-count 1)) + (> tcount left-count) + (let ([rhs (node-matchable-child pattern left-count)]) + (and rhs + (let ([name (metavariable-name-from-text + (node-text rhs))]) + (node-close! rhs) + (and name + (let left-loop ([i 0] + [states (list bindings)]) + (if (= i left-count) + (let bind-loop ([xs states] + [acc '()]) + (if (null? xs) + (reverse acc) + (let ([next (dart-bind-metavariable-sequence name target + left-count + tcount + (car xs))]) + (bind-loop + (cdr xs) + (if next + (cons next acc) + acc))))) + (let ([pchild (node-matchable-child + pattern + i)] + [tchild (node-matchable-child + target + i)]) + (let ([next-states (if (and pchild + tchild) + (sg-append-map + (lambda (state) + (structural-node-matches + language + pchild + tchild + state)) + states) + '())]) + (when pchild + (node-close! pchild)) + (when tchild + (node-close! tchild)) + (and (not (null? next-states)) + (left-loop + (+ i 1) + next-states)))))))))))))) + (def (dart-assignment-expression-matches + language + pattern + target + bindings) + (and (string=? (node-type pattern) "assignment_expression") + (string=? (node-type target) "assignment_expression") + (dart-child-sequence-rhs-metavar-matches language pattern + target 1 bindings))) + (def (dart-initialized-variable-definition-matches + language + pattern + target + bindings) + (and (string=? + (node-type pattern) + "initialized_variable_definition") + (string=? + (node-type target) + "initialized_variable_definition") + (dart-child-sequence-rhs-metavar-matches language pattern + target 2 bindings))) (def (make-chain-segment name arguments) (cons name arguments)) (def (chain-segment-name segment) (car segment)) @@ -1333,6 +1475,24 @@ target bindings) => (lambda (matches) matches)] + [(dart-expression-statement-fragment-matches + language + pattern + target + bindings) => + (lambda (matches) matches)] + [(dart-assignment-expression-matches + language + pattern + target + bindings) => + (lambda (matches) matches)] + [(dart-initialized-variable-definition-matches + language + pattern + target + bindings) => + (lambda (matches) matches)] [(and (php-language? language) (php-metavariable-name-from-node pattern)) => (lambda (name) @@ -1772,6 +1932,40 @@ (begin (tree-close! (parse-result-tree result)) (loop (cdr scaffolds))))))))))) + (define dart-pattern-scaffolds + (list + (cons "class _C{void _m(){" "}}") + (cons "class _C{void _m(){" ";}}") + (cons "" ""))) + (def (dart-parse-pattern rewritten-pattern) + (let ([trimmed (string-trim rewritten-pattern)]) + (let loop ([scaffolds dart-pattern-scaffolds]) + (if (null? scaffolds) + (values #f #f) + (let* ([prefix (caar scaffolds)] + [suffix (cdar scaffolds)] + [wrapped (string-append prefix trimmed suffix)] + [result (parse-target-string "dart" wrapped)] + [root (parse-result-root result)]) + (if (or (not root) (parse-result-has-errors? result)) + (begin + (when root (node-close! root)) + (tree-close! (parse-result-tree result)) + (loop (cdr scaffolds))) + (let* ([hstart (utf8-length prefix)] + [hend (+ hstart (utf8-length trimmed))] + [hole (and (<= (node-start-byte root) hstart) + (>= (node-end-byte root) hend) + (go-deepest-node-containing + root + hstart + hend))]) + (node-close! root) + (if hole + (values hole result) + (begin + (tree-close! (parse-result-tree result)) + (loop (cdr scaffolds))))))))))) (def (run-structural-matches language normalized @@ -1977,11 +2171,17 @@ language pattern-source)]) (cond - [(or (go-language? language) (php-language? language)) + [(or (go-language? language) + (php-language? language) + (string=? language "dart")) (let-values ([(normalized result) - (if (go-language? language) - (go-parse-pattern rewritten-pattern) - (php-parse-pattern rewritten-pattern))]) + (cond + [(go-language? language) + (go-parse-pattern rewritten-pattern)] + [(php-language? language) + (php-parse-pattern rewritten-pattern)] + [else + (dart-parse-pattern rewritten-pattern)])]) (if (not normalized) '() (let ([matches (run-structural-matches --- a/lib/semgrep/parse/parse-target.sls +++ b/lib/semgrep/parse/parse-target.sls @@ -30,6 +30,7 @@ [(string=? canonical "rust") 'rust] [(string=? canonical "ruby") 'ruby] [(string=? canonical "kotlin") 'kotlin] + [(string=? canonical "dart") 'dart] [else (error 'parse-target-string "unsupported language in current MVP" --- a/lib/semgrep/scan.sls +++ b/lib/semgrep/scan.sls @@ -489,6 +489,22 @@ (not (semicolon-trimmable-node? node))) (extend-finding-over-semicolon rule finding source) finding)) + (def (maybe-trim-dart-statement-semicolon language pattern + source node finding) + (if (and (string=? language "dart") + (not (pattern-trailing-semicolon? pattern)) + (string=? (node-type node) "expression_statement") + (let ([t (node-text node)]) + (and (> (string-length t) 0) + (char=? + (string-ref t (- (string-length t) 1)) + #\;)))) + (finding-with-range + finding + source + (finding-start-offset finding) + (- (finding-end-offset finding) 1)) + finding)) (def (base-finding-extra rule) (if (null? (rule-metadata rule)) '() @@ -746,7 +762,6 @@ (string=? canonical "terraform") (string=? canonical "csharp") (string=? canonical "swift") - (string=? canonical "dart") (string=? canonical "move_on_aptos") (string=? canonical "julia") (string=? canonical "cpp")))) @@ -20312,8 +20327,9 @@ finding0)] [finding (if range-binding finding1 - (maybe-extend-expression-semicolon rule pattern source node - finding1))] + (maybe-trim-dart-statement-semicolon language pattern source node + (maybe-extend-expression-semicolon rule pattern source node + finding1)))] [focused (apply-rule-focus rule finding)]) (node-close! node) (loop (cdr xs) (append (reverse focused) acc)))))))) @@ -31262,6 +31278,12 @@ "$T $L = $R" "this.$L = $R" "for ($T $L : $R) {\n ...\n}")] + [(string=? language "dart") + '("$L = $R" + "$L[$I] = $R" + "var $L = $R" + "final $L = $R" + "$T $L = $R")] [else '("$L = $R" "$L[$I] = $R")])) (def (scan-implicit-assignment-propagators rule language path source target-root) --- a/src/.jerbuild-hashes +++ b/src/.jerbuild-hashes @@ -2,12 +2,12 @@ ("src/semgrep/result.ss" . "22D23E40B49BA529") ("src/semgrep/output/json.ss" . "293881CFA2ADB7BC") ("src/semgrep/lang.ss" . "6982E07679D20836") - ("src/semgrep/parse/parse-target.ss" . "904B71D207617229") - ("src/semgrep/scan.ss" . "A994804D70D4F26E") - ("src/semgrep/fix.ss" . "2E5B65B1FEF3B2B1") - ("src/semgrep/output/text.ss" . "BE476CB84B807FBA") + ("src/semgrep/parse/parse-target.ss" . "97AA8FFEB12736DA") + ("src/semgrep/scan.ss" . "660D5F4ACA2CF97F") ("src/semgrep/rule.ss" . "E12C108153C181FA") ("src/semgrep/schema/lang.ss" . "CAE2CA859C9A9FD0") - ("src/semgrep/match/structural.ss" . "6047821D83B32FCE") + ("src/semgrep/output/text.ss" . "BE476CB84B807FBA") + ("src/semgrep/fix.ss" . "2E5B65B1FEF3B2B1") + ("src/semgrep/match/structural.ss" . "5BA4F1566448AF3A") ("src/semgrep/main.ss" . "A4EC9E7F2A09D25E") ("src/semgrep/cli.ss" . "EBDC4B1DAD3F13CC")) --- a/src/semgrep/match/structural.ss +++ b/src/semgrep/match/structural.ss @@ -898,6 +898,19 @@ end-col bindings))))) +(def (bind-sequence-range target start-index end-index bindings) + (let-values ([(start-byte end-byte start-line start-col end-line end-col) + (sequence-range target start-index end-index)]) + (bind-metavariable-value internal-match-range-name + "" + start-byte + end-byte + start-line + start-col + end-line + end-col + bindings))) + (def (pattern-container-range-eligible? pattern) (let ([count (node-matchable-child-count pattern)]) (and (> count 0) @@ -932,6 +945,131 @@ acc))))) matches))) +(def (match-child-prefix-with-end language pattern target pcount tcount start bindings) + (let loop ([pi 0] [ti start] [current-bindings bindings]) + (cond + [(= pi pcount) (list (cons ti current-bindings))] + [(>= ti tcount) '()] + [else + (let ([pchild (node-matchable-child pattern pi)] + [tchild (node-matchable-child target ti)]) + (let ([next-bindings + (if (and pchild tchild) + (structural-node-matches language pchild tchild current-bindings) + '())]) + (when pchild (node-close! pchild)) + (when tchild (node-close! tchild)) + (sg-append-map + (lambda (next) + (loop (+ pi 1) (+ ti 1) next)) + next-bindings)))]))) + +(def (dart-expression-statement-fragment-matches language pattern target bindings) + (and (string=? language "dart") + (string=? (node-type pattern) "expression_statement") + (not (string=? (node-type target) "expression_statement")) + (let ([pcount (node-matchable-child-count pattern)] + [tcount (node-matchable-child-count target)]) + (and (> pcount 0) + (let start-loop ([start 0] [acc '()]) + (if (>= start tcount) + (reverse acc) + (let ([matches + (match-child-prefix-with-end + language pattern target pcount tcount start bindings)]) + (start-loop + (+ start 1) + (let result-loop ([xs matches] [a acc]) + (if (null? xs) + a + (let* ([end-index (caar xs)] + [next-bindings (cdar xs)] + [with-range + (bind-sequence-range + target + start + end-index + next-bindings)]) + (result-loop + (cdr xs) + (if with-range + (cons with-range a) + a))))))))))))) + +(def (dart-bind-metavariable-sequence name target start-index end-index bindings) + (bind-ellipsis-metavariable name target start-index end-index bindings)) + +(def (dart-child-sequence-rhs-metavar-matches + language + pattern + target + left-count + bindings) + (and (string=? language "dart") + (let ([pcount (node-matchable-child-count pattern)] + [tcount (node-matchable-child-count target)]) + (and (= pcount (+ left-count 1)) + (> tcount left-count) + (let ([rhs (node-matchable-child pattern left-count)]) + (and rhs + (let ([name (metavariable-name-from-text (node-text rhs))]) + (node-close! rhs) + (and name + (let left-loop ([i 0] [states (list bindings)]) + (if (= i left-count) + (let bind-loop ([xs states] [acc '()]) + (if (null? xs) + (reverse acc) + (let ([next + (dart-bind-metavariable-sequence + name + target + left-count + tcount + (car xs))]) + (bind-loop + (cdr xs) + (if next (cons next acc) acc))))) + (let ([pchild (node-matchable-child pattern i)] + [tchild (node-matchable-child target i)]) + (let ([next-states + (if (and pchild tchild) + (sg-append-map + (lambda (state) + (structural-node-matches + language + pchild + tchild + state)) + states) + '())]) + (when pchild (node-close! pchild)) + (when tchild (node-close! tchild)) + (and (not (null? next-states)) + (left-loop + (+ i 1) + next-states)))))))))))))) + +(def (dart-assignment-expression-matches language pattern target bindings) + (and (string=? (node-type pattern) "assignment_expression") + (string=? (node-type target) "assignment_expression") + (dart-child-sequence-rhs-metavar-matches + language + pattern + target + 1 + bindings))) + +(def (dart-initialized-variable-definition-matches language pattern target bindings) + (and (string=? (node-type pattern) "initialized_variable_definition") + (string=? (node-type target) "initialized_variable_definition") + (dart-child-sequence-rhs-metavar-matches + language + pattern + target + 2 + bindings))) + (def (make-chain-segment name arguments) (cons name arguments)) @@ -1323,6 +1461,16 @@ => (lambda (matches) matches)] [(java-param-metavar-matches language pattern target bindings) => (lambda (matches) matches)] + [(dart-expression-statement-fragment-matches language pattern target bindings) + => (lambda (matches) matches)] + [(dart-assignment-expression-matches language pattern target bindings) + => (lambda (matches) matches)] + [(dart-initialized-variable-definition-matches + language + pattern + target + bindings) + => (lambda (matches) matches)] ;; PHP metavariable `$NAME` (uppercase) -> bind to the target node. [(and (php-language? language) (php-metavariable-name-from-node pattern)) @@ -1741,6 +1889,41 @@ (tree-close! (parse-result-tree result)) (loop (cdr scaffolds))))))))))) +;; Dart bare patterns parse as top-level declarations (`sink($X)` becomes a +;; function_signature), so we wrap them in a class method body and extract the +;; spanned node by byte range, like the PHP/Go scaffolds. +(define dart-pattern-scaffolds + (list (cons "class _C{void _m(){" "}}") ; statements + (cons "class _C{void _m(){" ";}}") ; bare expressions (need a `;`) + (cons "" ""))) ; top-level declarations + +(def (dart-parse-pattern rewritten-pattern) + (let ([trimmed (string-trim rewritten-pattern)]) + (let loop ([scaffolds dart-pattern-scaffolds]) + (if (null? scaffolds) + (values #f #f) + (let* ([prefix (caar scaffolds)] + [suffix (cdar scaffolds)] + [wrapped (string-append prefix trimmed suffix)] + [result (parse-target-string "dart" wrapped)] + [root (parse-result-root result)]) + (if (or (not root) (parse-result-has-errors? result)) + (begin + (when root (node-close! root)) + (tree-close! (parse-result-tree result)) + (loop (cdr scaffolds))) + (let* ([hstart (utf8-length prefix)] + [hend (+ hstart (utf8-length trimmed))] + [hole (and (<= (node-start-byte root) hstart) + (>= (node-end-byte root) hend) + (go-deepest-node-containing root hstart hend))]) + (node-close! root) + (if hole + (values hole result) + (begin + (tree-close! (parse-result-tree result)) + (loop (cdr scaffolds))))))))))) + (def (run-structural-matches language normalized target-root initial-bindings) (let* ([chain-ellipsis? (chain-node-has-ellipsis? normalized)] [raw-matches @@ -1949,11 +2132,14 @@ initial-bindings) (let ([rewritten-pattern (rewrite-metavariables language pattern-source)]) (cond - [(or (go-language? language) (php-language? language)) + [(or (go-language? language) + (php-language? language) + (string=? language "dart")) (let-values ([(normalized result) - (if (go-language? language) - (go-parse-pattern rewritten-pattern) - (php-parse-pattern rewritten-pattern))]) + (cond + [(go-language? language) (go-parse-pattern rewritten-pattern)] + [(php-language? language) (php-parse-pattern rewritten-pattern)] + [else (dart-parse-pattern rewritten-pattern)])]) (if (not normalized) '() (let ([matches (run-structural-matches --- a/src/semgrep/parse/parse-target.ss +++ b/src/semgrep/parse/parse-target.ss @@ -22,6 +22,7 @@ [(string=? canonical "rust") 'rust] [(string=? canonical "ruby") 'ruby] [(string=? canonical "kotlin") 'kotlin] + [(string=? canonical "dart") 'dart] [else (error 'parse-target-string "unsupported language in current MVP" language)]))) --- a/src/semgrep/scan.ss +++ b/src/semgrep/scan.ss @@ -555,6 +555,24 @@ (extend-finding-over-semicolon rule finding source) finding)) +;; Dart has no wrapping expression node for a call (`print(x)` is an identifier +;; plus a selector under expression_statement), so an expression pattern like +;; `print($X)` extracts/matches the whole statement, including its `;`. When the +;; pattern itself has no trailing `;`, trim that statement terminator so the +;; finding range matches Semgrep's expression span. +(def (maybe-trim-dart-statement-semicolon language pattern source node finding) + (if (and (string=? language "dart") + (not (pattern-trailing-semicolon? pattern)) + (string=? (node-type node) "expression_statement") + (let ([t (node-text node)]) + (and (> (string-length t) 0) + (char=? (string-ref t (- (string-length t) 1)) #\;)))) + (finding-with-range finding + source + (finding-start-offset finding) + (- (finding-end-offset finding) 1)) + finding)) + (def (base-finding-extra rule) (if (null? (rule-metadata rule)) '() @@ -818,7 +836,6 @@ (string=? canonical "terraform") (string=? canonical "csharp") (string=? canonical "swift") - (string=? canonical "dart") (string=? canonical "move_on_aptos") (string=? canonical "julia") (string=? canonical "cpp")))) @@ -20158,12 +20175,17 @@ finding0)] [finding (if range-binding finding1 - (maybe-extend-expression-semicolon - rule + (maybe-trim-dart-statement-semicolon + language pattern source node - finding1))] + (maybe-extend-expression-semicolon + rule + pattern + source + node + finding1)))] [focused (apply-rule-focus rule finding)]) (node-close! node) (loop (cdr xs) (append (reverse focused) acc)))))))) @@ -31271,6 +31293,13 @@ "$T $L = $R" "this.$L = $R" "for ($T $L : $R) {\n ...\n}")] + [(string=? language "dart") + ;; Dart bindings: var/final/typed local declarations. + '("$L = $R" + "$L[$I] = $R" + "var $L = $R" + "final $L = $R" + "$T $L = $R")] [else '("$L = $R" "$L[$I] = $R")]))