Skip to content

Standardize two linters on Cursor traversal and add shared astutil.Root#42719

Merged
pelikhan merged 2 commits into
mainfrom
copilot/go-fan-go-module-review
Jul 1, 2026
Merged

Standardize two linters on Cursor traversal and add shared astutil.Root#42719
pelikhan merged 2 commits into
mainfrom
copilot/go-fan-go-module-review

Conversation

Copilot AI commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

This change continues the x/tools alignment work by moving selected analyzers off legacy Inspector.Preorder callbacks to the recommended Cursor traversal style. It also introduces a shared helper so Cursor-based analyzers use one extraction/error path.

  • Summary

    • Added a shared astutil.Root(pass) helper returning inspector.Cursor.
    • Migrated sortslice and httpstatuscode from callback-based traversal to Cursor iteration, preserving existing diagnostic behavior.
  • Changes

    • Shared AST helper
      • pkg/linters/internal/astutil/astutil.go
      • Added:
        func Root(pass *analysis.Pass) (inspector.Cursor, error) {
            insp, err := Inspector(pass)
            if err != nil {
                return inspector.Cursor{}, err
            }
            return insp.Root(), nil
        }
    • Linter traversal migrations
      • pkg/linters/sortslice/sortslice.go
        • Replaced insp.Preorder(nodeFilter, func(n ast.Node) {...}) with for cur := range root.Preorder((*ast.CallExpr)(nil)) { ... }
      • pkg/linters/httpstatuscode/httpstatuscode.go
        • Replaced mixed-node callback traversal with:
          for cur := range root.Preorder((*ast.BinaryExpr)(nil), (*ast.SwitchStmt)(nil)) {
              switch node := cur.Node().(type) { ... }
          }
  • Representative pattern

    root, err := astutil.Root(pass)
    if err != nil {
        return nil, err
    }
    
    for cur := range root.Preorder((*ast.CallExpr)(nil)) {
        call, ok := cur.Node().(*ast.CallExpr)
        if !ok {
            continue
        }
        // existing analysis/reporting logic unchanged
    }

Copilot AI linked an issue Jul 1, 2026 that may be closed by this pull request
4 tasks
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Review Go module golang.org/x/tools integration Standardize two linters on Cursor traversal and add shared astutil.Root Jul 1, 2026
Copilot AI requested a review from pelikhan July 1, 2026 12:11
@pelikhan pelikhan marked this pull request as ready for review July 1, 2026 12:13
Copilot AI review requested due to automatic review settings July 1, 2026 12:13

Copilot AI 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.

Pull request overview

This PR continues the repository’s x/tools modernization by moving two Go analyzers from legacy Inspector.Preorder callback traversal to the newer Cursor-based traversal API, and centralizes the common “get inspector root cursor” logic behind a shared helper.

Changes:

  • Added astutil.Root(pass) to return an inspector.Cursor with a shared error path for missing/invalid inspect results.
  • Migrated sortslice to iterate root.Preorder((*ast.CallExpr)(nil)) while preserving existing filtering and diagnostics.
  • Migrated httpstatuscode to iterate root.Preorder((*ast.BinaryExpr)(nil), (*ast.SwitchStmt)(nil)) while preserving existing diagnostic behavior.
Show a summary per file
File Description
pkg/linters/internal/astutil/astutil.go Adds shared Root(*analysis.Pass) helper that returns the inspector root cursor (via existing Inspector(pass) extraction).
pkg/linters/sortslice/sortslice.go Replaces callback-based traversal with cursor iteration over *ast.CallExpr nodes.
pkg/linters/httpstatuscode/httpstatuscode.go Replaces callback-based traversal with cursor iteration over *ast.BinaryExpr and *ast.SwitchStmt nodes.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Low

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #42719 does not have the 'implementation' label and has only 30 new lines of code in business logic directories (threshold: 100).

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

No test files were added or modified in this PR. Test Quality Sentinel skipped.

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions github-actions Bot mentioned this pull request Jul 1, 2026

@github-actions github-actions Bot 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.

Non-blocking observations

The Cursor migration is mechanically correct — no behavioral regressions identified. All continue/return replacements are semantically equivalent, and both linters preserve their diagnostic logic.

Findings (non-blocking)

1. Dead type-assertion guard in sortslice.gocur.Node().(*ast.CallExpr) after Preorder((*ast.CallExpr)(nil)) is guaranteed to succeed; the !ok branch is unreachable dead code copied mechanically from the old callback style. Use the single-value assertion form.

2. Zero-value inspector.Cursor{} on error in Root — Unlike the nil-pointer returned by Inspector, a zero Cursor may silently produce zero iterations when the error is accidentally ignored, masking bugs rather than causing an obvious panic. Minimal godoc addition or a panic on unexpected type would keep the fail-fast contract.

🔎 Code quality review by PR Code Quality Reviewer · 87.9 AIC · ⌖ 9.81 AIC · ⊞ 1.6K
Comment /review to run again

