Filter dead Dart catch taint paths
ober
06738f3c10d844b7e8a93af6601d77fe05d8ed3b
--- 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 ``` --- 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)] --- 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")) --- 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)] --- 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"]