Cover JavaScript taint throw reachability

ober

1d6987f3009b1ccc8e4ec0d7ed7219a4ece35d81

diff --git a/HANDOFF_OPUS_4_8.md b/HANDOFF_OPUS_4_8.md
index 5b624db..8231fe9 100644
--- a/HANDOFF_OPUS_4_8.md
+++ b/HANDOFF_OPUS_4_8.md
@@ -1,218 +1,174 @@
 # Opus 4.8 Handoff: jerboa-semgrep Semgrep Parity
 
-Date: 2026-05-30 04:23 MDT
+Date: 2026-05-30 05:16 MDT
 Workspace: `/Users/user/mine/jerboa-semgrep`
 Sibling upstream Semgrep checkout: `/Users/user/mine/semgrep`
 Packaged Semgrep oracle: `/Users/user/.local/bin/semgrep`
-Base HEAD before this checkpoint:
-`403a73a Cover JavaScript JSX and XML fixtures`
+Base HEAD before this checkpoint: `4e0eedf Cover JavaScript deep pattern fixtures`
 
 The user wants this project carried forward until the pure Jerboa port reaches
-Semgrep parity. Do not treat this handoff as completion. This checkpoint closes
-the remaining JavaScript `.sgrep` pattern-fixture mismatches from the upstream
-`tests/patterns/js` corpus, but Semgrep parity is still broader than these
-fixture sweeps.
+Semgrep parity. Do not treat this handoff as completion. This checkpoint moves
+the next parity frontier from Python/JavaScript pattern fixtures into upstream
+JavaScript taint rules and fixes one concrete semantic mismatch there.
 
 ## Current Headline
 
-- Full Python pattern sweep: 164 passed / 0 mismatched.
-- Full JavaScript pattern sweep: 132 passed / 0 mismatched. This improved from
-  117 passed / 15 mismatched by closing the caching, deep statement/expression,
-  JSX deep-expression, and method-chain-vs-statement frontier.
-- Promoted JavaScript oracle slice: 91 passed / 0 mismatched.
-- Promoted Python oracle slice: 116 passed / 0 mismatched.
+- Full Python `.sgrep` pattern sweep: 164 passed / 0 mismatched.
+- Full JavaScript `.sgrep` pattern sweep: 132 passed / 0 mismatched.
+- Promoted JavaScript pattern oracle: 91 passed / 0 mismatched.
+- Promoted Python pattern oracle: 116 passed / 0 mismatched.
 - Local oracle: 42 passed / 0 failed.
-- Broad same-basename upstream sweep: 437 passed / 0 mismatched / 0 Jerboa
-  errors, with 3 current-Semgrep oracle errors.
-- Smoke suite: 300 tests, 300 passed, 0 failed.
-- Semgrep parity is not reached yet. Pattern fixtures for Python and
-  JavaScript are now clean, but the project still needs broader parser,
-  matcher, rule-validation, taint/dataflow, target-selection, CLI, output,
-  autofix, and corpus compatibility.
-
-## Recent Checkpoint Chain
-
-Recent commits/checkpoints that matter for the next agent:
-
-- `2abb070 Cover JavaScript import equivalence fixtures`: committed. It
-  promoted the 13-case import/CommonJS equivalence cluster and left the full JS
-  sweep at 82 passed / 50 mismatched.
-- `1de036c Cover JavaScript singleton pattern fixtures`: committed. It
-  promoted ten small JS singleton fixture cases and left the full JS sweep at
-  92 passed / 40 mismatched.
-- `2cda4de Cover JavaScript decorator and catch fixtures`: committed. It
-  promoted six catch/decorator/export cases and left the full JS sweep at
-  98 passed / 34 mismatched.
-- `1b617d3 Cover JavaScript class and dependency fixtures`: committed. It
-  promoted five class/dependency-field cases and left the full JS sweep at
-  103 passed / 29 mismatched.
-- `88336fa Cover JavaScript arrow and function fixtures`: committed. It
-  promoted three arrow/function cases and left the full JS sweep at
-  106 passed / 26 mismatched.
-- `94d853d Cover JavaScript equality and AC fixtures`: committed. It promoted
-  three equality/range/AC cases and left the full JS sweep at
-  109 passed / 23 mismatched.
-- `403a73a Cover JavaScript JSX and XML fixtures`: committed. It promoted
-  eight JSX/XML cases and left the full JS sweep at
-  117 passed / 15 mismatched.
-- This checkpoint: commit as `Cover JavaScript deep pattern fixtures`. It
-  promotes 15 caching/deep/method-chain cases and leaves the full JS pattern
-  sweep at 132 passed / 0 mismatched.
-
-The current checkpoint modifies only the Jerboa scanner, generated scanner
-artifact, promoted oracle regex, smoke tests, generated build hashes, and this
-handoff. No Semgrep upstream files were changed.
-
-## Project Contract
-
-`what.md` is still the project contract:
-
-- Replace Semgrep's OCaml and Python implementation with ordinary Jerboa `.ss`
-  modules.
-- Preserve existing Semgrep rules unchanged.
-- Preserve Semgrep-compatible scan behavior and output formats.
-- Keep the result embeddable in Jerboa applications.
-
-The implementation is still a compatibility work in progress. Python and
-JavaScript `.sgrep` pattern fixtures are green, but full Semgrep parity also
-requires broader rule, taint, target-selection, parser, output, autofix, and
-CLI compatibility beyond these fixture slices.
-
-## Files In This Checkpoint
+- Smoke suite: 301 tests, 301 passed, 0 failed.
+- Broad same-basename upstream `tests/rules` sweep: 437 passed / 0 mismatched
+  / 0 Jerboa errors, with 3 packaged-Semgrep current errors.
+- New upstream `tests/tainting_rules/js` sweep: 5 passed / 6 mismatched /
+  0 Jerboa errors / 0 current errors. This improved from 4 passed / 7
+  mismatched by matching Semgrep's handling of unreachable JS taint sinks after
+  `throw`.
+
+Semgrep parity is not reached. Python and JavaScript pattern fixtures are clean,
+but taint/dataflow, parser compatibility, rule validation, CLI behavior, output,
+autofix, target selection, ignore handling, and broader upstream test corpora
+remain open.
+
+## What Changed In This Checkpoint
 
 This checkpoint modifies:
 
 ```text
-Makefile
 HANDOFF_OPUS_4_8.md
 src/semgrep/scan.ss
 lib/semgrep/scan.sls
 src/.jerbuild-hashes
+tests/oracle/normalize-findings.ss
 tests/smoke.ss
 ```
 
 `lib/semgrep/scan.sls` and `src/.jerbuild-hashes` are generated by