call, ok := n.(*ast.CallExpr)
for cur := range root.Preorder((*ast.CallExpr)(nil)) {
call, ok := cur.Node().(*ast.CallExpr)
if !ok {

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.

Dead type-assertion guard: cur.Node().(*ast.CallExpr) always succeeds here and the !ok branch is unreachable dead code.

💡 Detail

root.Preorder((*ast.CallExpr)(nil)) passes *ast.CallExpr as the sole filter type, so the inspector package guarantees every yielded cursor holds exactly a *ast.CallExpr. The two-value form call, ok := cur.Node().(*ast.CallExpr) will therefore always set ok = true.

The if !ok { continue } guard is dead code — a mechanical carry-over from the old callback style.

Recommended fix:

// Single-value assertion is safe — guaranteed by Preorder filter:
call := cur.Node().(*ast.CallExpr)

Leaving the dead branch misleads reviewers into thinking it is reachable, and silently becomes a masked bug if the filter type is later changed without updating the assertion.

insp, err := Inspector(pass)
if err != nil {
return inspector.Cursor{}, err
}

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.

Weaker error sentinel for value-type return: returning inspector.Cursor{} on error is harder to detect than the nil pointer returned by Inspector.

💡 Detail

Inspector returns (*inspector.Inspector, error); a caller that ignores the error gets a nil pointer that will immediately panic on first use — a loud, fast failure.

Root returns (inspector.Cursor, error); a caller that ignores the error gets a zero inspector.Cursor{}. Depending on the inspector library's behaviour for the zero value, this will either silently produce zero iterations (masking the bug entirely) or panic with a less diagnostic stack trace.

Consider documenting the zero-value behaviour explicitly, or panicking eagerly on error to match the fail-fast contract of the surrounding helpers:

// Option A — document the risk in the godoc:
// If the error is non-nil, the returned Cursor is the zero value and
// must not be used; iterating over it will produce zero results or panic.

// Option B — keep only Inspector exposed publicly and let callers call .Root() themselves,
// preserving the nil-pointer sentinel for the error path.


// Root extracts the inspector root cursor from pass.ResultOf.
// It returns an error if the inspect result has an unexpected type.
func Root(pass *analysis.Pass) (inspector.Cursor, error) {

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.

Convention fork with no migration plan: Root() creates a second pattern for obtaining an AST cursor alongside the one already used by all 9 existing Cursor-style linters.

💡 Detail

All 9 other linters that already use Cursor traversal do this:

insp, err := astutil.Inspector(pass)
if err != nil { return nil, err }
// ...
for cur := range insp.Root().Preorder(...) { ... }

This PR introduces Root() and migrates exactly 2 linters to it, leaving 9 linters on the old pattern. The codebase now has two conventions for the same one-liner with no documented preference.

The helper also doesn't fully replace Inspector(): any linter needing WithStack or other *inspector.Inspector methods still needs Inspector() directly, so Root() can't be universally adopted anyway.

Options:

  1. Remove Root() and keep one convention — callers write insp.Root() themselves (one extra call, zero ambiguity).
  2. Migrate all 11 linters to Root() in this PR so the new convention is consistent from the start.

Option 1 is lower risk. Option 2 requires touching 9 more files.

@github-actions github-actions Bot 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.

Skills-Based Review 🧠

Applied /codebase-design and /tdd — no blocking issues, three targeted suggestions.

📋 Key Themes & Highlights

Key Themes

  1. Dead-code guard in sortslice: cur.Node().(*ast.CallExpr) is filtered by Preorder, so the !ok branch can never trigger — it should use a plain (non-ok) assertion.
  2. Incomplete pattern migration: astutil.Root is a good helper, but four existing linters still call astutil.Inspector(pass) + .Root() directly (deferinloop, timesleepnocontext, execcommandwithoutcontext, panic-in-library-code). Two coexisting patterns reduce navigability.
  3. Missing unit test for Root: All other astutil helpers have direct unit tests; Root's error path is only implicitly covered by analyser-level tests.

Positive Highlights

  • Clean migration from callback to iterator style — the return to continue mapping is correct throughout.
  • astutil.Root doc comment is clear and matches the style of Inspector.
  • Net line reduction while improving readability — good trade-off.
  • No diagnostic behaviour changed; existing analysistest test data validates correctness.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 70.5 AIC · ⌖ 6.89 AIC · ⊞ 6.6K
Comment /matt to run again

call, ok := n.(*ast.CallExpr)
for cur := range root.Preorder((*ast.CallExpr)(nil)) {
call, ok := cur.Node().(*ast.CallExpr)
if !ok {

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.

[/codebase-design] The !ok guard is dead code — Preorder((*ast.CallExpr)(nil)) guarantees only *ast.CallExpr nodes are yielded, so the type assertion will never fail.

💡 Suggested simplification

Replace:

call, ok := cur.Node().(*ast.CallExpr)
if !ok {
    continue
}

With:

call := cur.Node().(*ast.CallExpr)

The node-type filter passed to Preorder is a compile-time guarantee that cur.Node() returns exactly the filtered type. Keeping the ok branch misleads readers into thinking the assertion could fail. The same pattern applies in httpstatuscode.go for the cur.Node() type-switch — though there the switch default is simply a no-op, not dead code per se.

@copilot please address this.


// Root extracts the inspector root cursor from pass.ResultOf.
// It returns an error if the inspect result has an unexpected type.
func Root(pass *analysis.Pass) (inspector.Cursor, error) {

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.

[/codebase-design] Root is a thin two-liner that delegates straight to Inspector + .Root(). Consider whether this helper justifies its own named function in the public surface.

💡 Context

The helper provides real value by centralising the error-handling pattern and giving call sites a single import. However, several existing linters (deferinloop, timesleepnocontext, execcommandwithoutcontext, panic-in-library-code) already call astutil.Inspector(pass) and then chain .Root() directly — they were not migrated in this PR. As a result, two equivalent patterns now coexist in the codebase:

// pattern A (existing)
insp, err := astutil.Inspector(pass)
for cur := range insp.Root().Preorder(...) {

// pattern B (new, this PR)
root, err := astutil.Root(pass)
for cur := range root.Preorder(...) {

To make Root worthwhile, the remaining linters should be migrated too — or a note should be added to CONTEXT.md / the PR description explaining that follow-up migration is planned.

@copilot please address this.

if err != nil {
return inspector.Cursor{}, err
}
return insp.Root(), nil

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.

[/tdd] astutil.Root has no direct unit test — only Inspector is implicitly exercised through analyzer tests. An explicit unit test would lock in the error-path behaviour.

💡 Suggested test (alongside existing `TestIsPkgSelector`)
func TestRoot_InvalidResult(t *testing.T) {
    t.Parallel()
    pass := &analysis.Pass{
        ResultOf: map[*analysis.Analyzer]any{
            inspect.Analyzer: "not an inspector",
        },
    }
    _, err := Root(pass)
    if err == nil {
        t.Fatal("expected error for unexpected result type, got nil")
    }
}

This mirrors the existing pattern for Inspector and ensures the error message is preserved through the delegation layer.

@copilot please address this.

@pelikhan

pelikhan commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

@github-actions github-actions Bot 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.

Review: Standardize two linters on Cursor traversal and add shared astutil.Root

Overall this is a clean, well-executed refactor. The astutil.Root() helper is a natural extension of astutil.Inspector() and the migration from callback-style Preorder to range-based Cursor iteration is correct for both linters.

One non-blocking issue worth cleaning up:

Dead code guard in sortslice.go (and pre-existing in others)

call, ok := cur.Node().(*ast.CallExpr)
if !ok {
    continue
}

When iterating with root.Preorder((*ast.CallExpr)(nil)), the type filter guarantees every yielded cursor holds exactly an *ast.CallExpr — the !ok branch is never reachable. The same dead-code guard was copied from the pre-existing timeafterleak.go / deferinloop.go callers. Simplifying to a direct assertion would be cleaner and signals to the reader that the filter provides that guarantee.

Other observations
  • httpstatuscode.go: The switch node := cur.Node().(type) idiom is correct here because two node types are filtered — no dead code.
  • Existing callers like timeafterleak, ctxbackground, deferinloop, and httpnoctx still use the insp.Root().Preorder() inline pattern rather than astutil.Root(). A follow-up PR to migrate those would make the codebase fully consistent.
  • No test for astutil.Root(), but this matches the existing coverage posture for astutil.Inspector().

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • proxy.golang.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "proxy.golang.org"

See Network Configuration for more information.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 81.7 AIC · ⌖ 6.99 AIC · ⊞ 4.9K

call, ok := cur.Node().(*ast.CallExpr)
if !ok {
return
continue

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.

The !ok guard (lines 35–37) is unreachable dead code: root.Preorder((*ast.CallExpr)(nil)) only ever yields *ast.CallExpr nodes, so the type assertion always succeeds and the continue is never taken.

Consider simplifying to a direct assertion:

for cur := range root.Preorder((*ast.CallExpr)(nil)) {
    call := cur.Node().(*ast.CallExpr)
    // ...
}

(The same pattern exists pre-existing in timeafterleak.go and deferinloop.go — a follow-up to clean those up would make all Cursor-based linters consistent.)

@copilot please address this.

@pelikhan pelikhan merged commit 31bc0a0 into main Jul 1, 2026
91 of 103 checks passed
@pelikhan pelikhan deleted the copilot/go-fan-go-module-review branch July 1, 2026 12:45
Copilot stopped work on behalf of pelikhan due to an error July 1, 2026 12:46
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.82.2

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.

[go-fan] Go Module Review: golang.org/x/tools

3 participants