Skip to content

fix(hir): propagate ws Client handle class across function-call boundaries - #5493

Merged
proggeramlug merged 8 commits into
PerryTS:mainfrom
machineloop:fix/ws-client-handle-scope
Jun 21, 2026
Merged

fix(hir): propagate ws Client handle class across function-call boundaries#5493
proggeramlug merged 8 commits into
PerryTS:mainfrom
machineloop:fix/ws-client-handle-scope

Conversation

@machineloop

@machineloop machineloop commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Summary

The ("ws","Client") handle delivered to a WebSocket-upgrade callback
server.on("upgrade", (req, wsId, head) => …) (raw node:http) only dispatched its
.send()/.on()/.close() to the dedicated js_ws_*_client_i64 runtime when used inline
in the callback. Passing wsId into a helper made those calls a silent no-op — the frame
was dropped with no error. This propagates the handle's host class across the call boundary so
the helper dispatches correctly.

Changes

  • crates/perry-hir/src/lower/pre_scan.rs — new pre_scan_cross_fn_native_params pass (run
    before any function body lowers). It seeds from the upgrade idiom X.on("upgrade", (req, wsId, head) => …)gated on X being a createServer(...) result (static proxy for a
    ("http","HttpServer") instance; mirrors the runtime check, avoids false positives on
    arbitrary .on("upgrade")) — and follows the handle through userFn(…, wsId, …) calls
    transitively (fixpoint), recording a (fn_name, param_index) -> (module, class) hint.
  • crates/perry-hir/src/lower/lowering_context.rs + …/lower/context.rs — add the
    param_native_hints map.
  • crates/perry-hir/src/lower/lower_module_fn.rs — invoke the pass alongside the other pre-scans.
  • crates/perry-hir/src/lower_decl/fn_decl.rs — apply the hint (only when the param isn't
    already a native instance, so an explicit annotation wins) before the body lowers.
  • crates/perry/tests/ws_client_handle_cross_function_dispatch.rs — regression tests.

Related issue

Test plan

cargo test -p perry --test ws_client_handle_cross_function_dispatch   # 4 passed
cargo fmt -p perry-hir -p perry --check                               # clean
cargo build --release --workspace --exclude perry-ui-{ios,tvos,watchos,gtk4,android,windows}

A live WS round-trip needs a socket + client and Perry's raw-upgrade wsId has no
Node-ws-compatible API to diff against, so the bug (which runtime symbol the call site lowers
to) is asserted at the LLVM-IR level. TDD red→green: for the helper-only case the IR went from
0 call @js_ws_send_client_i64 sites (silent no-op) to 1. Tests also cover transitive
forwarding, .on/.close, and a control proving a non-ws .send param keeps running its own
method (no mis-tagging).

  • cargo build --release -p perry clean (the crates this change touches). The full
    --workspace release build fails only on windows-future — a Windows-only transitive
    dep that can't compile on this macOS host (env limitation, unrelated to this change).
  • cargo test --workspace … passes — my new tests pass; the suite's only failures
    (integer_locals_provenance, functional_batch2_regressions, and a
    blocklist_addsubnet_prefix TempDir::join compile break) reproduce identically on
    clean main without this change
    , so they are pre-existing and unrelated.
  • (if user-facing) Added a #[test] in the affected crate (crates/perry/tests/)
  • (if CLI / stdlib / runtime API changed) Updated docs/src/ — N/A (internal HIR dispatch)
  • (if touching a platform UI backend) — N/A

Screenshots / output

function handleConnection(req, wsId) { wsId.send("hi"); }   // before: dropped; after: delivered
server.on("upgrade", (req, wsId, head) => { handleConnection(req, wsId); });
// IR: helper `wsId.send` -> `call void @js_ws_send_client_i64(...)`  (0 sites before, 1 after)

Checklist

  • I have NOT bumped the workspace version or edited CLAUDE.md / CHANGELOG.md
  • My commits follow the loose fix: prefix convention
  • I've read CONTRIBUTING.md and agree to the Code of Conduct

