-
Notifications
You must be signed in to change notification settings - Fork 444
Standardize two linters on Cursor traversal and add shared astutil.Root
#42719
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -103,6 +103,16 @@ func Inspector(pass *analysis.Pass) (*inspector.Inspector, error) { | |
| return insp, nil | ||
| } | ||
|
|
||
| // 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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/codebase-design] 💡 ContextThe helper provides real value by centralising the error-handling pattern and giving call sites a single import. However, several existing linters ( // 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 @copilot please address this. |
||
| insp, err := Inspector(pass) | ||
| if err != nil { | ||
| return inspector.Cursor{}, err | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Weaker error sentinel for value-type return: returning 💡 Detail
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. |
||
| return insp.Root(), nil | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] 💡 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 @copilot please address this. |
||
| } | ||
|
|
||
| // NodeText formats node as Go source text using go/printer. | ||
| func NodeText(fset *token.FileSet, node ast.Node) string { | ||
| var buf bytes.Buffer | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,47 +25,45 @@ var Analyzer = &analysis.Analyzer{ | |
| } | ||
|
|
||
| func run(pass *analysis.Pass) (any, error) { | ||
| insp, err := astutil.Inspector(pass) | ||
| root, err := astutil.Root(pass) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| noLintLinesByFile := nolint.BuildLineIndex(pass, "sortslice") | ||
|
|
||
| nodeFilter := []ast.Node{(*ast.CallExpr)(nil)} | ||
|
|
||
| insp.Preorder(nodeFilter, func(n ast.Node) { | ||
| call, ok := n.(*ast.CallExpr) | ||
| for cur := range root.Preorder((*ast.CallExpr)(nil)) { | ||
| call, ok := cur.Node().(*ast.CallExpr) | ||
| if !ok { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dead type-assertion guard: 💡 Detail
The 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/codebase-design] The 💡 Suggested simplificationReplace: call, ok := cur.Node().(*ast.CallExpr)
if !ok {
continue
}With: call := cur.Node().(*ast.CallExpr)The node-type filter passed to @copilot please address this. |
||
| return | ||
| continue | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The 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 @copilot please address this. |
||
| } | ||
|
|
||
| pos := pass.Fset.PositionFor(call.Pos(), false) | ||
| if filecheck.IsTestFile(pos.Filename) { | ||
| return | ||
| continue | ||
| } | ||
| if nolint.HasDirective(pos, noLintLinesByFile) { | ||
| return | ||
| continue | ||
| } | ||
|
|
||
| sel, ok := call.Fun.(*ast.SelectorExpr) | ||
| if !ok { | ||
| return | ||
| continue | ||
| } | ||
| pkgIdent, ok := sel.X.(*ast.Ident) | ||
| if !ok { | ||
| return | ||
| continue | ||
| } | ||
| if pass.TypesInfo == nil { | ||
| return | ||
| continue | ||
| } | ||
| obj := pass.TypesInfo.ObjectOf(pkgIdent) | ||
| // ObjectOf can be nil when type information is incomplete. | ||
| if obj == nil { | ||
| return | ||
| continue | ||
| } | ||
| pkgName, ok := obj.(*types.PkgName) | ||
| if !ok || pkgName.Imported().Path() != "sort" { | ||
| return | ||
| continue | ||
| } | ||
|
|
||
| switch sel.Sel.Name { | ||
|
|
@@ -75,7 +73,7 @@ func run(pass *analysis.Pass) (any, error) { | |
| case "SliceStable": | ||
| pass.ReportRangef(call, "sort.SliceStable is not type-safe; use slices.SortStableFunc instead") | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| return nil, nil | ||
| } | ||
There was a problem hiding this comment.
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:
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 needingWithStackor other*inspector.Inspectormethods still needsInspector()directly, soRoot()can't be universally adopted anyway.Options:
Root()and keep one convention — callers writeinsp.Root()themselves (one extra call, zero ambiguity).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.