-`make build`/`make test`, but they are tracked and should be committed with the
-matching source changes.
+`make build`/`make test`, but they are tracked and must stay in the commit with
+the matching `src/semgrep/scan.ss` changes.
 
-Expected post-commit state:
+### Oracle Normalization
 
-```sh
-git status --short --branch
-```
-
-should show a clean `main` branch.
-
-## Verified This Checkpoint
-
-All commands below were run from `/Users/user/mine/jerboa-semgrep`.
-
-Build:
-
-```sh
-make build
-```
-
-Result:
+`tests/oracle/normalize-findings.ss` now strips check-id prefixes emitted by
+packaged Semgrep for rules under `tests/tainting_rules`, including language
+subdirectories such as:
 
 ```text
-jerbuild: 14 processed, 0 skipped, 0 errors
+Users.user.mine.semgrep.tests.tainting_rules.js.
+tests.tainting_rules.js.
 ```
 
-Focused JavaScript caching/deep-condition slice:
+Without this normalization, `tests/tainting_rules/js` appeared to have extra
+mismatches where the only difference was a check id like
+`Users.user.mine.semgrep.tests.tainting_rules.js.taint-test` versus
+`taint-test`.
 
-```sh
-SEMGREP_CURRENT=/Users/user/.local/bin/semgrep PATTERN_LANGS=js CASE_REGEX='^(caching_deep|caching_deep_metavar|caching_different_depth|caching_flat|caching_nested|deep_cond)$' LIST_MISMATCHES=1 MAX_DIFFS=200 tests/oracle/patterns-sweep.sh
-```
+### JavaScript Taint Reachability
 
-Result:
+`src/semgrep/scan.ss` now has a generic taint reachability wrapper:
 
-```text
-patterns-sweep: 6 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 6 compared
-```
-
-Focused JavaScript deep statement/expression slice:
-
-```sh
-SEMGREP_CURRENT=/Users/user/.local/bin/semgrep PATTERN_LANGS=js CASE_REGEX='^(deep_deep_stmt2|deep_expr_and_ellipsis|deep_expr_vs_statement|deep_stmt|deep_stmt2|misc_deep_expr_stmt|misc_method_chain_vs_stmt)$' LIST_MISMATCHES=1 MAX_DIFFS=260 tests/oracle/patterns-sweep.sh
-```
+- `javascript-taint-line-opens-unreachable?`
+- `javascript-taint-unreachable-finding?`
+- `filter-javascript-taint-reachable-findings`
+- `filter-taint-reachable-findings`
 
-Result:
+The wrapper is applied wherever taint specs are scanned:
 
-```text
-patterns-sweep: 7 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 7 compared
-```
+- sources and source matches
+- sinks
+- explicit propagators
+- implicit assignment propagators
+- sanitizers through `scan-taint-specs`
 
-Focused final JavaScript deep fixtures:
+The JavaScript filter currently treats only a leading `throw ...` line as
+opening an unreachable region until the next line that starts with `}`. This is
+deliberately narrower than the existing constant-propagation helper. A first
+attempt also treated plain `return ...` as unreachable, but that was too coarse:
+it suppressed valid taint findings inside multiline return expressions and
+returned callbacks. The regression was caught by upstream
+`tests/rules/taint_object_destructure`. Future `return` support should be
+statement-aware, not line-only.
 
-```sh
-SEMGREP_CURRENT=/Users/user/.local/bin/semgrep PATTERN_LANGS=js CASE_REGEX='^(deep_deep_stmt1|deep_expr_xml)$' LIST_MISMATCHES=1 MAX_DIFFS=260 tests/oracle/patterns-sweep.sh
-```
+### Smoke Coverage
 
-Result:
+`tests/smoke.ss` adds:
 
 ```text
-patterns-sweep: 2 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 2 compared
+scan JavaScript taint ignores unreachable after throw
 ```
 
-Smoke suite:
+It checks that:
 
-```sh
-make test
+```javascript
+if (random()) {
+  sink(source);
+} else {
+  throw "error";
+  sink(source);
+}
 ```
 
-Result:
+reports only the reachable sink.
 
-```text
-300 tests, 300 passed, 0 failed
-```
+## Verification Run
+
+All commands below were run from `/Users/user/mine/jerboa-semgrep`.
 
-Promoted JavaScript oracle:
+Smoke:
 
 ```sh
-SEMGREP_CURRENT=/Users/user/.local/bin/semgrep make patterns-js-oracle
+make test
 ```
 
 Result:
 
 ```text
-patterns-sweep: 91 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 91 compared
+301 tests, 301 passed, 0 failed
 ```
 
-Full upstream JavaScript pattern sweep:
+Focused upstream JS taint `throw` case:
 
 ```sh
-SEMGREP_CURRENT=/Users/user/.local/bin/semgrep PATTERN_LANGS=js LIST_MISMATCHES=1 MAX_DIFFS=80 tests/oracle/patterns-sweep.sh
+SEMGREP_CURRENT=/Users/user/.local/bin/semgrep \
+UPSTREAM_RULE_DIR=/Users/user/mine/semgrep/tests/tainting_rules/js \
+CASE_REGEX='^(throw)$' LIST_MISMATCHES=1 MAX_DIFFS=80 \
+tests/oracle/upstream-sweep.sh
 ```
 
 Result:
 
 ```text
-patterns-sweep: 132 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 132 compared
+upstream-sweep: 1 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 1 compared
 ```
 
-Promoted Python oracle:
+Focused regression guard for the rejected `return` implementation:
 
 ```sh
-SEMGREP_CURRENT=/Users/user/.local/bin/semgrep make patterns-oracle
+SEMGREP_CURRENT=/Users/user/.local/bin/semgrep \
+UPSTREAM_RULE_DIR=/Users/user/mine/semgrep/tests/rules \
+CASE_REGEX='^(taint_object_destructure)$' LIST_MISMATCHES=1 MAX_DIFFS=80 \
+tests/oracle/upstream-sweep.sh
 ```
 
 Result:
 
 ```text
-patterns-sweep: 116 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 116 compared
+upstream-sweep: 1 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 1 compared
 ```
 
-Full upstream Python pattern sweep:
+Full upstream JS tainting-rule subdirectory:
 
 ```sh
-SEMGREP_CURRENT=/Users/user/.local/bin/semgrep PATTERN_LANGS=python LIST_MISMATCHES=1 MAX_DIFFS=60 tests/oracle/patterns-sweep.sh
+SEMGREP_CURRENT=/Users/user/.local/bin/semgrep \
+UPSTREAM_RULE_DIR=/Users/user/mine/semgrep/tests/tainting_rules/js \
+LIST_MISMATCHES=1 MAX_DIFFS=160 tests/oracle/upstream-sweep.sh
 ```
 
 Result:
 
 ```text
-patterns-sweep: 164 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 164 compared
+upstream-sweep: 5 passed, 6 mismatched, 0 jerboa errors, 0 current errors, 11 compared
 ```
 
 Local oracle:
@@ -227,10 +183,12 @@ Result:
 oracle: 42 passed, 0 failed
 ```
 
-Broad same-basename upstream sweep:
+Broad same-basename upstream `tests/rules` sweep:
 
 ```sh
-SEMGREP_CURRENT=/Users/user/.local/bin/semgrep tests/oracle/upstream-sweep.sh
+SEMGREP_CURRENT=/Users/user/.local/bin/semgrep \
+UPSTREAM_RULE_DIR=/Users/user/mine/semgrep/tests/rules \
+LIST_MISMATCHES=1 MAX_DIFFS=40 tests/oracle/upstream-sweep.sh
 ```
 
 Result:
@@ -243,276 +201,118 @@ The three current-side errors are packaged-Semgrep schema failures for
 `anywhere_global`, `anywhere_include`, and `anywhere_metavar`, not Jerboa
 mismatches.
 
-Whitespace check:
-
-```sh
-git diff --check
-```
-
-Result: no output and exit code 0.
-
-## What Changed In This Checkpoint
-
-`JS_PATTERN_CASE_REGEX` in `Makefile` now promotes these 15 additional JS
-fixture cases:
-
-```text
-caching_deep
-caching_deep_metavar
-caching_different_depth
-caching_flat
-caching_nested
-deep_cond
-deep_deep_stmt1
-deep_deep_stmt2
-deep_expr_and_ellipsis
-deep_expr_vs_statement
-deep_expr_xml
-deep_stmt
-deep_stmt2
-misc_deep_expr_stmt
-misc_method_chain_vs_stmt
-```
-
-The full promoted JS oracle now covers 91 cases. It is still a promoted slice,
-not the full 132-case JS corpus; keep running the full JS sweep before claiming
-future JS pattern parity.
-
-## Implementation Map
+## Remaining `tests/tainting_rules/js` Mismatches
 
-All scanner logic is in `src/semgrep/scan.ss` and generated into
-`lib/semgrep/scan.sls`.
+The `throw` mismatch is fixed. The six remaining JS tainting-rule mismatches
+are real semantic gaps:
 
-### Prior JavaScript Checkpoints
+1. `await`: Semgrep reports two string-concat SQL sinks under Express-style
+   arrow callbacks; Jerboa reports zero. Likely gap: source matching with
+   `pattern-inside: function ... ($REQ, $RES) {...}` does not model arrow
+   callback parameters as compatible sources, and/or the string-concat sink is
+   not connected through `await`.
 
-The existing committed fallback bundles are still important context:
+2. `eslint_obj_inj`: Semgrep reports `return o[c]` after `c` is conditionally
+   assigned from tainted parameter `x`; Jerboa misses that branch merge. Likely
+   gap: path-insensitive propagation from conditional assignments such as
+   `if (...) c = x else c = 1`.
 
-- Import/CommonJS equivalence from `2abb070`.
-- Singleton JavaScript fixture fallbacks from `1de036c`.
-- Catch/decorator/export fallbacks from `2cda4de`.
-- Class/dependency-field fallbacks from `1b617d3`.
-- Arrow/function fallbacks from `88336fa`.
-- Equality/range/AC fallbacks from `94d853d`.
-- JSX/XML simple-element fallbacks from `403a73a`.
+3. `metavar_eq_conditional`: Semgrep keeps both possible taint bindings from
+   `cond ? get(A) : get(B)` and reports both `A` and `B` messages at both
+   sinks; Jerboa keeps only the locally matching binding at each sink. Likely
+   gap: conditional-expression sources need multiple alternative taint
+   states/metavariable environments.
 
-Those bundles remain deliberately shape-gated. They are compatibility bridges
-for upstream fixtures, not replacements for a complete generic Semgrep matcher.
+4. `metavar_eq_simple`: The rule sets `options: taint_unify_mvars: true`.
+   Semgrep reports only matching source/sink metavariable pairs; Jerboa
+   overreports wrong `$X` combinations and unrelated sources. Likely gap:
+   source-origin metavariable compatibility is checked too late or with the
+   wrong finding after assignment propagation.
 
-### Deep Sequence/Condition Bundle In This Checkpoint
+5. `sanitized_by_side_effect`: Semgrep still reports `jwt.decode(token, true)`
+   when `jwt.verify(token2, key)` sanitized a different token. Jerboa
+   over-sanitizes. Likely gap: by-side-effect sanitizer identity should require
+   compatible token/source binding, not just same function scope and ordering.
 
-This checkpoint extends `scan-javascript-small-pattern-fallbacks` with:
+6. `simpl_nodejs_eval`: Jerboa misses the direct concat sink
+   `s.run('lol(' + req.query.userInput + ')', cb)` and has a one-column
+   overlong range on the template-literal sink. Likely gaps: direct string
+   concatenation taint into a focused sink expression, plus template literal
+   range trimming.
 
-- `scan-javascript-deep-sequence-pattern`
-- `scan-javascript-deep-condition-pattern`
+## Recommended Next Work
 
-The deep-sequence fallback covers simple semicolon statement sequences with
-ellipsis separators, including numeric literals and simple metavariable
-statements. It closes:
+Start with one of these focused targets:
 
-```text
-caching_deep
-caching_deep_metavar
-caching_different_depth
-caching_flat
-caching_nested
-```
-
-Important behavior:
-
-- It builds line-level simple statement records with brace depth.
-- It allows ellipsis to enter nested blocks from a shallower start.
-- It does not allow a match that starts inside a nested block to escape to a
-  shallower following statement.
-- If the final matched statement is deeper than the start statement, the
-  finding range extends through the closing brace that returns to the start
-  depth. This mirrors Semgrep's ranges for `caching_deep` and
-  `caching_nested`.
-- Metavariable statements bind the statement token without the trailing
-  semicolon.
-
-The deep-condition fallback covers:
-
-```text
-if(<... $X = true ...>)
-  ...
+```sh
+SEMGREP_CURRENT=/Users/user/.local/bin/semgrep \
+UPSTREAM_RULE_DIR=/Users/user/mine/semgrep/tests/tainting_rules/js \
+CASE_REGEX='^(metavar_eq_simple)$' LIST_MISMATCHES=1 MAX_DIFFS=120 \
+tests/oracle/upstream-sweep.sh
 ```
 
-It finds `if` conditions containing an assignment to `true`, including nested
-conditions such as `(b == c) && (x = true)`, and returns the full `if` block
-range. This closes `deep_cond`.
+`metavar_eq_simple` is probably the best next fix because the rule option is
+already parsed and helper functions exist:
 
-Key helpers:
+- `taint-unify-mvars?`
+- `taint-mvars-compatible?`
+- `bindings-compatible?`
+- `propagated-source`
+- `source-state-reaches-sink-spec-shape?`
 
-- `javascript-deep-sequence-pattern-token`
-- `javascript-simple-sequence-statement-infos`
-- `javascript-deep-sequence-scope-end`
-- `javascript-condition-true-assignment-binding`
+Inspect whether propagated taint states preserve the source metavariable
+bindings needed for unification after assignment. Be careful not to break the
+existing smoke test `scan JavaScript taint unifies source and sink
+metavariables`.
 
-### Deep Statement/Expression Bundle In This Checkpoint
+Alternative focused targets:
 
-This checkpoint also adds these focused fallbacks:
+- `await`, if you want to improve function/arrow `pattern-inside` compatibility.
+- `sanitized_by_side_effect`, if you want to improve sanitizer/source identity.
+- `simpl_nodejs_eval`, if you want a smaller expression/range fix.
 
-- `scan-javascript-deep-expr-vs-statement-pattern`
-- `scan-javascript-function-console-log-pattern`
-- `scan-javascript-var-function-console-log-pattern`
-- `scan-javascript-index-use-pattern`
-- `scan-javascript-require-secret-verify-pattern`
-- `scan-javascript-try-localstorage-catch-pattern`
-- `scan-javascript-express-assignment-ellipsis-pattern`
-- `scan-javascript-var-deep-console-log-pattern`
-- `scan-javascript-deep-html-template-pattern`
+After any taint change, run at least:
 
-Cases closed:
-
-```text
-deep_deep_stmt1
-deep_deep_stmt2
-deep_expr_and_ellipsis
-deep_expr_vs_statement
-deep_expr_xml
-deep_stmt
-deep_stmt2
-misc_deep_expr_stmt
-misc_method_chain_vs_stmt
-```
-
-Behavior covered:
-
-- `function $F() { ... console.log(...); }` returns the full function
-  declaration range and binds `$F`.
-- `var $X = "..."; ... function $F() { ... console.log($X); }` returns the
-  range from the string variable declaration through the function body close.
-- `const $JWT = require("jsonwebtoken"); ... const $SECRET = "..."; ...
-  $JWT.verify($P, $SECRET, ...);` returns the range through the enclosing
-  arrow/function block containing the verify call and binds JWT, secret, and
-  payload.
-- `try { ... (<... localStorage.$METHOD(...) ...>); ... } catch ($ERR) { ... }`
-  returns the try/catch range and binds the localStorage method plus catch
-  parameter.
-- `<... a ...>; b` returns the statement containing token `a` plus the
-  following `b` statement.
-- `$ARG = [$V]; ... <... $O[$ARG] ...>;` returns the range from the array
-  assignment through the indexed use and binds argument, value, and object.
-- `$APP = express() ...` returns both the assignment-only finding with the
-  semicolon excluded and the wider `app.configure(...)` finding with the
-  configure semicolon included, matching Semgrep's ambiguity in
-  `misc_method_chain_vs_stmt`.
-- `var $X = 1; ... console.log($X);` returns ranges through the first nested
-  block/call containing the log and rejects a nested `function foo(x)` that
-  shadows the variable name.
-- `<... {__html: `...`} ...>` returns all six React deep-expression fixture
-  ranges: the return parenthesized JSX expression, the `<body>` paired range,
-  each `<noscript>`/`<script>` self-closing tag, and each inner
-  `dangerouslySetInnerHTML` object range.
-
-Important range notes:
-
-- `misc_method_chain_vs_stmt` intentionally has different semicolon behavior
-  for its two findings: the assignment-only range excludes `;`, while the
-  configure-call range includes it.
-- `deep_expr_xml` starts the attribute object range at the second `{` in
-  `dangerouslySetInnerHTML={{` and ends at the first matching `}` of the inner
-  object, matching Semgrep's fixture output.
-- `deep_deep_stmt1` uses a fixture-specific shadowing check for function
-  parameters such as `function foo(x)`.
-
-Key helpers:
-
-- `javascript-function-declaration-info`
-- `javascript-console-log-call-of?`
-- `javascript-index-use-line-info`
-- `javascript-find-verify-call-line`
-- `javascript-localstorage-method-binding`
-- `javascript-express-assignment-line-info`
-- `javascript-deep-html-attribute-range`
-- `javascript-deep-html-tag-range`
-- `javascript-deep-html-body-range`
-- `javascript-deep-html-return-range`
-
-## Smoke Coverage Added
-
-This checkpoint adds two smoke tests:
-
-- `scan JavaScript deep sequence and condition fixture fallbacks`
-- `scan JavaScript deep statement and expression fixture fallbacks`
-
-They cover:
-
-- Flat and nested numeric statement sequences.
-- Sequence ranges that enter nested blocks and include closing braces.
-- Sequence starts that cannot escape to shallower final statements.
-- Metavariable sequence starts.
-- Deep `if(<... $X = true ...>)` assignment conditions.
-- Function and var-to-function console-log ranges.
-- JSON Web Token require/secret/verify sequence ranges.
-- Try/localStorage/catch ranges and bindings.
-- Deep expression-vs-statement ranges.
-- Assignment-to-indexed-use ranges.
-- Express assignment ambiguity, including both the assignment-only and
-  configure-call findings.
-- Deep `var $X = 1; ... console.log($X);` shadowing rejection.
-- React `__html` JSX deep-expression ranges.
-
-The smoke suite count is now 300.
-
-## Current Frontier
-
-The full Python `.sgrep` pattern fixture sweep is clean:
-
-```text
-patterns-sweep: 164 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 164 compared
+```sh
+make test
+SEMGREP_CURRENT=/Users/user/.local/bin/semgrep make oracle
+SEMGREP_CURRENT=/Users/user/.local/bin/semgrep \
+UPSTREAM_RULE_DIR=/Users/user/mine/semgrep/tests/tainting_rules/js \
+LIST_MISMATCHES=1 MAX_DIFFS=160 tests/oracle/upstream-sweep.sh
 ```
 
-The full JavaScript `.sgrep` pattern fixture sweep is clean:
+For broader safety, rerun:
 
-```text
-patterns-sweep: 132 passed, 0 mismatched, 0 jerboa errors, 0 current errors, 0 skipped, 132 compared
+```sh
+SEMGREP_CURRENT=/Users/user/.local/bin/semgrep \
+UPSTREAM_RULE_DIR=/Users/user/mine/semgrep/tests/rules \
+LIST_MISMATCHES=1 MAX_DIFFS=40 tests/oracle/upstream-sweep.sh
 ```
 
-There are no remaining JavaScript `.sgrep` pattern-fixture mismatches in the
-current upstream checkout. The next high-value parity work should move beyond
-these pattern fixture slices:
+It is slow and quiet for long periods, but it caught the rejected `return`
+reachability implementation.
 
-1. Expand same-basename upstream rule coverage beyond the 440-case sweep.
-2. Add targeted parity gates for Semgrep parser behavior and language-specific
-   AST adaptation outside Python/JavaScript.
-3. Broaden taint/dataflow compatibility against upstream rule and CLI tests.
-4. Audit CLI/output/autofix/schema compatibility against Semgrep snapshots.
-5. Reduce the shape-specific JavaScript fallbacks into more general structural
-   matcher behavior where doing so is safe and test-backed.
+## Operational Notes
 
-## Regression Guards To Keep
+- Use `rg` first for source discovery.
+- Use `apply_patch` for manual edits.
+- Do not revert unrelated user changes in a dirty tree.
+- `make build` can pass even when scanner logic is behaviorally wrong. Always
+  run a focused oracle for newly touched paths.
+- Keep `SEMGREP_CURRENT=/Users/user/.local/bin/semgrep` set for comparisons
+  against packaged Semgrep.
+- The sibling upstream checkout at `/Users/user/mine/semgrep` provides the
+  fixture and rule corpora.
+- The upstream sweep harness compares same-basename rule/target pairs in one
+  directory at a time. Use `UPSTREAM_RULE_DIR=.../tests/tainting_rules/js` for
+  the active JS taint frontier.
+- If you broaden check-id normalization again, prefer explicit known prefixes.
+  Avoid normalizing arbitrary suffixes that could hide real check-id bugs.
 
-- Rerun the focused 15-case JS deep slice after touching any helper added in
-  this checkpoint:
+Expected post-commit state:
 
 ```sh
-SEMGREP_CURRENT=/Users/user/.local/bin/semgrep PATTERN_LANGS=js CASE_REGEX='^(caching_deep|caching_deep_metavar|caching_different_depth|caching_flat|caching_nested|deep_cond|deep_deep_stmt1|deep_deep_stmt2|deep_expr_and_ellipsis|deep_expr_vs_statement|deep_expr_xml|deep_stmt|deep_stmt2|misc_deep_expr_stmt|misc_method_chain_vs_stmt)$' LIST_MISMATCHES=1 MAX_DIFFS=300 tests/oracle/patterns-sweep.sh
+git status --short --branch
 ```
 
-- Rerun `make patterns-js-oracle`; it is now the 91-case promoted JS guard.
-- Rerun the full 132-case JS pattern sweep before claiming JS pattern parity.
-- Rerun the full 164-case Python pattern sweep before claiming both Python and
-  JavaScript pattern parity.
-- Rerun the broad upstream same-basename sweep after any fallback that can
-  affect general rule matching. It is slow and quiet for long periods, but it
-  caught previous overmatching.
-- Keep the older focused guards from prior checkpoints for import/CommonJS,
-  singleton, catch/decorator/export, class/dependency, arrow/function,
-  equality/AC, and JSX/XML helpers.
-
-## Operational Notes
-
-- Use `rg` first for source discovery.
-- Prefer `apply_patch` for manual edits.
-- Do not revert unrelated user changes in a dirty tree.
-- `make build` can pass even when a scanner fallback has a runtime definition
-  issue. Always run a focused oracle for newly touched paths.
-- `SEMGREP_CURRENT=/Users/user/.local/bin/semgrep` is required for comparisons
-  against the packaged Semgrep oracle.
-- The sibling Semgrep checkout at `/Users/user/mine/semgrep` provides upstream
-  fixtures. JS pattern fixtures live under
-  `/Users/user/mine/semgrep/tests/patterns/js/`.
-- Some upstream fixture patterns include leading comments that flow through the
-  fixture harness into the pattern text. Prefer robust shape detection over
-  whole-pattern equality when comments can appear.
+should show a clean `main` branch.
diff --git a/lib/semgrep/scan.sls b/lib/semgrep/scan.sls
index d73b356..ca86989 100644
--- a/lib/semgrep/scan.sls
+++ b/lib/semgrep/scan.sls
@@ -30006,6 +30006,66 @@
                       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))]
+              [trimmed (string-trim line)])
+         (sg-string-prefix? "throw " trimmed)))
+  (def (javascript-taint-unreachable-finding? source finding)
+       (let ([target (finding-start-offset finding)]
+             [len (string-length source)])
+         (let loop ([line-start 0] [unreachable? #f])
+           (cond
+             [(>= line-start target) unreachable?]
+             [(> line-start len) unreachable?]
+             [else
+              (let* ([line-end (line-end-after source line-start)]
+                     [next (if (< line-end len) (+ line-end 1) (+ len 1))])
+                (if (<= target line-end)
+                    unreachable?
+                    (let* ([closed? (js-cp-line-closes-block?
+                                      source
+                                      line-start
+                                      line-end)]
+                           [skip? (and unreachable? (not closed?))]
+                           [next-unreachable? (or (and unreachable?
+                                                       (not closed?))
+                                                  (and (not skip?)
+                                                       (javascript-taint-line-opens-unreachable?
+                                                         source
+                                                         line-start
+                                                         line-end)))])
+                      (loop next next-unreachable?))))]))))
+  (def (filter-javascript-taint-reachable-findings
+         language
+         source
+         findings)
+       (if (javascript-like-language? language)
+           (sg-filter
+             (lambda (finding)
+               (not (javascript-taint-unreachable-finding?
+                      source
+                      finding)))
+             findings)
+           findings))
+  (def (filter-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
@@ -30013,7 +30073,7 @@
          (map (lambda (spec)
                 (map (lambda (finding)
                        (taint-state-for-spec spec finding default-label))
-                     (filter-python-taint-reachable-findings
+                     (filter-taint-reachable-findings
                        language
                        source
                        (scan-positive-pattern-entry rule (alist-ref/default spec 'entry #f) language
@@ -30035,7 +30095,7 @@
                          (cons
                            'requires
                            (alist-ref/default spec 'requires #f))))
-                     (filter-python-taint-reachable-findings
+                     (filter-taint-reachable-findings
                        language
                        source
                        (scan-positive-pattern-entry rule (alist-ref/default spec 'entry #f) language
@@ -30060,7 +30120,7 @@
                          (cons
                            'requires
                            (alist-ref/default spec 'requires #f))))
-                     (filter-python-taint-reachable-findings
+                     (filter-taint-reachable-findings
                        language
                        source
                        (scan-positive-pattern-entry rule (alist-ref/default spec 'entry #f) language
@@ -30096,7 +30156,7 @@
                              'replace-labels
                              #f))
                          (cons 'implicit-assignment #f)))
-                     (filter-python-taint-reachable-findings
+                     (filter-taint-reachable-findings
                        language
                        source
                        (scan-positive-pattern-entry rule (alist-ref/default propagator 'entry #f)
@@ -30138,7 +30198,7 @@
                          (cons 'label #f) (cons 'requires #f)
                          (cons 'replace-labels #f)
                          (cons 'implicit-assignment #t)))
-                     (filter-python-taint-reachable-findings
+                     (filter-taint-reachable-findings
                        language
                        source
                        (scan-positive-pattern-entry rule (cons 'pattern pattern) language path source
diff --git a/src/.jerbuild-hashes b/src/.jerbuild-hashes
index cc24bec..ad89e81 100644
--- a/src/.jerbuild-hashes
+++ b/src/.jerbuild-hashes
@@ -3,7 +3,7 @@
   ("src/semgrep/output/json.ss" . "293881CFA2ADB7BC")
   ("src/semgrep/lang.ss" . "6982E07679D20836")
   ("src/semgrep/parse/parse-target.ss" . "E74854DDDACF6BA")
-  ("src/semgrep/scan.ss" . "4B73E368A6EB1730")
+  ("src/semgrep/scan.ss" . "564654C107FA90C0")
   ("src/semgrep/fix.ss" . "2E5B65B1FEF3B2B1")
   ("src/semgrep/output/text.ss" . "BE476CB84B807FBA")
   ("src/semgrep/rule.ss" . "E12C108153C181FA")
diff --git a/src/semgrep/scan.ss b/src/semgrep/scan.ss
index a879bae..5ed8aec 100644
--- a/src/semgrep/scan.ss
+++ b/src/semgrep/scan.ss
@@ -29971,12 +29971,62 @@
         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))]
+         [trimmed (string-trim line)])
+    (sg-string-prefix? "throw " trimmed)))
+
+(def (javascript-taint-unreachable-finding? source finding)
+  (let ([target (finding-start-offset finding)]
+        [len (string-length source)])
+    (let loop ([line-start 0] [unreachable? #f])
+      (cond
+        [(>= line-start target) unreachable?]
+        [(> line-start len) unreachable?]
+        [else
+         (let* ([line-end (line-end-after source line-start)]
+                [next (if (< line-end len)
+                          (+ line-end 1)
+                          (+ len 1))])
+           (if (<= target line-end)
+               unreachable?
+               (let* ([closed? (js-cp-line-closes-block?
+                                source
+                                line-start
+                                line-end)]
+                      [skip? (and unreachable? (not closed?))]
+                      [next-unreachable?
+                       (or (and unreachable? (not closed?))
+                           (and (not skip?)
+                                (javascript-taint-line-opens-unreachable?
+                                  source
+                                  line-start
+                                  line-end)))])
+                 (loop next next-unreachable?))))]))))
+
+(def (filter-javascript-taint-reachable-findings language source findings)
+  (if (javascript-like-language? language)
+      (sg-filter
+        (lambda (finding)
+          (not (javascript-taint-unreachable-finding?
+                 source
+                 finding)))
+        findings)
+      findings))
+
+(def (filter-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
          (map (lambda (spec)
                 (map (lambda (finding)
                        (taint-state-for-spec spec finding default-label))
-                     (filter-python-taint-reachable-findings
+                     (filter-taint-reachable-findings
                        language
                        source
                        (scan-positive-pattern-entry
@@ -30000,7 +30050,7 @@
                                  "__SOURCE__"))
                          (cons 'requires
                                (alist-ref/default spec 'requires #f))))
-                     (filter-python-taint-reachable-findings
+                     (filter-taint-reachable-findings
                        language
                        source
                        (scan-positive-pattern-entry
@@ -30035,7 +30085,7 @@
                                            #f))))
                          (cons 'requires
                                (alist-ref/default spec 'requires #f))))
-                     (filter-python-taint-reachable-findings
+                     (filter-taint-reachable-findings
                        language
                        source
                        (scan-positive-pattern-entry
@@ -30069,7 +30119,7 @@
                                                   'replace-labels
                                                   #f))
                          (cons 'implicit-assignment #f)))
-                     (filter-python-taint-reachable-findings
+                     (filter-taint-reachable-findings
                        language
                        source
                        (scan-positive-pattern-entry
@@ -30132,7 +30182,7 @@
                          (cons 'requires #f)
                          (cons 'replace-labels #f)
                          (cons 'implicit-assignment #t)))
-                     (filter-python-taint-reachable-findings
+                     (filter-taint-reachable-findings
                        language
                        source
                        (scan-positive-pattern-entry
diff --git a/tests/oracle/normalize-findings.ss b/tests/oracle/normalize-findings.ss
index 4f7edb7..48d00cd 100644
--- a/tests/oracle/normalize-findings.ss
+++ b/tests/oracle/normalize-findings.ss
@@ -39,6 +39,26 @@
 
 (def known-check-id-prefixes
   '("Users.user.mine.semgrep.tests.rules."
+    "Users.user.mine.semgrep.tests.tainting_rules.dart."
+    "Users.user.mine.semgrep.tests.tainting_rules.go."
+    "Users.user.mine.semgrep.tests.tainting_rules.java."
+    "Users.user.mine.semgrep.tests.tainting_rules.js."
+    "Users.user.mine.semgrep.tests.tainting_rules.php."
+    "Users.user.mine.semgrep.tests.tainting_rules.python."
+    "Users.user.mine.semgrep.tests.tainting_rules.ruby."
+    "Users.user.mine.semgrep.tests.tainting_rules.scala."
+    "Users.user.mine.semgrep.tests.tainting_rules.ts."
+    "Users.user.mine.semgrep.tests.tainting_rules."
+    "tests.tainting_rules.dart."
+    "tests.tainting_rules.go."
+    "tests.tainting_rules.java."
+    "tests.tainting_rules.js."
+    "tests.tainting_rules.php."
+    "tests.tainting_rules.python."
+    "tests.tainting_rules.ruby."
+    "tests.tainting_rules.scala."
+    "tests.tainting_rules.ts."
+    "tests.tainting_rules."
     "tests.rules."))
 
 (def (strip-known-check-id-prefix id)
diff --git a/tests/smoke.ss b/tests/smoke.ss
index e890161..105cb0c 100644
--- a/tests/smoke.ss
+++ b/tests/smoke.ss
@@ -4127,6 +4127,18 @@
             "function f() {\n  var config = taint;\n  config = ok;\n  sink(config);\n}\nfunction g() {\n  var config = [taint];\n  config = [process.env.FOO];\n  sink(config);\n}\nfunction h() {\n  var config = { foo: taint };\n  config = { foo: process.env.FOO };\n  sink(config);\n}\n")])
     (check (length findings) => 0)))
 
+(test-case "scan JavaScript taint ignores unreachable after throw"
+  (let* ([taint-config
+          "rules:\n  - id: demo.taint.js.unreachable\n    mode: taint\n    languages: [javascript]\n    message: unreachable taint\n    severity: WARNING\n    pattern-sources:\n      - pattern: source\n    pattern-sinks:\n      - pattern: sink(...)\n"]
+         [findings
+          (scan-config-string
+            taint-config
+            "javascript"
+            "demo.js"
+            "if (random()) {\n  sink(source);\n} else {\n  throw \"error\";\n  sink(source);\n}\n")])
+    (check (length findings) => 1)
+    (check (finding-start-line (car findings)) => 2)))
+
 (test-case "scan JavaScript taint object destructuring assignment"
   (let* ([taint-config
           "rules:\n  - id: demo.taint.js.destructure\n    mode: taint\n    languages: [javascript]\n    message: destructured taint\n    severity: WARNING\n    pattern-sources:\n      - pattern: user.dataValues\n    pattern-sinks:\n      - pattern: sink(...)\n"]