Summary by CodeRabbit

  • Bug Fixes / New Features
    • Improved WebSocket upgrade-handle behavior so send, on, and close dispatch to the correct Client runtime even when the wsId handle is forwarded through helper call chains, including nested callbacks.
    • Added safeguards for edge cases such as parameter shadowing, polymorphic forwarding, and correct behavior when mixed with non-WebSocket objects.
  • Tests
    • Expanded the WebSocket cross-function dispatch regression suite with helper chains, negative controls, and additional scenarios for parameter indexing and createServer provenance.

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f6b65e9f-7329-4db3-a73b-6c4b51432dae

📥 Commits

Reviewing files that changed from the base of the PR and between 48ae4a2 and d67a1be.

📒 Files selected for processing (1)
  • crates/perry-hir/src/lower/pre_scan.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/perry-hir/src/lower/pre_scan.rs

📝 Walkthrough

Walkthrough

A new pre-scan pass (pre_scan_cross_fn_native_params) is added to the HIR lowering pipeline. It detects WebSocket upgrade handles bound via createServer, taints wsId-like parameters as ("ws","Client") instances, propagates that taint transitively across direct function calls using scope-aware pruning, and stores results in a new LoweringContext field (param_native_hints) keyed by function declaration span byte offset and parameter index. lower_fn_decl consumes these hints to register parameters as native instances before body lowering, ensuring wsId.send/on/close dispatch to correct LLVM runtime symbols. Comprehensive regression tests validate dispatch correctness, transitive propagation, and edge cases including parameter shadowing and HTTP import provenance.

Changes

Cross-function WS Client Handle Dispatch

