Skip to content

fix(linter/no-eval): resolve this binding for functions returned from an IIFE#23643

Draft
shulaoda wants to merge 1 commit into
oxc-project:mainfrom
shulaoda:06-19-fix_is_callee_closure_shadowing
Draft

fix(linter/no-eval): resolve this binding for functions returned from an IIFE#23643
shulaoda wants to merge 1 commit into
oxc-project:mainfrom
shulaoda:06-19-fix_is_callee_closure_shadowing

Conversation

@shulaoda

@shulaoda shulaoda commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

What

is_callee (in ast_util.rs) is used only by is_default_this_binding, which is used only by the no-eval rule to decide whether this in this.eval(...) refers to the global object.

Two coupled bugs in that path are fixed here:

  1. is_callee always returned false. Its closure parameter was named node, shadowing the outer node, so node.kind().span() referred to the parent CallExpression rather than the node under test. callee.span().contains_inclusive(...) then compared the callee span against the whole call-expression span — impossible — so it never reported an actual callee.

  2. The traversal in is_default_this_binding never stepped above the call. After the is_callee check, the loop did current_node = parent (the ReturnStatement / arrow) instead of continuing from above the call site (ESLint does node = func.parent / node = parent.parent), so it always bottomed out at return true. This masked fix (1) entirely.

Effect

With both fixed, no-eval no longer false-positives on a function returned from an immediately-invoked function and assigned to a member, which matches ESLint:

// foo.cjs, { allowIndirect: false }
obj.foo = (function() { return function() { this.eval('foo'); }; })()

The returned function's this is bound to obj, not the global object, so this.eval is not indirect eval. Previously this was reported; now it is not.

Test

Added a no-eval pass case for the above. The whole no-eval suite still passes. (A buggy is_callee falls through the Some(func) if is_callee(...) guard to return true, re-flagging the case, so the rule test covers both fixes.)

@shulaoda shulaoda requested a review from camc314 as a code owner June 19, 2026 11:27
@codspeed-hq

codspeed-hq Bot commented Jun 19, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 5 untouched benchmarks
⏩ 66 skipped benchmarks1


Comparing shulaoda:06-19-fix_is_callee_closure_shadowing (7c7de6d) with main (eb8bedc)

Open in CodSpeed

Footnotes

  1. 66 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@camc314 camc314 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we add a test case via a rule for this on?

how is this function used?

@shulaoda shulaoda marked this pull request as draft June 19, 2026 12:02
@shulaoda shulaoda force-pushed the 06-19-fix_is_callee_closure_shadowing branch from 237e84e to 7c7de6d Compare June 19, 2026 12:17
@shulaoda shulaoda changed the title fix(linter): correct closure shadowing that made is_callee always return false fix(linter/no-eval): resolve this binding for functions returned from an IIFE Jun 19, 2026
@shulaoda

Copy link
Copy Markdown
Contributor Author

Good call — done, and it pointed to a better fix.

is_callee is used only by is_default_this_binding (in ast_util.rs), which is used only by the no-eval rule (to decide whether this in this.eval(...) is the global object).

The isolated is_callee fix turned out to have no observable effect, because is_default_this_binding had a second bug right next to it: after the is_callee check it did current_node = parent instead of stepping above the call (ESLint does node = func.parent / node = parent.parent), so it always bottomed out at return true. Fixing only one of the two does nothing.

I've fixed both and added a rule test. For example, under { allowIndirect: false } in a CommonJS file:

obj.foo = (function() { return function() { this.eval('foo'); }; })()

is now correctly not reported (the returned function's this is bound to obj, matching ESLint), whereas before it was a false positive. The full no-eval suite still passes.

I've updated the PR title/description accordingly.

@camc314 camc314 added the A-linter Area - Linter label Jun 19, 2026
@camc314 camc314 self-assigned this Jun 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-linter Area - Linter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants