Merge: per-method scope-aware taint reachability

ober

62e1509c27cd5d2f5a804c38b088577e5c254454

diff --git a/HANDOFF_OPUS_4_8.md b/HANDOFF_OPUS_4_8.md
index b334e14..190a5a3 100644
--- a/HANDOFF_OPUS_4_8.md
+++ b/HANDOFF_OPUS_4_8.md
@@ -193,20 +193,21 @@ languages, so closing them is the tip of the larger work:
    `java-iterator-missed-propagation`, and `documentbuilderfactory` overfit clauses
    (java 42/42, smoke 321/321 each step).
 
-   **The next fundamental blocker is SCOPE-AWARE REACHABILITY (verified).** With
-   param-sources matching, `taint_assume_safe_numbers1` still fails because the
-   general engine's source→sink reachability is *positional/global*, not
-   per-method: every method's `$X` param reaches every `sink(...)`, so test1's
-   sink is (wrongly) flagged by test3-10's params. Confirmed empirically — a
-   numeric-typed-source filter (`source-state-numeric-typed-param?`, classifies
-   `int`/`long`/`Integer`… correctly) drops the right sources but findings persist
-   because cross-method non-numeric sources still reach. So the order is: (1)
-   per-method (CFG/scope) reachability, THEN (2) numeric/boolean source+value
-   type-classification, THEN (3) numeric-result value-analysis (`Integer.valueOf`,
-   `+`, `!=`). Each is real dataflow/type-inference work; together they're a
-   multi-day engine, and the `test` rule-id group needs ALL of them + best-fit-sink,
-   getter/setter, lambda, jpa, FQN before its overfit clause retires. The same
-   engine retires the other languages' taint handlers + unblocks dart.
+   **(1) Per-method scope-aware reachability — BUILT.** The general engine's
+   reachability was positional/global (every method's `$X` param reached every
+   `sink(...)`). Root cause: `finding-simple-function-scope` scanned upward for a
+   lower-indent function line, so a finding ON a signature line (a parameter)
+   returned no scope and `same-simple-function-scope?` bypassed the constraint.
+   Fix: a finding whose own line is a function-scope line takes that line as its
+   scope, so a parameter is scoped to its own method. Non-regressing (all
+   structural langs unchanged, smoke 321/321). This unblocks the param-source chain.
+   STILL TODO for assume-safe: (2) numeric/boolean source+value type-classification
+   (helpers were prototyped: `source-state-numeric-typed-param?` classifies
+   `int`/`long`/`Integer`… correctly via `param-type-before-name`), THEN (3)
+   numeric-result value-analysis (`Integer.valueOf`, `+`, `!=`). The `test` rule-id
+   group needs (2)+(3) + best-fit-sink, getter/setter, lambda, jpa, FQN before its
+   overfit clause retires. The same engine retires the other languages' taint
+   handlers + unblocks dart.
    - a few per-language pattern hard-cases (go constant-folding/struct_tags, php
      metavar-call/non-prim-type, rust macro-call, kotlin named-ellipsis).
 
diff --git a/lib/semgrep/scan.sls b/lib/semgrep/scan.sls
index ce2dff2..d348915 100644
--- a/lib/semgrep/scan.sls
+++ b/lib/semgrep/scan.sls
@@ -31850,18 +31850,20 @@
        (let* ([line (finding-start-line finding)]
               [target-line (source-line source line)]
               [target-indent (line-indent target-line)])
-         (if (= target-indent 0)
-             (and (simple-function-scope-line? target-line) line)
-             (let loop ([current-line (- line 1)])
-               (cond
-                 [(< current-line 1) #f]
-                 [else
-                  (let* ([text (source-line source current-line)]
-                         [indent (line-indent text)])
-                    (if (and (< indent target-indent)
-                             (simple-function-scope-line? text))
-                        current-line
-                        (loop (- current-line 1))))])))))
+         (cond
+           [(simple-function-scope-line? target-line) line]
+           [(= target-indent 0) #f]
+           [else
+            (let loop ([current-line (- line 1)])
+              (cond
+                [(< current-line 1) #f]
+                [else
+                 (let* ([text (source-line source current-line)]
+                        [indent (line-indent text)])
+                   (if (and (< indent target-indent)
+                            (simple-function-scope-line? text))
+                       current-line
+                       (loop (- current-line 1))))]))])))
   (def (same-simple-function-scope? source a b)
        (let ([a-scope (finding-simple-function-scope source a)]
              [b-scope (finding-simple-function-scope source b)])
diff --git a/src/semgrep/scan.ss b/src/semgrep/scan.ss
index a9cc6d3..953ff74 100644
--- a/src/semgrep/scan.ss
+++ b/src/semgrep/scan.ss
@@ -31800,9 +31800,14 @@
     (let* ([line (finding-start-line finding)]
            [target-line (source-line source line)]
            [target-indent (line-indent target-line)])
-      (if (= target-indent 0)
-        (and (simple-function-scope-line? target-line) line)
-        (let loop ([current-line (- line 1)])
+      (cond
+        ;; The finding is ON a function signature line (e.g. a method parameter):
+        ;; that line is its scope. Without this a parameter source has no scope
+        ;; and reaches sinks in every method (cross-method false positives).
+        [(simple-function-scope-line? target-line) line]
+        [(= target-indent 0) #f]
+        [else
+         (let loop ([current-line (- line 1)])
           (cond
             [(< current-line 1) #f]
             [else
@@ -31811,7 +31816,7 @@
                (if (and (< indent target-indent)
                         (simple-function-scope-line? text))
                    current-line
-                   (loop (- current-line 1))))])))))
+                   (loop (- current-line 1))))]))])))
 
 (def (same-simple-function-scope? source a b)
   (let ([a-scope (finding-simple-function-scope source a)]