Skip to content

fix: prevent panic on type switch bindings in anonymous functions - #534

Open
Thanatos9404 wants to merge 3 commits into
uber-go:mainfrom
Thanatos9404:fix/anonymousfunc-type-switch-panic
Open

Thanatos9404 wants to merge 3 commits into
uber-go:mainfrom
Thanatos9404:fix/anonymousfunc-type-switch-panic

Conversation

@Thanatos9404

@Thanatos9404 Thanatos9404 commented Oct 19, 2026 •

Copy link
Copy Markdown

Fixes #531

With -experimental-anonymous-function, a type switch binding inside a function literal (switch n := v.(type)) makes the closure collection panic, which hides every other diagnostic in the package. The header n has a nil Defs entry, and each case clause declares its own implicit variable.

Changes:

  • collectClosure skips the header identifier, next to the blank identifier check from fix: prevent panic with blank identifiers in anonymous functions (cleaned-up replacement PR)Β #510.
  • A variable now belongs to the literal when its scope chain reaches the literal's scope (isDeclaredIn). The old scope.Lookup check treated the case clause variables as captured, which caused a second panic.
  • A second commit fixes the 7 self-check findings (make nilaway-lint) that this fix exposes, without changing behaviour.

Behaviour change: variables declared in nested blocks of a literal (if, for) no longer become fake parameters. On 16 standard library packages and x/tools nilness, only the wording of crypto/x509 diagnostics changes, plus html/template_test, where a dependency that used to panic is now analyzed.

Testing: new cases in the assertion/anonymousfunc testdata and in testdata/src/anonymousfunction. TestClosureCollection and TestAnonymousFunction fail on main and pass with this change.

Still open: a binding declared outside a literal and used inside it panics on a separate path (GetDeclaringIdent); I can follow up.

With -experimental-anonymous-function, a type switch with a binding
inside a function literal (`switch n := v.(type)`) made the closure
collection panic, which also hid every other diagnostic in the package.

The binding in the switch header does not denote a variable by itself:
go/types records a nil object for it in Defs and declares a separate
implicit variable in each case clause instead. Skip the header
identifier, and decide whether a variable belongs to the function
literal by walking its scope chain instead of looking it up in the
function scope only. Otherwise the implicit variables of the case
clauses, which live in nested scopes, are collected as closure
variables and passed as extra arguments at the call site, which then
panics in the function analysis.

As a result, variables declared in other nested scopes of a function
literal (for example in an `if` or `for` statement) are no longer
collected as closure variables either. This changes some results for
the better: a nil flow into such a variable is now reported from the
variable itself instead of through a fake parameter, and a variadic
function literal with such a variable no longer hits the panic from
issue uber-go#532.

Fixes uber-go#531
The type switch panic fixed in the previous commit also hit several
NilAway packages in `make nilaway-lint`, and the resulting INTERNAL
ERROR was filtered out. Now that those packages are analyzed again, the
self-check reports 7 findings. None of the changes below alter NilAway's
behaviour:

* Call `record` and `markUnstable` from `ast.Preorder` loops instead of
  `ast.Inspect` callbacks (the callbacks always returned true), and turn
  `appendTypeToTypeTriggers` into a method. The anonymous function
  analysis does not pass the variables captured by a function literal
  through another function literal that calls it, so it reported them
  as unassigned.
* Use `slices.Reverse` in `SplitFieldChain`.
* Return early in `requireComparators` if `actualExpr` is nil, which
  cannot happen once `expectedExprValue` is known.
* Check that the base is not nil in `TestSplitFieldChain` before using
  it.
@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Review in Change Stack β†’

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f0246270-3ec2-45fa-bce9-b2f42c7e4784
Commits

Reviewing files that changed from the base of the PR and between 2c45c17 and 6ff1a0e.

Files selected for processing: 1
  • hook/split_blocks_on.go

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Summary by CodeRabbit

  • Bug Fixes
    • Improved analysis of variables captured by anonymous functions, including declarations in nested scopes and type-switch bindings. Diagnostics involving these variables are now handled more accurately, including cases with shadowing and nested function literals.
  • Tests
    • Expanded coverage for anonymous functions, variadic functions, nested local variables, type switches, and nil-flow scenarios. This includes cases that check diagnostic handling when variables are captured or declared within nested scopes.

Walkthrough

Closure analysis now treats declarations in nested scopes as local to a function literal and skips type-switch header identifiers without definition entries. Added tests cover closure collection and anonymous-function diagnostics. Other changes centralize affiliation trigger appending, use preorder traversal in two function analyses, skip comparator generation when the actual expression is missing, and use slices.Reverse for field-chain segments.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix Β· Severity of issue fixed: Medium

Merge Risk: Minimal Β· up to 6ff1a

No concrete merge-blocking risk remains in the reviewed change.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely identifies the main change: preventing a panic caused by type-switch bindings in anonymous functions.
Description check Passed The description directly explains the panic, the cause, the implemented fixes, testing, behavior changes, and remaining scope.
Linked Issues check Passed The implementation satisfies the coding requirement in [#531]. collectClosure skips a type-switch header identifier when TypesInfo.Defs contains a nil entry. isDeclaredIn recognizes variables de…
Out of Scope Changes check Passed The changes remain within the scope of [#531]. Closure-analysis updates and anonymous-function fixtures implement and verify the fix. The self-check changes, traversal updates, nil guard, `slices.Reve…
Full details: Linked Issues check

Explanation

The implementation satisfies the coding requirement in [#531]. collectClosure skips a type-switch header identifier when TypesInfo.Defs contains a nil entry. isDeclaredIn recognizes variables declared in the function literal and nested scopes. The focused test asserts that closure analysis returns no recovered panic, and the new type-switch fixture covers the issue reproducer. The separate outer-binding panic is not required by [#531].

Full details: Out of Scope Changes check

Explanation

The changes remain within the scope of [#531]. Closure-analysis updates and anonymous-function fixtures implement and verify the fix. The self-check changes, traversal updates, nil guard, slices.Reverse change, and test assertion support failures exposed by this work. No unrelated feature or public API change is shown in the supplied change summary.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

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

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@ccuetoh ccuetoh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hi @Thanatos9404! Thank you very much for your contribution and for taking this fix. I have only one small nit, everything else looks all right!

Please note that you need to sign the CLA before the changes can be merged. See the CLAassistant bot comment.

Comment thread hook/split_blocks_on.go Outdated

// likely represents a case that we don't support.
if expectedExprValue == _unknown {
if expectedExprValue == _unknown || actualExpr == nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

actualExpr here cannot be nil or am I missing something?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

You're right, it can't be nil there: every branch that sets expectedExprValue also sets actualExpr. The check is only for make nilaway-lint. This literal has a type switch with a binding, so before the fix the self-check panicked on it and that error was filtered out; now it gets analyzed, and since NilAway doesn't relate the two variables it reports actualExpr as possibly nil once it reaches hook.go:128. Happy to add a short comment saying that, or handle it differently if you prefer.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ah, understood. Let's please add a comment here since this is non-functional code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 6ff1a0e: I moved the actualExpr nil check into its own if and added a comment saying it is only there for the self-check.

Split the actualExpr nil check from the unsupported-case check and add a
comment: actualExpr is always set together with expectedExprValue, so the
check only exists for NilAway's self-check, which does not relate the two
variables.
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.

Anonymous-function analysis panics on a type switch binding inside a function literal

3 participants