Filter dead Dart catch taint paths

ober

06738f3c10d844b7e8a93af6601d77fe05d8ed3b

diff --git a/HANDOFF_OPUS_4_8.md b/HANDOFF_OPUS_4_8.md
index 05d0cfd..87a1c58 100644
--- a/HANDOFF_OPUS_4_8.md
+++ b/HANDOFF_OPUS_4_8.md
@@ -1,26 +1,27 @@
 # Opus 4.8 Handoff: jerboa-semgrep Semgrep Parity
 
-Date: 2026-05-30 11:50 MDT
+Date: 2026-05-30 12:21 MDT
 Workspace: `/Users/user/mine/jerboa-semgrep`
 Sibling upstream Semgrep checkout: `/Users/user/mine/semgrep`
 Packaged Semgrep oracle: `/Users/user/.local/bin/semgrep`
 Branch: `main`
 Base HEAD before this checkpoint:
-`482f971 Honor Django safe URL taint guards`
+`633e0f1 Respect Ruby case branch taint paths`
 
 The user wants the pure Jerboa Semgrep port carried forward until it reaches
 Semgrep parity. Do not treat this handoff as completion. The latest checkpoint
-closes the current upstream Ruby tainting-rule mismatch, `switch`, by making
-sanitizer blocking and assignment kills respect sibling Ruby `case` branches.
+closes upstream Dart tainting-rule mismatch `try_return` by filtering
+impossible Dart `catch` sources after non-throwing `try` returns and by adding
+simple brace-style function scope detection for taint reachability.
 
 ## Immediate State
 
-The worktree was clean at `482f971` before this checkpoint. The latest
+The worktree was clean at `633e0f1` before this checkpoint. The latest
 implementation change is in
 [src/semgrep/scan.ss](/Users/user/mine/jerboa-semgrep/src/semgrep/scan.ss);
 [lib/semgrep/scan.sls](/Users/user/mine/jerboa-semgrep/lib/semgrep/scan.sls)
 and [src/.jerbuild-hashes](/Users/user/mine/jerboa-semgrep/src/.jerbuild-hashes)
-were regenerated by `make test`. Focused Ruby `case` branch taint smoke
+were regenerated by `make test`. Focused Dart `try`/`catch` taint smoke
 coverage was added in [tests/smoke.ss](/Users/user/mine/jerboa-semgrep/tests/smoke.ss).
 
 Current headline:
@@ -30,7 +31,7 @@ Current headline:
 - Promoted JavaScript pattern oracle: 91 passed / 0 mismatched.
 - Promoted Python pattern oracle: 116 passed / 0 mismatched.
 - Local oracle: 42 passed / 0 failed.
-- Smoke suite: 312 tests / 312 passed.
+- Smoke suite: 313 tests / 313 passed.
 - Broad same-basename upstream `tests/rules` sweep: 437 passed /
   0 mismatched / 0 Jerboa errors, with 3 packaged-Semgrep current errors.
 - Upstream `tests/tainting_rules/js` sweep: 11 passed / 0 mismatched /
@@ -39,11 +40,14 @@ Current headline:
   0 Jerboa errors / 0 current errors.
 - Upstream `tests/tainting_rules/ruby` sweep: 1 passed / 0 mismatched /
   0 Jerboa errors / 0 current errors.
+- Upstream `tests/tainting_rules/dart` sweep: 1 passed / 1 mismatched /
+  0 Jerboa errors / 0 current errors. Remaining mismatch: `arrays_if`.
 
 Semgrep parity is not reached. The JavaScript, Python, and Ruby
 `tests/tainting_rules` subdirectories are clean against the packaged oracle.
 The active frontier is now semantic taint-control-flow parity in the remaining
-upstream `tests/tainting_rules` subdirectories, especially Dart, Go, and PHP.
+upstream `tests/tainting_rules` subdirectories, especially Dart `arrays_if`,
+then Go and PHP.
 
 ## Repository Context
 
@@ -182,6 +186,18 @@ This checkpoint:
 
 Latest checkpoint:
 