Layer / File(s) Summary
LoweringContext hint map field and initialization
crates/perry-hir/src/lower/lowering_context.rs, crates/perry-hir/src/lower/context.rs
Adds param_native_hints: HashMap<(u32, usize), (String, String)> to LoweringContext, keyed by function declaration identifier span byte offset and parameter index, and initializes it to an empty map in with_class_id_start.
pre_scan_cross_fn_native_params taint propagation
crates/perry-hir/src/lower/pre_scan.rs
Implements the full pre-scan pass: discovers HTTP-imported createServer/createSecureServer bindings, locates "upgrade" handler callbacks to seed wsId taint, runs scope-aware fixpoint propagation over direct call sites with parameter-shadowing pruning and spread-argument invalidation, and writes (fn_decl_span_lo, param_index) → ("ws","Client") into ctx.param_native_hints. Includes comprehensive AST helpers for HTTP provenance detection, server-binding discovery, call collection through nested closures, shadowing detection across scopes, and guarded taint propagation with step-count recursion limits.
Pipeline wiring: pre-scan invocation and hint consumption
crates/perry-hir/src/lower/lower_module_fn.rs, crates/perry-hir/src/lower_decl/fn_decl.rs
lower_module_full calls pre_scan_cross_fn_native_params before any function-body lowering; lower_fn_decl queries (fn_decl.ident.span.lo.0, param_index) in ctx.param_native_hints and registers untyped parameters as native (module, class) instances before body lowering.
Regression tests: IR dispatch validation and edge cases
crates/perry/tests/ws_client_handle_cross_function_dispatch.rs
Adds test infrastructure (compile_to_ir, compile_and_run, ir_has_call, ir_call_count) and comprehensive tests: foundational tests validating direct/transitive forwarding and .on/.close dispatch plus negative control, four PR-review edge cases (leading this: parameter alignment, non-HTTP createServer filtering, nested scope shadowing, identity-sensitive same-named function dispatch), polymorphism guards against mixed-type forwarding, and transitive demotion correctness.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐇 A websocket handle, passed down the chain,
Once lost its true type — a dispatch in vain.
Now a pre-scan sniffs out every wsId friend,
Tracks it through helpers, from start to the end,
Taint walks the AST with scope-aware grace,
js_ws_send_client_i64 finds its place! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and accurately summarizes the main change: propagating the ws Client handle class across function-call boundaries.
Description check ✅ Passed The description includes all required template sections: Summary, Changes, Test plan with checkboxes, and a completed Checklist.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-hir/src/lower/lowering_context.rs`:
- Around line 196-204: The param_native_hints HashMap key uses only
function_name and parameter index, which is not unique when the same function
name exists in different scopes (shadowed or nested functions). Replace the
String function_name component in the (String, usize) key with a stable AST
identity such as a Span or eventually a FuncId mapping that can uniquely
identify each function declaration. This ensures that native instance hints
seeded for one function declaration in lower_fn_decl do not incorrectly apply to
unrelated declarations with the same name, preventing cross-contamination of
type hints like the ("ws","Client") dispatch mismatches.

In `@crates/perry-hir/src/lower/pre_scan.rs`:
- Around line 463-493: The `is_create_server_call` function currently accepts
any callee named `createServer` or `createSecureServer` without verifying it
originates from the Node.js HTTP modules. To fix this, you need to first scan
the module's imports to build a set of verified identifiers that are aliases or
direct references to `createServer`/`createSecureServer` from `node:http`,
`node:https`, or `node:http2` modules. Then modify `is_create_server_call` to
check whether the callee (for bare calls, the identifier; for member calls, the
object being accessed) is in this verified set of safe imports. This ensures
only actual HTTP server creation calls are detected, not user-defined factories
with the same name.
- Around line 310-315: The parameter name collection in the mapping operation
needs to skip TypeScript `this` parameters to align with how `lower_fn_decl`
handles them. In the code where `fd.function.params` is being iterated and names
are collected via `cross_fn_pat_name`, add a filter to exclude parameters where
the extracted name equals "this". This same fix should also be applied to the
similar code block mentioned at lines 390-396 to ensure consistency across all
parameter enumeration paths and prevent hint index misalignment.
- Around line 645-659: The collect_calls_in_callback_body function and related
collectors are descending into nested function/arrow scopes while maintaining
taint information from outer scopes, causing incorrect tainting when parameters
are shadowed in nested functions. Modify the collectors to not recurse into
nested function bodies (both ast::Expr::Fn and nested arrows within callback
bodies) so that taint analysis respects scope boundaries and does not inherit
outer scope bindings through shadowed parameters. This applies to
collect_calls_in_callback_body, the collectors called from it, and any other
places where nested functions are recursively analyzed without scope separation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 092b0dca-d37b-495e-8bae-e547184ec445

📥 Commits

Reviewing files that changed from the base of the PR and between d30bfa5 and 53cd19d.

📒 Files selected for processing (6)
  • crates/perry-hir/src/lower/context.rs
  • crates/perry-hir/src/lower/lower_module_fn.rs
  • crates/perry-hir/src/lower/lowering_context.rs
  • crates/perry-hir/src/lower/pre_scan.rs
  • crates/perry-hir/src/lower_decl/fn_decl.rs
  • crates/perry/tests/ws_client_handle_cross_function_dispatch.rs

Comment thread crates/perry-hir/src/lower/lowering_context.rs Outdated
Comment thread crates/perry-hir/src/lower/pre_scan.rs
Comment thread crates/perry-hir/src/lower/pre_scan.rs Outdated
Comment thread crates/perry-hir/src/lower/pre_scan.rs Outdated
@machineloop

Copy link
Copy Markdown
Contributor Author

Actionable comments posted: 4
@coderabbitai autofix stacked pr

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

This command requires write access to the repository. Only users with write or admin permissions can trigger CodeRabbit to commit or create pull requests.

machineloop added a commit to machineloop/perry that referenced this pull request Jun 21, 2026
CodeRabbit (PR PerryTS#5493): lower_fn_decl drops the `this:` type-only param before
enumerating params, but the pre-scan kept it, so the hint index and propagated
param name diverged for `function f(this: T, req, wsId)`. Skip `this` in the
pre-scan param list to match — call sites never pass it positionally.
machineloop added a commit to machineloop/perry that referenced this pull request Jun 21, 2026
CodeRabbit (PR PerryTS#5493): is_create_server_call matched any callee named
createServer/createSecureServer (user factories, unrelated obj.createServer()),
which could seed ("ws","Client") taint from a non-HTTP object. Restrict the
seed to createServer/createSecureServer that resolve to a node:http / node:https
/ node:http2 import or require alias, mirroring the runtime's HttpServer
provenance.
machineloop added a commit to machineloop/perry that referenced this pull request Jun 21, 2026
CodeRabbit (PR PerryTS#5493): param_native_hints keyed by (function_name, param_index)
let a hint seeded for a top-level helper mis-apply to an unrelated same-named
nested/redeclared function in lower_fn_decl. Key by the function declaration's
identifier span (a stable AST identity) at both the record and apply sites, so
distinct declarations get distinct keys.
machineloop added a commit to machineloop/perry that referenced this pull request Jun 21, 2026
…ugh shadowing

CodeRabbit (PR PerryTS#5493): the call collectors descended into nested function/arrow
bodies with a flat name-keyed taint set, so a nested `function later(wsId){…}`
that shadows the outer upgrade handle would mis-tag `later`'s callees. Replace
the flat collect-then-check with a scope-aware walk that drops any name a nested
scope rebinds as a parameter before recursing — preserving legitimate captures
(`wsId.on('message', m => handleConnection(req, wsId))`) while respecting
shadowing.
machineloop added a commit to machineloop/perry that referenced this pull request Jun 21, 2026
…5493)

Regression tests for the four CodeRabbit findings: a leading TS `this:` param
(index alignment), a non-http `createServer` (seed gating), a shadowing nested
param (scope-aware taint), and a same-named nested function (identity keying).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/perry-hir/src/lower/pre_scan.rs (1)

544-716: 💤 Low value

Minor gap: ast::Expr::Class and TsSatisfies not traversed.

The catch-all silently skips class expressions (which can contain method bodies) and TypeScript's satisfies wrapper. In practice, these patterns are rare inside upgrade handlers, but if a tainted handle flows through them the hint won't propagate.

Likely acceptable for this PR scope given the low probability of these patterns in real-world upgrade callbacks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-hir/src/lower/pre_scan.rs` around lines 544 - 716, The
walk_taint_expr function's match statement has a catch-all arm that silently
skips ast::Expr::Class and ast::Expr::TsSatisfies expressions, preventing taint
propagation through these patterns. Add explicit match arms before the catch-all
for these two cases: for ast::Expr::Class, traverse the class body methods
similar to how ast::Expr::Object handles method properties by pruning
parameters, and for ast::Expr::TsSatisfies, traverse the inner expression like
the other TypeScript assertion types (TsAs, TsNonNull, etc.) at the end of the
match.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/perry-hir/src/lower/pre_scan.rs`:
- Around line 544-716: The walk_taint_expr function's match statement has a
catch-all arm that silently skips ast::Expr::Class and ast::Expr::TsSatisfies
expressions, preventing taint propagation through these patterns. Add explicit
match arms before the catch-all for these two cases: for ast::Expr::Class,
traverse the class body methods similar to how ast::Expr::Object handles method
properties by pruning parameters, and for ast::Expr::TsSatisfies, traverse the
inner expression like the other TypeScript assertion types (TsAs, TsNonNull,
etc.) at the end of the match.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 23750ec6-304f-4b19-a04b-e1dad24b1a09

📥 Commits

Reviewing files that changed from the base of the PR and between 53cd19d and ae82da3.

📒 Files selected for processing (4)
  • crates/perry-hir/src/lower/lowering_context.rs
  • crates/perry-hir/src/lower/pre_scan.rs
  • crates/perry-hir/src/lower_decl/fn_decl.rs
  • crates/perry/tests/ws_client_handle_cross_function_dispatch.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/perry-hir/src/lower_decl/fn_decl.rs
  • crates/perry-hir/src/lower/lowering_context.rs

@machineloop
machineloop force-pushed the fix/ws-client-handle-scope branch from ae82da3 to 9b1e63d Compare June 21, 2026 14:43
machineloop added a commit to machineloop/perry that referenced this pull request Jun 21, 2026
CodeRabbit (PR PerryTS#5493): lower_fn_decl drops the `this:` type-only param before
enumerating params, but the pre-scan kept it, so the hint index and propagated
param name diverged for `function f(this: T, req, wsId)`. Skip `this` in the
pre-scan param list to match — call sites never pass it positionally.
machineloop added a commit to machineloop/perry that referenced this pull request Jun 21, 2026
CodeRabbit (PR PerryTS#5493): is_create_server_call matched any callee named
createServer/createSecureServer (user factories, unrelated obj.createServer()),
which could seed ("ws","Client") taint from a non-HTTP object. Restrict the
seed to createServer/createSecureServer that resolve to a node:http / node:https
/ node:http2 import or require alias, mirroring the runtime's HttpServer
provenance.
machineloop added a commit to machineloop/perry that referenced this pull request Jun 21, 2026
CodeRabbit (PR PerryTS#5493): param_native_hints keyed by (function_name, param_index)
let a hint seeded for a top-level helper mis-apply to an unrelated same-named
nested/redeclared function in lower_fn_decl. Key by the function declaration's
identifier span (a stable AST identity) at both the record and apply sites, so
distinct declarations get distinct keys.
machineloop added a commit to machineloop/perry that referenced this pull request Jun 21, 2026
…ugh shadowing

CodeRabbit (PR PerryTS#5493): the call collectors descended into nested function/arrow
bodies with a flat name-keyed taint set, so a nested `function later(wsId){…}`
that shadows the outer upgrade handle would mis-tag `later`'s callees. Replace
the flat collect-then-check with a scope-aware walk that drops any name a nested
scope rebinds as a parameter before recursing — preserving legitimate captures
(`wsId.on('message', m => handleConnection(req, wsId))`) while respecting
shadowing.
machineloop added a commit to machineloop/perry that referenced this pull request Jun 21, 2026
…5493)

Regression tests for the four CodeRabbit findings: a leading TS `this:` param
(index alignment), a non-http `createServer` (seed gating), a shadowing nested
param (scope-aware taint), and a same-named nested function (identity keying).
machineloop and others added 6 commits June 21, 2026 09:44
…aries

The `("ws","Client")` handle delivered to a WebSocket-upgrade callback
`server.on("upgrade", (req, wsId, head) => …)` only dispatched its
`.send()`/`.on()`/`.close()` to the dedicated `js_ws_*_client_i64` runtime
when codegen statically knew the receiver was the upgrade Client. That class
is tagged at the upgrade callback's parameter, but was NOT propagated when
`wsId` was passed to a helper (`handleConnection(req, wsId)`): inside the
callee the parameter is untyped, the dispatch table's `class_filter:
Some("Client")` rows no longer matched, and `wsId.send(...)` silently lowered
to a generic no-op — the frame was dropped with no error thrown.

Adds `pre_scan_cross_fn_native_params`, a pre-lowering pass that seeds from
the HTTP-upgrade idiom (gated on the receiver being a `createServer(...)`
result, mirroring the runtime `("http","HttpServer")` check) and follows the
handle through subsequent `userFn(…, wsId, …)` calls transitively, recording a
`(fn_name, param_index) -> (module, class)` hint. `lower_fn_decl` applies the
hint (only when the param isn't already a native instance, so explicit
annotations win) before the body lowers, so the callee's `wsId.send(...)`
dispatches to `js_ws_send_client_i64` exactly like the inline call.

Regression test (ws_client_handle_cross_function_dispatch) compiles to LLVM IR
and asserts the helper/transitive/.on/.close call sites lower to the
`js_ws_*_client_i64` runtime, plus a control proving a non-ws `.send` param
keeps running its own method (no mis-tagging).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CodeRabbit (PR PerryTS#5493): lower_fn_decl drops the `this:` type-only param before
enumerating params, but the pre-scan kept it, so the hint index and propagated
param name diverged for `function f(this: T, req, wsId)`. Skip `this` in the
pre-scan param list to match — call sites never pass it positionally.
CodeRabbit (PR PerryTS#5493): is_create_server_call matched any callee named
createServer/createSecureServer (user factories, unrelated obj.createServer()),
which could seed ("ws","Client") taint from a non-HTTP object. Restrict the
seed to createServer/createSecureServer that resolve to a node:http / node:https
/ node:http2 import or require alias, mirroring the runtime's HttpServer
provenance.
CodeRabbit (PR PerryTS#5493): param_native_hints keyed by (function_name, param_index)
let a hint seeded for a top-level helper mis-apply to an unrelated same-named
nested/redeclared function in lower_fn_decl. Key by the function declaration's
identifier span (a stable AST identity) at both the record and apply sites, so
distinct declarations get distinct keys.
…ugh shadowing

CodeRabbit (PR PerryTS#5493): the call collectors descended into nested function/arrow
bodies with a flat name-keyed taint set, so a nested `function later(wsId){…}`
that shadows the outer upgrade handle would mis-tag `later`'s callees. Replace
the flat collect-then-check with a scope-aware walk that drops any name a nested
scope rebinds as a parameter before recursing — preserving legitimate captures
(`wsId.on('message', m => handleConnection(req, wsId))`) while respecting
shadowing.
…5493)

Regression tests for the four CodeRabbit findings: a leading TS `this:` param
(index alignment), a non-http `createServer` (seed gating), a shadowing nested
param (scope-aware taint), and a same-named nested function (identity keying).
@machineloop
machineloop force-pushed the fix/ws-client-handle-scope branch from 9b1e63d to 9e56613 Compare June 21, 2026 14:44
Ralph Küpper and others added 2 commits June 21, 2026 17:30
… handle

The cross-fn propagation tags a function PARAMETER, which fixes the
dispatch for that param across EVERY call site of the function — not
just the upgrade-fed one. A helper reused for both the upgrade `wsId`
and a non-ws value would therefore have its non-ws caller's
`.send()/.on()/.close()` silently re-routed to `js_ws_*_client_i64`
(a handle-map miss → dropped frame, the exact bug this pass fixes) and
its own method bypassed.

Add a polymorphism guard: after the taint walk, keep a hint only when
the function is provably always handed the ws handle at that parameter.
A function with any non-ws caller (or a spread-arg caller, whose
positional mapping is unreliable) is dropped. The caller scan is
scope-aware so a nested function that SHADOWS a top-level name is not
mistaken for a call to the top-level one.

Also propagate the guard transitively: a hint reached by following the
handle THROUGH an upstream param depends on that param, so a polymorphic
intermediate demotes everything downstream of it (dependency fixpoint).

Tests: a polymorphic direct helper, a polymorphic intermediate
(transitive), and a ws-exclusive helper with multiple ws callers
(guardrail against over-correction). Also drops a stray `if` paren that
tripped an unused_parens warning.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…erryTS#5493 review)

CodeRabbit nitpick: walk_taint_expr's catch-all silently skipped ast::Expr::Class
(method bodies can carry a tainted handle) and TypeScript's `satisfies` wrapper.
Traverse class method/constructor bodies (params pruned like object methods) +
field initializers + static blocks, and unwrap TsSatisfies like the other TS
assertion wrappers.
@machineloop

Copy link
Copy Markdown
Contributor Author

Addressed the remaining nitpick (Expr::Class / TsSatisfies not traversed in walk_taint_expr) in d67a1be: class method/constructor bodies are now traversed with their params pruned (like object methods), plus field initializers and static blocks, and TsSatisfies is unwrapped like the other TS assertion wrappers. Rebased onto the latest branch head. All 11 ws-dispatch tests pass.

@proggeramlug
proggeramlug merged commit 9b9be40 into PerryTS:main Jun 21, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants