Skip to content
This repository was archived by the owner on Nov 10, 2025. It is now read-only.

Commit 69bbbfc

Browse files
committed
chore: Address review comments
Signed-off-by: Kemal Akkoyun <[email protected]>
1 parent 5f5f6a0 commit 69bbbfc

File tree

8 files changed

+220
-21
lines changed

8 files changed

+220
-21
lines changed

errorlint/analysis.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
4040
var lints []analysis.Diagnostic
4141
extInfo := newTypesInfoExt(pass)
4242
if checkComparison {
43-
l := LintErrorComparisons(extInfo)
43+
l := LintErrorComparisons(pass.Fset, extInfo)
4444
lints = append(lints, l...)
4545
}
4646
if checkAsserts {

errorlint/analysis_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,8 @@ func TestIssueFixRegressions(t *testing.T) {
5858
analyzer := NewAnalyzer()
5959
analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), analyzer, "issues/fix")
6060
}
61+
62+
func TestASTManipulationFixes(t *testing.T) {
63+
analyzer := NewAnalyzer()
64+
analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), analyzer, "astmanipulation")
65+
}

errorlint/lint.go

Lines changed: 80 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func extractComparison(cond ast.Expr) *ast.BinaryExpr {
9090

9191
func buildVarDeclaration(assertion typeAssertion) string {
9292
targetTypeStr := exprToString(assertion.targetType)
93-
if baseType, found := strings.CutPrefix(targetTypeStr, "*") ; found {
93+
if baseType, found := strings.CutPrefix(targetTypeStr, "*"); found {
9494
return fmt.Sprintf("%s := &%s{}", assertion.varName, baseType)
9595
}
9696
return fmt.Sprintf("var %s %s", assertion.varName, targetTypeStr)
@@ -133,7 +133,7 @@ func groupDiagnosticsByIfStmt(lints []analysis.Diagnostic, extInfo *TypesInfoExt
133133
continue
134134
}
135135

136-
ifStmt := containingIf(extInfo, node)
136+
ifStmt := statementContainsIf(extInfo, node)
137137
if ifStmt == nil {
138138
otherLints = append(otherLints, lint)
139139
continue
@@ -297,7 +297,7 @@ func isFmtErrorfCallExpr(info types.Info, expr ast.Expr) (*ast.CallExpr, bool) {
297297
return nil, false
298298
}
299299

300-
func LintErrorComparisons(info *TypesInfoExt) []analysis.Diagnostic {
300+
func LintErrorComparisons(fset *token.FileSet, info *TypesInfoExt) []analysis.Diagnostic {
301301
var lints []analysis.Diagnostic
302302

303303
// Check for error comparisons.
@@ -353,13 +353,20 @@ func LintErrorComparisons(info *TypesInfoExt) []analysis.Diagnostic {
353353
replacement = "!" + replacement
354354
}
355355

356+
textEdits := []analysis.TextEdit{{
357+
Pos: binExpr.Pos(),
358+
End: binExpr.End(),
359+
NewText: []byte(replacement),
360+
}}
361+
362+
if file := findContainingFile(info, binExpr); file != nil && needsErrorsImport(file) {
363+
importFix := createImportFix(file)
364+
textEdits = append(textEdits, importFix.TextEdits...)
365+
}
366+
356367
diagnostic.SuggestedFixes = []analysis.SuggestedFix{{
357-
Message: "Use errors.Is() to compare errors",
358-
TextEdits: []analysis.TextEdit{{
359-
Pos: binExpr.Pos(),
360-
End: binExpr.End(),
361-
NewText: []byte(replacement),
362-
}},
368+
Message: "Use errors.Is() to compare errors",
369+
TextEdits: textEdits,
363370
}}
364371

365372
lints = append(lints, diagnostic)
@@ -489,7 +496,49 @@ func LintErrorComparisons(info *TypesInfoExt) []analysis.Diagnostic {
489496
return lints
490497
}
491498

492-
// exprToString converts an expression to its string representation.
499+
func findContainingFile(info *TypesInfoExt, node ast.Node) *ast.File {
500+
current := node
501+
for current != nil {
502+
if file, ok := current.(*ast.File); ok {
503+
return file
504+
}
505+
current = info.NodeParent[current]
506+
}
507+
return nil
508+
}
509+
510+
func needsErrorsImport(file *ast.File) bool {
511+
for _, imp := range file.Imports {
512+
if imp.Path.Value == `"errors"` {
513+
return false
514+
}
515+
}
516+
return true
517+
}
518+
519+
func createImportFix(file *ast.File) analysis.SuggestedFix {
520+
var importPos token.Pos
521+
var newText string
522+
523+
if len(file.Imports) > 0 {
524+
lastImport := file.Imports[len(file.Imports)-1]
525+
importPos = lastImport.End()
526+
newText = "\n\t\"errors\""
527+
} else {
528+
importPos = file.Name.End()
529+
newText = "\n\nimport \"errors\""
530+
}
531+
532+
return analysis.SuggestedFix{
533+
Message: "Add missing errors import",
534+
TextEdits: []analysis.TextEdit{{
535+
Pos: importPos,
536+
End: importPos,
537+
NewText: []byte(newText),
538+
}},
539+
}
540+
}
541+
493542
func exprToString(expr ast.Expr) string {
494543
switch e := expr.(type) {
495544
case *ast.Ident:
@@ -1123,6 +1172,9 @@ func combinedFix(ifStmt *ast.IfStmt, extInfo *TypesInfoExt) *analysis.SuggestedF
11231172
// Check if this is an else-if statement
11241173
components.context.isElseIf = isElseIfStatement(ifStmt, extInfo)
11251174

1175+
// Handle else-if transformations carefully
1176+
// Note: else-if transformations change structure but preserve functionality
1177+
11261178
// Build the replacement text using the extracted components
11271179
replacement := buildReplacement(components)
11281180
if replacement == "" {
@@ -1137,13 +1189,21 @@ func combinedFix(ifStmt *ast.IfStmt, extInfo *TypesInfoExt) *analysis.SuggestedF
11371189
endPos = ifStmt.Body.End()
11381190
}
11391191

1192+
textEdits := []analysis.TextEdit{{
1193+
Pos: ifStmt.Pos(),
1194+
End: endPos,
1195+
NewText: []byte(replacement),
1196+
}}
1197+
1198+
// Add errors import if needed
1199+
if file := findContainingFile(extInfo, ifStmt); file != nil && needsErrorsImport(file) {
1200+
importFix := createImportFix(file)
1201+
textEdits = append(textEdits, importFix.TextEdits...)
1202+
}
1203+
11401204
return &analysis.SuggestedFix{
1141-
Message: "Use errors.As and errors.Is for error handling",
1142-
TextEdits: []analysis.TextEdit{{
1143-
Pos: ifStmt.Pos(),
1144-
End: endPos,
1145-
NewText: []byte(replacement),
1146-
}},
1205+
Message: "Use errors.As and errors.Is for error handling",
1206+
TextEdits: textEdits,
11471207
}
11481208
}
11491209

@@ -1177,8 +1237,8 @@ type comparison struct {
11771237
negated bool
11781238
}
11791239

1180-
// context holds if statement context information
1181-
type context struct {
1240+
// controlFlowContext holds if statement controlFlowContext information
1241+
type controlFlowContext struct {
11821242
isElseIf bool
11831243
bodyStmts []ast.Stmt
11841244
}
@@ -1188,7 +1248,7 @@ type context struct {
11881248
type ifComponents struct {
11891249
assertion typeAssertion
11901250
comparison comparison
1191-
context context
1251+
context controlFlowContext
11921252
}
11931253

11941254
// parseIfComponents extracts the components of the if statement pattern
@@ -1224,7 +1284,7 @@ func parseIfComponents(ifStmt *ast.IfStmt) *ifComponents {
12241284
target: rightBinExpr.Y,
12251285
negated: rightBinExpr.Op == token.NEQ,
12261286
},
1227-
context: context{
1287+
context: controlFlowContext{
12281288
isElseIf: false, // Will be set by the calling function if needed
12291289
bodyStmts: ifStmt.Body.List, // Capture body statements
12301290
},
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package astmanipulation
2+
3+
import (
4+
"os"
5+
"syscall"
6+
)
7+
8+
// Test inline comment preservation
9+
func InlineComments() {
10+
var err error
11+
12+
if err == syscall.ENOENT { // want "comparing with == will fail on wrapped errors. Use errors.Is to check for a specific error"
13+
// This block comment should be preserved
14+
println("File not found")
15+
}
16+
}
17+
18+
// Test else-if comment preservation (main PR issue)
19+
func ElseIfComments() {
20+
var err error
21+
22+
if err != nil {
23+
println("error occurred")
24+
} else if e, ok := err.(*os.PathError); ok && e.Err == syscall.ESRCH { // want "type assertion and error comparison will fail on wrapped errors. Use errors.As and errors.Is to check for specific errors"
25+
// If the process exits while reading its /proc/$PID/maps, the kernel will
26+
// return ESRCH. Handle it as if the process did not exist.
27+
println("Process not found")
28+
}
29+
}
30+
31+
// Test block comments
32+
func BlockComments() {
33+
var err error
34+
35+
/* Pre-condition block comment */
36+
if e, ok := err.(*os.PathError); ok { // want "type assertion on error will fail on wrapped errors. Use errors.As to check for specific errors"
37+
/* Inline block comment */ println("Path error:", e.Path)
38+
}
39+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package astmanipulation
2+
3+
import (
4+
"errors"
5+
"os"
6+
"syscall"
7+
)
8+
9+
// Test inline comment preservation
10+
func InlineComments() {
11+
var err error
12+
13+
if errors.Is(err, syscall.ENOENT) { // want "comparing with == will fail on wrapped errors. Use errors.Is to check for a specific error"
14+
// This block comment should be preserved
15+
println("File not found")
16+
}
17+
}
18+
19+
// Test else-if comment preservation (main PR issue)
20+
func ElseIfComments() {
21+
var err error
22+
23+
if err != nil {
24+
println("error occurred")
25+
} else {
26+
e := &os.PathError{}
27+
if errors.As(err, &e) && errors.Is(e.Err, syscall.ESRCH) {
28+
println("Process not found")
29+
}
30+
}
31+
}
32+
33+
// Test block comments
34+
func BlockComments() {
35+
var err error
36+
37+
/* Pre-condition block comment */
38+
e := &os.PathError{}
39+
if errors.As(err, &e) { // want "type assertion on error will fail on wrapped errors. Use errors.As to check for specific errors"
40+
/* Inline block comment */ println("Path error:", e.Path)
41+
}
42+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package astmanipulation
2+
3+
import (
4+
"io"
5+
"os"
6+
)
7+
8+
// Test missing errors import with simple comparison
9+
func MissingImportSimple() {
10+
var err error
11+
var n int
12+
13+
if n == 0 || (err != nil && err != io.EOF) { // want "comparing with != will fail on wrapped errors. Use errors.Is to check for a specific error"
14+
return
15+
}
16+
}
17+
18+
// Test missing errors import with type assertion
19+
func MissingImportTypeAssertion() {
20+
var err error
21+
22+
if e, ok := err.(*os.PathError); ok { // want "type assertion on error will fail on wrapped errors. Use errors.As to check for specific errors"
23+
println("Path error:", e.Path)
24+
}
25+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package astmanipulation
2+
3+
import (
4+
"errors"
5+
"io"
6+
"os"
7+
)
8+
9+
// Test missing errors import with simple comparison
10+
func MissingImportSimple() {
11+
var err error
12+
var n int
13+
14+
if n == 0 || (err != nil && !errors.Is(err, io.EOF)) { // want "comparing with != will fail on wrapped errors. Use errors.Is to check for a specific error"
15+
return
16+
}
17+
}
18+
19+
// Test missing errors import with type assertion
20+
func MissingImportTypeAssertion() {
21+
var err error
22+
23+
e := &os.PathError{}
24+
if errors.As(err, &e) { // want "type assertion on error will fail on wrapped errors. Use errors.As to check for specific errors"
25+
println("Path error:", e.Path)
26+
}
27+
}

errorlint/testdata/src/issues/fix/github-100.go.golden

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package issues
22

33
import (
4+
"errors"
45
"os"
56
"syscall"
67
)

0 commit comments

Comments
 (0)