+- Adds Dart impossible-catch filtering for taint findings inside `catch`
+  blocks whose matching `try` body returns a simple non-throwing expression.
+- Adds conservative brace-style function scope detection so Dart/C-like
+  functions do not leak taint across sibling functions or methods.
+- Closes upstream Dart tainting-rule case `try_return`.
+- Moves the Dart tainting-rule sweep from `0 passed / 2 mismatched /
+  0 Jerboa errors` to `1 passed / 1 mismatched / 0 Jerboa errors`.
+- Adds smoke coverage:
+  `scan Dart taint ignores catch after nonthrowing return`.
+
+Previous checkpoint:
+
 - Adds Ruby `case`/`when`/`else` sibling-branch detection for taint
   reachability.
 - Prevents a sanitizer or clean assignment in one Ruby `when` branch from
@@ -244,6 +260,104 @@ tests/smoke.ss
 
 ## Latest Code Change Details
 
+### Dart Try-Return Catch Reachability
+
+The upstream Dart taint case `try_return` is translated from the Java
+`try_return` fixture. The important contrast is:
+
+```dart
+String? test1() {
+  String str = "safe";
+  try {
+    return couldThrow();
+  } catch (e) {
+    str = source();
+  }
+  sink(str);      // report: couldThrow() may throw
+}
+
+int? test2() {
+  int cannotThrow = 42;
+  String str = "safe";
+  try {
+    return cannotThrow;
+  }
+  catch (e) {
+    str = source();
+  }
+  sink(str);      // no report: catch is dead
+}
+```
+
+Jerboa previously reported both sinks. Two issues were involved:
+
+- The `catch` body after `return cannotThrow;` was treated as reachable, so the
+  `source()` assignment could taint `str`.
+- Dart methods were not recognized by `same-simple-function-scope?`, so the
+  reachable source in `test1` could also flow into the sink in `test2`.
+
+The patch adds these helpers in
+[src/semgrep/scan.ss](/Users/user/mine/jerboa-semgrep/src/semgrep/scan.ss):
+
+- `dart-significant-trimmed-line`
+- `dart-control-line-normalized`
+- `dart-try-line?`
+- `dart-catch-line?`
+- `dart-parent-catch-clause`
+- `dart-matching-try-line`
+- `dart-return-expression`
+- `dart-simple-nonthrowing-expression?`
+- `dart-try-body-outcome`
+- `dart-finding-in-impossible-catch-branch?`
+- `filter-dart-taint-reachable-findings`
+
+`filter-taint-reachable-findings` now composes the Dart filter with the
+existing Python and JavaScript reachability filters. The Dart filter is
+conservative: it only treats returns of identifiers, `null`, booleans, quoted
+strings, and numeric literals as non-throwing. It intentionally leaves
+`return couldThrow();` as unknown/reachable.
+
+The patch also adds:
+
+- `simple-c-like-function-line?`
+
+and includes it in `simple-function-scope-line?`. This recognizes
+brace-style function declarations such as:
+
+```dart
+String? test1() {
+void F(Object? x) {
+```
+
+and also helps other C-like languages avoid cross-function taint leakage. It
+explicitly excludes common control-flow headers such as `if`, `for`, `while`,
+`switch`, and `catch`.
+
+Smoke coverage added:
+
+```scheme
+(test-case "scan Dart taint ignores catch after nonthrowing return"
+  ...)
+```
+
+Focused upstream verification:
+
+```text
+upstream-sweep: 1 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 1 compared
+```
+
+Full Dart taint verification:
+
+```text
+upstream-sweep: 1 passed, 1 mismatched, 0 jerboa errors, 0 current errors, 2 compared
+```
+
+Remaining Dart mismatch:
+
+```text
+arrays_if
+```
+
 ### Ruby Case Branch Taint Reachability
 
 The upstream Ruby taint case `switch` is:
@@ -929,7 +1043,22 @@ make test
 Result:
 
 ```text
-312 tests, 312 passed, 0 failed
+313 tests, 313 passed, 0 failed
+```
+
+Focused upstream Dart taint `try_return`:
+
+```sh
+SEMGREP_CURRENT=/Users/user/.local/bin/semgrep \
+UPSTREAM_RULE_DIR=/Users/user/mine/semgrep/tests/tainting_rules/dart \
+CASE_REGEX='^(try_return)$' LIST_MISMATCHES=1 MAX_DIFFS=300 \
+tests/oracle/upstream-sweep.sh
+```
+
+Result:
+
+```text
+upstream-sweep: 1 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 1 compared
 ```
 
 Focused upstream Ruby taint `switch`:
