Merge dart structural migration

ober

30c2df837dca626126526cc83e0cb7ea650def4c

diff --git a/HANDOFF_OPUS_4_8.md b/HANDOFF_OPUS_4_8.md
index 98839fb..97d34c7 100644
--- 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;
diff --git a/lib/semgrep/match/structural.sls b/lib/semgrep/match/structural.sls
index ac6b3a0..9ad400d 100644
--- 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
diff --git a/lib/semgrep/parse/parse-target.sls b/lib/semgrep/parse/parse-target.sls
index a81ed5a..8bc5bc8 100644
--- 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"
diff --git a/lib/semgrep/scan.sls b/lib/semgrep/scan.sls
index ba65723..d123c51 100644
--- 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)
diff --git a/src/.jerbuild-hashes b/src/.jerbuild-hashes
index 6d5a28d..d83e072 100644
--- 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"))
diff --git a/src/semgrep/match/structural.ss b/src/semgrep/match/structural.ss
index 9a84678..e901cf5 100644
--- 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
diff --git a/src/semgrep/parse/parse-target.ss b/src/semgrep/parse/parse-target.ss
index 1872685..2940b5e 100644
--- 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)])))
 
diff --git a/src/semgrep/scan.ss b/src/semgrep/scan.ss
index 142338f..36068a7 100644
--- 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")]))