@@ -1184,6 +1313,20 @@ Result:
 upstream-sweep: 1 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 1 compared
 ```
 
+Full Dart tainting-rule subdirectory:
+
+```sh
+SEMGREP_CURRENT=/Users/user/.local/bin/semgrep \
+UPSTREAM_RULE_DIR=/Users/user/mine/semgrep/tests/tainting_rules/dart \
+LIST_MISMATCHES=1 MAX_DIFFS=300 tests/oracle/upstream-sweep.sh
+```
+
+Result:
+
+```text
+upstream-sweep: 1 passed, 1 mismatched, 0 jerboa errors, 0 current errors, 2 compared
+```
+
 ## Cross-Language Tainting-Rule Frontier
 
 The upstream `tests/tainting_rules/js`, `tests/tainting_rules/python`, and
@@ -1193,7 +1336,7 @@ Additional subdirectory sweeps found:
 - `java`: 2 passed / 0 mismatched / 0 Jerboa errors.
 - `scala`: 1 passed / 0 mismatched / 0 Jerboa errors.
 - `ts`: 1 passed / 0 mismatched / 0 Jerboa errors.
-- `dart`: 0 passed / 2 mismatched: `arrays_if`, `try_return`.
+- `dart`: 1 passed / 1 mismatched: `arrays_if`.
 - `go`: 4 passed / 5 mismatched: `command-injection`, `continue`,
   `goto_dead_code`, `make`, `zip-traversal`.
 - `php`: 2 passed / 4 mismatched: `break`, `echo`, `lval_var_sink`,
@@ -1201,11 +1344,10 @@ Additional subdirectory sweeps found:
 - `python`: 12 passed / 0 mismatched / 0 Jerboa errors.
 - `ruby`: 1 passed / 0 mismatched / 0 Jerboa errors.
 
-The next compact targets are probably Dart `try_return` or `arrays_if`. Dart
-has only two current mismatches, so it is the smallest dirty subdirectory after
-Ruby. If those turn out to require broad Dart syntax fallback work, move to the
-PHP `break`/`echo` control-flow pair or the Go `continue`/`goto_dead_code`
-cluster.
+The next compact target is Dart `arrays_if`, the only remaining Dart
+tainting-rule mismatch. If it requires broader Dart source-pattern fallback
+work than is worth doing immediately, move to the PHP `break`/`echo`
+control-flow pair or the Go `continue`/`goto_dead_code` cluster.
 
 ## Completed Target: `sanitized_by_side_effect`
 
@@ -1676,22 +1818,13 @@ Actual improvement:
 ## Recommended Next Target
 
 The next compact target is outside JavaScript, Python, and Ruby taint. A
-practical choice is Dart `try_return`, one of only two current Dart
-tainting-rule mismatches:
-
-```sh
-SEMGREP_CURRENT=/Users/user/.local/bin/semgrep \
-UPSTREAM_RULE_DIR=/Users/user/mine/semgrep/tests/tainting_rules/dart \
-CASE_REGEX='^(try_return)$' LIST_MISMATCHES=1 MAX_DIFFS=260 \
-tests/oracle/upstream-sweep.sh
-```
-
-The paired Dart mismatch is `arrays_if`:
+practical choice is Dart `arrays_if`, the only remaining current Dart
+tainting-rule mismatch:
 
 ```sh
 SEMGREP_CURRENT=/Users/user/.local/bin/semgrep \
 UPSTREAM_RULE_DIR=/Users/user/mine/semgrep/tests/tainting_rules/dart \
-CASE_REGEX='^(arrays_if)$' LIST_MISMATCHES=1 MAX_DIFFS=260 \
+CASE_REGEX='^(arrays_if)$' LIST_MISMATCHES=1 MAX_DIFFS=300 \
 tests/oracle/upstream-sweep.sh
 ```
 
diff --git a/lib/semgrep/scan.sls b/lib/semgrep/scan.sls
index ff9761a..67388cf 100644
--- a/lib/semgrep/scan.sls
+++ b/lib/semgrep/scan.sls
@@ -30628,6 +30628,138 @@
                           finding))))
              findings)
            findings))
+  (def (dart-significant-trimmed-line source line-start)
+       (let* ([line-end (line-end-after source line-start)]
+              [trimmed (string-trim
+                         (substring source line-start line-end))])
+         (and (> (string-length trimmed) 0)
+              (not (sg-string-prefix? "//" trimmed))
+              trimmed)))
+  (def (dart-control-line-normalized trimmed)
+       (javascript-strip-leading-close-braces trimmed))
+  (def (dart-try-line? trimmed)
+       (python-line-starts-keyword?
+         (dart-control-line-normalized trimmed)
+         "try"))
+  (def (dart-catch-line? trimmed)
+       (let ([normalized (dart-control-line-normalized trimmed)])
+         (or (python-line-starts-keyword? normalized "catch")
+             (and (python-line-starts-keyword? normalized "on")
+                  (string-find-substring normalized " catch")))))
+  (def (dart-parent-catch-clause source offset)
+       (let* ([line-start (line-start-before source offset)]
+              [target-indent (line-indent-at-offset source offset)])
+         (let loop ([current-start line-start]
+                    [current-indent target-indent])
+           (let ([prev-start (python-previous-line-start
+                               source
+                               current-start)])
+             (if (not prev-start)
+                 #f
+                 (let ([trimmed (dart-significant-trimmed-line
+                                  source
+                                  prev-start)])
+                   (if (not trimmed)
+                       (loop prev-start current-indent)
+                       (let ([indent (line-indent-at-offset
+                                       source
+                                       prev-start)])
+                         (if (>= indent current-indent)
+                             (loop prev-start current-indent)
+                             (if (dart-catch-line? trimmed)
+                                 (list
+                                   (cons 'indent indent)
+                                   (cons 'start prev-start))
+                                 (loop prev-start indent)))))))))))
+  (def (dart-matching-try-line source catch-clause)
+       (let ([clause-start (alist-ref/default
+                             catch-clause
+                             'start
+                             #f)]
+             [clause-indent (alist-ref/default catch-clause 'indent 0)])
+         (and clause-start
+              (let loop ([current-start clause-start])
+                (let ([prev-start (python-previous-line-start
+                                    source
+                                    current-start)])
+                  (if (not prev-start)
+                      #f
+                      (let ([trimmed (dart-significant-trimmed-line
+                                       source
+                                       prev-start)])
+                        (if (not trimmed)
+                            (loop prev-start)
+                            (let ([indent (line-indent-at-offset
+                                            source
+                                            prev-start)])
+                              (cond
+                                [(< indent clause-indent) #f]
+                                [(> indent clause-indent)
+                                 (loop prev-start)]
+                                [(dart-try-line? trimmed) prev-start]
+                                [else (loop prev-start)]))))))))))
+  (def (dart-return-expression trimmed)
+       (and (python-line-starts-keyword? trimmed "return")
+            (let* ([len (string-length trimmed)]
+                   [raw (string-trim (substring trimmed 6 len))]
+                   [expr (if (sg-string-suffix? ";" raw)
+                             (string-trim
+                               (substring raw 0 (- (string-length raw) 1)))
+                             raw)])
+              expr)))
+  (def (dart-simple-nonthrowing-expression? text)
+       (let ([trimmed (string-trim text)])
+         (or (identifier-only-text? trimmed)
+             (string=? trimmed "null")
+             (string=? trimmed "true")
+             (string=? trimmed "false")
+             (quoted-string? trimmed)
+             (parse-number-literal trimmed #f))))
+  (def (dart-try-body-outcome source try-start)
+       (let* ([try-indent (line-indent-at-offset source try-start)]
+              [first-body-line (python-next-line-start source try-start)])
+         (let loop ([current-start first-body-line])
+           (if (not current-start)
+               'unknown
+               (let ([trimmed (dart-significant-trimmed-line
+                                source
+                                current-start)])
+                 (if (not trimmed)
+                     (loop (python-next-line-start source current-start))
+                     (let ([indent (line-indent-at-offset
+                                     source
+                                     current-start)])
+                       (cond
+                         [(<= indent try-indent) 'unknown]
+                         [(dart-return-expression trimmed) =>
+                          (lambda (expr)
+                            (if (dart-simple-nonthrowing-expression? expr)
+                                'no-throw-return
+                                'unknown))]
+                         [else 'unknown]))))))))
+  (def (dart-finding-in-impossible-catch-branch?
+         source
+         finding)
+       (let* ([clause (dart-parent-catch-clause
+                        source
+                        (finding-start-offset finding))]
+              [try-start (and clause
+                              (dart-matching-try-line source clause))]
+              [outcome (and try-start
+                            (dart-try-body-outcome source try-start))])
+         (eq? outcome 'no-throw-return)))
+  (def (filter-dart-taint-reachable-findings
+         language
+         source
+         findings)
+       (if (dart-language? language)
+           (sg-filter
+             (lambda (finding)
+               (not (dart-finding-in-impossible-catch-branch?
+                      source
+                      finding)))
+             findings)
+           findings))
   (def (javascript-taint-line-opens-unreachable?
          source
          line-start
@@ -30681,13 +30813,16 @@
          language
          source
          findings)
-       (filter-javascript-taint-reachable-findings
+       (filter-dart-taint-reachable-findings
          language
          source
-         (filter-python-taint-reachable-findings
+         (filter-javascript-taint-reachable-findings
            language
            source
-           findings)))
+           (filter-python-taint-reachable-findings
+             language
+             source
+             findings))))
   (def (scan-taint-specs rule specs language path source
          target-root default-label)
        (apply
@@ -31091,9 +31226,27 @@
   (def (simple-top-level-js-function-line? line)
        (and (= (line-indent line) 0)
             (sg-string-prefix? "function " (string-trim line))))
+  (def (simple-c-like-function-line? line)
+       (let ([trimmed (string-trim line)])
+         (and (sg-string-suffix? "{" trimmed)
+              (not (sg-string-prefix? "if " trimmed))
+              (not (sg-string-prefix? "if(" trimmed))
+              (not (sg-string-prefix? "for " trimmed))
+              (not (sg-string-prefix? "for(" trimmed))
+              (not (sg-string-prefix? "while " trimmed))
+              (not (sg-string-prefix? "while(" trimmed))
+              (not (sg-string-prefix? "switch " trimmed))
+              (not (sg-string-prefix? "switch(" trimmed))
+              (not (sg-string-prefix? "catch " trimmed))
+              (not (sg-string-prefix? "catch(" trimmed))
+              (re-search
+                (re "^[A-Za-z_$][A-Za-z0-9_$<>?,\\[\\] ]*[ \\t]+[A-Za-z_$][A-Za-z0-9_$]*[ \\t]*\\(")
+                trimmed
+                0))))
   (def (simple-function-scope-line? line)
        (or (simple-python-def-line? line)
-           (simple-top-level-js-function-line? line)))
+           (simple-top-level-js-function-line? line)
+           (simple-c-like-function-line? line)))
   (def (finding-simple-function-scope source finding)
        (let* ([line (finding-start-line finding)]
               [target-line (source-line source line)]
diff --git a/src/.jerbuild-hashes b/src/.jerbuild-hashes
index 5487d1d..795e575 100644
--- a/src/.jerbuild-hashes
+++ b/src/.jerbuild-hashes
@@ -3,11 +3,11 @@
   ("src/semgrep/output/json.ss" . "293881CFA2ADB7BC")
   ("src/semgrep/lang.ss" . "6982E07679D20836")
   ("src/semgrep/parse/parse-target.ss" . "E74854DDDACF6BA")
-  ("src/semgrep/scan.ss" . "523EDB0189EB42F5")
-  ("src/semgrep/schema/lang.ss" . "CAE2CA859C9A9FD0")
-  ("src/semgrep/rule.ss" . "E12C108153C181FA")
+  ("src/semgrep/scan.ss" . "C5787423DCB2D8C7")
   ("src/semgrep/fix.ss" . "2E5B65B1FEF3B2B1")
   ("src/semgrep/output/text.ss" . "BE476CB84B807FBA")
+  ("src/semgrep/rule.ss" . "E12C108153C181FA")
+  ("src/semgrep/schema/lang.ss" . "CAE2CA859C9A9FD0")
   ("src/semgrep/match/structural.ss" . "6FE77014EE9FDCE4")
   ("src/semgrep/main.ss" . "A4EC9E7F2A09D25E")
   ("src/semgrep/cli.ss" . "EBDC4B1DAD3F13CC"))
diff --git a/src/semgrep/scan.ss b/src/semgrep/scan.ss
index b1d9d76..83083f3 100644
--- a/src/semgrep/scan.ss
+++ b/src/semgrep/scan.ss
@@ -30606,6 +30606,122 @@
         findings)
       findings))
 
+(def (dart-significant-trimmed-line source line-start)
+  (let* ([line-end (line-end-after source line-start)]
+         [trimmed (string-trim (substring source line-start line-end))])
+    (and (> (string-length trimmed) 0)
+         (not (sg-string-prefix? "//" trimmed))
+         trimmed)))
+
+(def (dart-control-line-normalized trimmed)
+  (javascript-strip-leading-close-braces trimmed))
+
+(def (dart-try-line? trimmed)
+  (python-line-starts-keyword?
+    (dart-control-line-normalized trimmed)
+    "try"))
+
+(def (dart-catch-line? trimmed)
+  (let ([normalized (dart-control-line-normalized trimmed)])
+    (or (python-line-starts-keyword? normalized "catch")
+        (and (python-line-starts-keyword? normalized "on")
+             (string-find-substring normalized " catch")))))
+
+(def (dart-parent-catch-clause source offset)
+  (let* ([line-start (line-start-before source offset)]
+         [target-indent (line-indent-at-offset source offset)])
+    (let loop ([current-start line-start] [current-indent target-indent])
+      (let ([prev-start (python-previous-line-start source current-start)])
+        (if (not prev-start)
+            #f
+            (let ([trimmed (dart-significant-trimmed-line source prev-start)])
+              (if (not trimmed)
+                  (loop prev-start current-indent)
+                  (let ([indent (line-indent-at-offset source prev-start)])
+                    (if (>= indent current-indent)
+                        (loop prev-start current-indent)
+                        (if (dart-catch-line? trimmed)
+                            (list (cons 'indent indent)
+                                  (cons 'start prev-start))
+                            (loop prev-start indent)))))))))))
+
+(def (dart-matching-try-line source catch-clause)
+  (let ([clause-start (alist-ref/default catch-clause 'start #f)]
+        [clause-indent (alist-ref/default catch-clause 'indent 0)])
+    (and clause-start
+         (let loop ([current-start clause-start])
+           (let ([prev-start (python-previous-line-start source current-start)])
+             (if (not prev-start)
+                 #f
+                 (let ([trimmed
+                        (dart-significant-trimmed-line source prev-start)])
+                   (if (not trimmed)
+                       (loop prev-start)
+                       (let ([indent (line-indent-at-offset
+                                       source
+                                       prev-start)])
+                         (cond
+                           [(< indent clause-indent) #f]
+                           [(> indent clause-indent) (loop prev-start)]
+                           [(dart-try-line? trimmed) prev-start]
+                           [else (loop prev-start)]))))))))))
+
+(def (dart-return-expression trimmed)
+  (and (python-line-starts-keyword? trimmed "return")
+       (let* ([len (string-length trimmed)]
+              [raw (string-trim (substring trimmed 6 len))]
+              [expr (if (sg-string-suffix? ";" raw)
+                        (string-trim
+                          (substring raw 0 (- (string-length raw) 1)))
+                        raw)])
+         expr)))
+
+(def (dart-simple-nonthrowing-expression? text)
+  (let ([trimmed (string-trim text)])
+    (or (identifier-only-text? trimmed)
+        (string=? trimmed "null")
+        (string=? trimmed "true")
+        (string=? trimmed "false")
+        (quoted-string? trimmed)
+        (parse-number-literal trimmed #f))))
+
+(def (dart-try-body-outcome source try-start)
+  (let* ([try-indent (line-indent-at-offset source try-start)]
+         [first-body-line (python-next-line-start source try-start)])
+    (let loop ([current-start first-body-line])
+      (if (not current-start)
+          'unknown
+          (let ([trimmed (dart-significant-trimmed-line source current-start)])
+            (if (not trimmed)
+                (loop (python-next-line-start source current-start))
+                (let ([indent (line-indent-at-offset source current-start)])
+                  (cond
+                    [(<= indent try-indent) 'unknown]
+                    [(dart-return-expression trimmed)
+                     => (lambda (expr)
+                          (if (dart-simple-nonthrowing-expression? expr)
+                              'no-throw-return
+                              'unknown))]
+                    [else 'unknown]))))))))
+
+(def (dart-finding-in-impossible-catch-branch? source finding)
+  (let* ([clause (dart-parent-catch-clause
+                   source
+                   (finding-start-offset finding))]
+         [try-start (and clause (dart-matching-try-line source clause))]
+         [outcome (and try-start (dart-try-body-outcome source try-start))])
+    (eq? outcome 'no-throw-return)))
+
+(def (filter-dart-taint-reachable-findings language source findings)
+  (if (dart-language? language)
+      (sg-filter
+        (lambda (finding)
+          (not (dart-finding-in-impossible-catch-branch?
+                 source
+                 finding)))
+        findings)
+      findings))
+
 (def (javascript-taint-line-opens-unreachable? source line-start line-end)
   (let* ([first (line-first-nonspace source line-start line-end)]
          [line (js-cp-trim-comment (substring source first line-end))]
@@ -30651,10 +30767,13 @@
       findings))
 
 (def (filter-taint-reachable-findings language source findings)
-  (filter-javascript-taint-reachable-findings
+  (filter-dart-taint-reachable-findings
     language
     source
-    (filter-python-taint-reachable-findings language source findings)))
+    (filter-javascript-taint-reachable-findings
+      language
+      source
+      (filter-python-taint-reachable-findings language source findings))))
 
 (def (scan-taint-specs rule specs language path source target-root default-label)
   (apply append
@@ -31087,9 +31206,28 @@
   (and (= (line-indent line) 0)
        (sg-string-prefix? "function " (string-trim line))))
 
+(def (simple-c-like-function-line? line)
+  (let ([trimmed (string-trim line)])
+    (and (sg-string-suffix? "{" trimmed)
+         (not (sg-string-prefix? "if " trimmed))
+         (not (sg-string-prefix? "if(" trimmed))
+         (not (sg-string-prefix? "for " trimmed))
+         (not (sg-string-prefix? "for(" trimmed))
+         (not (sg-string-prefix? "while " trimmed))
+         (not (sg-string-prefix? "while(" trimmed))
+         (not (sg-string-prefix? "switch " trimmed))
+         (not (sg-string-prefix? "switch(" trimmed))
+         (not (sg-string-prefix? "catch " trimmed))
+         (not (sg-string-prefix? "catch(" trimmed))
+         (re-search
+           (re "^[A-Za-z_$][A-Za-z0-9_$<>?,\\[\\] ]*[ \\t]+[A-Za-z_$][A-Za-z0-9_$]*[ \\t]*\\(")
+           trimmed
+           0))))
+
 (def (simple-function-scope-line? line)
   (or (simple-python-def-line? line)
-      (simple-top-level-js-function-line? line)))
+      (simple-top-level-js-function-line? line)
+      (simple-c-like-function-line? line)))
 
 (def (finding-simple-function-scope source finding)
     (let* ([line (finding-start-line finding)]
diff --git a/tests/smoke.ss b/tests/smoke.ss
index 965a1a8..10e0d7f 100644
--- a/tests/smoke.ss
+++ b/tests/smoke.ss
@@ -2334,6 +2334,18 @@
     (check (length cast-findings) => 2)
     (check (finding-message (car cast-findings)) => "s is a String")))
 
+(test-case "scan Dart taint ignores catch after nonthrowing return"
+  (let* ([dart-config
+          "rules:\n  - id: demo.dart.try-return\n    languages: [dart]\n    message: tainted\n    mode: taint\n    pattern-sinks:\n      - pattern: sink(...)\n    pattern-sources:\n      - pattern: source(...)\n    severity: WARNING\n"]
+         [findings
+          (scan-config-string
+            dart-config
+            "dart"
+            "demo.dart"
+            "String? couldThrow() { return null; }\nString source() { return \"tainted\"; }\nvoid sink(String s) {}\nString? test1() {\n  String str = \"safe\";\n  try {\n    return couldThrow();\n  } catch (e) {\n    str = source();\n  }\n  sink(str);\n  return null;\n}\nint? test2() {\n  int cannotThrow = 42;\n  String str = \"safe\";\n  try {\n    return cannotThrow;\n  }\n  catch (e) {\n    str = source();\n  }\n  sink(str);\n  return null;\n}\n")])
+    (check (length findings) => 1)
+    (check (finding-start-line (car findings)) => 11)))
+
 (test-case "scan Move metavariable type with generic text fallback"
   (let* ([move-config
           "rules:\n  - id: metavar_type_simple_move_on_aptos\n    patterns:\n      - pattern: $X.do_nothing(...)\n      - metavariable-type:\n          metavariable: $X\n          types:\n            - ConstructorRef\n            - object\n    message: Semgrep found a match\n    languages:\n      - move_on_aptos\n    severity: WARNING\n"]