Skip to content

Commit df889d9

Browse files
committed
feat: Improve errorlint with better variable naming and error handling
- Add function for meaningful error variable names - Preserve original "ok" variable names in type assertions - Use errors.As() with generated variable names - Add tests for custom and wrapped errors Signed-off-by: Kemal Akkoyun <[email protected]>
1 parent 29cbe81 commit df889d9

File tree

5 files changed

+308
-17
lines changed

5 files changed

+308
-17
lines changed

errorlint/lint.go

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,9 @@ func LintErrorTypeAssertions(fset *token.FileSet, info *TypesInfoExt) []analysis
489489
// For assignment statements like: targetErr, ok := err.(*SomeError)
490490
if assign, ok := parent.(*ast.AssignStmt); ok && len(assign.Lhs) == 2 {
491491
if id, ok := assign.Lhs[0].(*ast.Ident); ok {
492-
varName := id.Name
492+
// Generate a suitable variable name, handling underscore case
493+
// Example: _, ok := err.(*MyError) -> myError := &MyError{}; ok := errors.As(err, &myError)
494+
varName := generateErrorVarName(id.Name, baseType)
493495

494496
// If this is part of an if statement initialization
495497
ifParent, isIfInit := info.NodeParent[assign].(*ast.IfStmt)
@@ -532,9 +534,20 @@ func LintErrorTypeAssertions(fset *token.FileSet, info *TypesInfoExt) []analysis
532534
} else {
533535
varDecl = fmt.Sprintf("var %s %s", varName, baseType)
534536
}
537+
538+
// Preserve the original name of the "ok" variable
539+
// Example: myErr, wasFound := err.(*MyError)
540+
// Should use "wasFound" in the transformed code, not just "ok"
541+
okName := "ok" // Default
542+
if len(assign.Lhs) > 1 {
543+
if okIdent, okOk := assign.Lhs[1].(*ast.Ident); okOk && okIdent.Name != "_" {
544+
okName = okIdent.Name
545+
}
546+
}
547+
535548
// Align with golden file format
536-
replacement := fmt.Sprintf("%s\nok := errors.As(%s, &%s)",
537-
varDecl, errExpr, varName)
549+
replacement := fmt.Sprintf("%s\n%s := errors.As(%s, &%s)",
550+
varDecl, okName, errExpr, varName)
538551

539552
diagnostic.SuggestedFixes = []analysis.SuggestedFix{{
540553
Message: "Use errors.As() for type assertions on errors",
@@ -551,15 +564,15 @@ func LintErrorTypeAssertions(fset *token.FileSet, info *TypesInfoExt) []analysis
551564

552565
if _, ok := parent.(*ast.IfStmt); ok {
553566
// For if statements without initialization but with direct type assertion in condition
554-
// This case is less common but could happen
567+
varName := generateErrorVarName("target", baseType)
555568
var varDecl string
556569
if isPointerType {
557-
varDecl = fmt.Sprintf("target := &%s{}", baseType)
570+
varDecl = fmt.Sprintf("%s := &%s{}", varName, baseType)
558571
} else {
559-
varDecl = fmt.Sprintf("var target %s", baseType)
572+
varDecl = fmt.Sprintf("var %s %s", varName, baseType)
560573
}
561-
replacement := fmt.Sprintf("%s\nif errors.As(%s, &target)",
562-
varDecl, errExpr)
574+
replacement := fmt.Sprintf("%s\nif errors.As(%s, &%s)",
575+
varDecl, errExpr, varName)
563576

564577
diagnostic.SuggestedFixes = []analysis.SuggestedFix{{
565578
Message: "Use errors.As() for type assertions on errors",
@@ -573,17 +586,19 @@ func LintErrorTypeAssertions(fset *token.FileSet, info *TypesInfoExt) []analysis
573586
continue
574587
}
575588

576-
// For standalone type assertions: err.(*SomeError) or err.(SomeError).
577-
// Create an anonymous function that handles the errors.As call.
589+
// Handle standalone type assertions without assignment
590+
// Example: _ = err.(*MyError)
591+
// Transforms to: _ = func() *MyError { var target *MyError; _ = errors.As(err, &target); return target }()
592+
varName := generateErrorVarName("target", baseType)
578593
var targetDecl string
579594
if isPointerType {
580-
targetDecl = fmt.Sprintf("target := &%s{}", baseType)
595+
targetDecl = fmt.Sprintf("%s := &%s{}", varName, baseType)
581596
} else {
582-
targetDecl = fmt.Sprintf("var target %s", baseType)
597+
targetDecl = fmt.Sprintf("var %s %s", varName, baseType)
583598
}
584599

585-
replacement := fmt.Sprintf("func() %s {\n\t%s\n\t_ = errors.As(%s, &target)\n\treturn target\n}()",
586-
targetType, targetDecl, errExpr)
600+
replacement := fmt.Sprintf("func() %s {\n\t%s\n\t_ = errors.As(%s, &%s)\n\treturn %s\n}()",
601+
targetType, targetDecl, errExpr, varName, varName)
587602

588603
diagnostic.SuggestedFixes = []analysis.SuggestedFix{{
589604
Message: "Use errors.As() for type assertions on errors",
@@ -830,3 +845,40 @@ func implementsError(t types.Type) bool {
830845

831846
return false
832847
}
848+
849+
// generateErrorVarName creates an appropriate variable name for error type assertions.
850+
// If originalName is "_" or a generic placeholder, it generates a more meaningful name
851+
// based on the error type, following Go naming conventions:
852+
//
853+
// Examples:
854+
// - originalName="_", typeName="MyError" → "myError" (camelCase conversion)
855+
// - originalName="_", typeName="pkg.CustomError" → "customError" (package prefix removed)
856+
// - originalName="existingName" → "existingName" (original name preserved)
857+
// - originalName="_", typeName="" → "myErr" (fallback for unknown types)
858+
//
859+
// This helps ensure code readability when converting type assertions to errors.As calls,
860+
// particularly when dealing with underscore identifiers that can't be referenced.
861+
func generateErrorVarName(originalName, typeName string) string {
862+
// If the original name is not an underscore, use it
863+
if originalName != "_" {
864+
return originalName
865+
}
866+
867+
// Handle underscore case by generating a name based on the type
868+
// Strip any package prefix like "pkg."
869+
if lastDot := strings.LastIndex(typeName, "."); lastDot >= 0 {
870+
typeName = typeName[lastDot+1:]
871+
}
872+
873+
// Convert first letter to lowercase for camelCase
874+
if len(typeName) > 0 {
875+
firstChar := strings.ToLower(typeName[:1])
876+
if len(typeName) > 1 {
877+
return firstChar + typeName[1:]
878+
}
879+
return firstChar
880+
}
881+
882+
// If we couldn't determine a good name, use default.
883+
return "anErr"
884+
}

errorlint/testdata/src/errorassert/errorassert.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,23 @@ func (ValueError) Error() string {
2424
return "value error"
2525
}
2626

27+
// CustomUnwrapError demonstrates custom unwrap implementation
28+
type CustomUnwrapError struct {
29+
inner error
30+
}
31+
32+
func (e *CustomUnwrapError) Error() string { return "custom: " + e.inner.Error() }
33+
func (e *CustomUnwrapError) Unwrap() error { return e.inner }
34+
35+
// CustomIsError demonstrates custom Is implementation
36+
type CustomIsError struct{}
37+
38+
func (*CustomIsError) Error() string { return "custom is error" }
39+
func (*CustomIsError) Is(target error) bool {
40+
_, ok := target.(*MyError)
41+
return ok
42+
}
43+
2744
func doSomething() error {
2845
return &MyError{}
2946
}
@@ -40,6 +57,18 @@ func doSomethingValueWrapped() error {
4057
return fmt.Errorf("wrapped value: %w", ValueError{})
4158
}
4259

60+
func getCustomUnwrapError() error {
61+
return &CustomUnwrapError{inner: &MyError{}}
62+
}
63+
64+
func getCustomIsError() error {
65+
return &CustomIsError{}
66+
}
67+
68+
func deepWrappedError() error {
69+
return fmt.Errorf("level1: %w", fmt.Errorf("level2: %w", &MyError{}))
70+
}
71+
4372
// This should be flagged - direct type assertion
4473
func TypeAssertionDirect() {
4574
err := doSomething()
@@ -149,3 +178,56 @@ func NonErrorTypeAssertion() {
149178
fmt.Println(s)
150179
}
151180
}
181+
182+
// This should be flagged - type assertion on an error with custom unwrap
183+
func TypeAssertCustomUnwrap() {
184+
err := getCustomUnwrapError()
185+
me, ok := err.(*MyError) // want "type assertion on error will fail on wrapped errors. Use errors.As to check for specific errors"
186+
if ok {
187+
fmt.Println("got my error:", me)
188+
}
189+
}
190+
191+
// This should be flagged - type assertion on deeply wrapped error
192+
func TypeAssertDeepWrapped() {
193+
err := deepWrappedError()
194+
me, ok := err.(*MyError) // want "type assertion on error will fail on wrapped errors. Use errors.As to check for specific errors"
195+
if ok {
196+
fmt.Println("got my error:", me)
197+
}
198+
}
199+
200+
// This should be flagged - type assertion on error with custom Is method
201+
func TypeAssertCustomIs() {
202+
err := getCustomIsError()
203+
me, ok := err.(*MyError) // want "type assertion on error will fail on wrapped errors. Use errors.As to check for specific errors"
204+
if ok {
205+
fmt.Println("got my error:", me)
206+
}
207+
}
208+
209+
// This tests the error conversion case
210+
func ErrorConversion() {
211+
var err error = &MyError{}
212+
var iface interface{} = err
213+
214+
// This should NOT be flagged - not asserting on an error type
215+
_, ok1 := iface.(*MyError)
216+
217+
// This should be flagged - asserting on an error type
218+
_, ok2 := err.(*MyError) // want "type assertion on error will fail on wrapped errors. Use errors.As to check for specific errors"
219+
220+
fmt.Println(ok1, ok2)
221+
}
222+
223+
// This should NOT be flagged - using errors.As with a pointer to a pointer
224+
func UsingErrorsAsWithPointerToPointer() {
225+
var err error = &MyError{}
226+
227+
me := new(*MyError)
228+
if errors.As(err, me) {
229+
fmt.Println(me)
230+
} else {
231+
fmt.Println("-")
232+
}
233+
}

errorlint/testdata/src/errorassert/errorassert.go.golden

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,23 @@ func (ValueError) Error() string {
2424
return "value error"
2525
}
2626

27+
// CustomUnwrapError demonstrates custom unwrap implementation
28+
type CustomUnwrapError struct {
29+
inner error
30+
}
31+
32+
func (e *CustomUnwrapError) Error() string { return "custom: " + e.inner.Error() }
33+
func (e *CustomUnwrapError) Unwrap() error { return e.inner }
34+
35+
// CustomIsError demonstrates custom Is implementation
36+
type CustomIsError struct{}
37+
38+
func (*CustomIsError) Error() string { return "custom is error" }
39+
func (*CustomIsError) Is(target error) bool {
40+
_, ok := target.(*MyError)
41+
return ok
42+
}
43+
2744
func doSomething() error {
2845
return &MyError{}
2946
}
@@ -40,6 +57,18 @@ func doSomethingValueWrapped() error {
4057
return fmt.Errorf("wrapped value: %w", ValueError{})
4158
}
4259

60+
func getCustomUnwrapError() error {
61+
return &CustomUnwrapError{inner: &MyError{}}
62+
}
63+
64+
func getCustomIsError() error {
65+
return &CustomIsError{}
66+
}
67+
68+
func deepWrappedError() error {
69+
return fmt.Errorf("level1: %w", fmt.Errorf("level2: %w", &MyError{}))
70+
}
71+
4372
// This should be flagged - direct type assertion
4473
func TypeAssertionDirect() {
4574
err := doSomething()
@@ -169,4 +198,61 @@ func NonErrorTypeAssertion() {
169198
if s, ok := i.(string); ok {
170199
fmt.Println(s)
171200
}
172-
}
201+
}
202+
203+
// This should be flagged - type assertion on an error with custom unwrap
204+
func TypeAssertCustomUnwrap() {
205+
err := getCustomUnwrapError()
206+
me := &MyError{}
207+
ok := errors.As(err, &me) // want "type assertion on error will fail on wrapped errors. Use errors.As to check for specific errors"
208+
if ok {
209+
fmt.Println("got my error:", me)
210+
}
211+
}
212+
213+
// This should be flagged - type assertion on deeply wrapped error
214+
func TypeAssertDeepWrapped() {
215+
err := deepWrappedError()
216+
me := &MyError{}
217+
ok := errors.As(err, &me) // want "type assertion on error will fail on wrapped errors. Use errors.As to check for specific errors"
218+
if ok {
219+
fmt.Println("got my error:", me)
220+
}
221+
}
222+
223+
// This should be flagged - type assertion on error with custom Is method
224+
func TypeAssertCustomIs() {
225+
err := getCustomIsError()
226+
me := &MyError{}
227+
ok := errors.As(err, &me) // want "type assertion on error will fail on wrapped errors. Use errors.As to check for specific errors"
228+
if ok {
229+
fmt.Println("got my error:", me)
230+
}
231+
}
232+
233+
// This tests the error conversion case
234+
func ErrorConversion() {
235+
var err error = &MyError{}
236+
var iface interface{} = err
237+
238+
// This should NOT be flagged - not asserting on an error type
239+
_, ok1 := iface.(*MyError)
240+
241+
// This should be flagged - asserting on an error type
242+
myError := &MyError{}
243+
ok2 := errors.As(err, &myError) // want "type assertion on error will fail on wrapped errors. Use errors.As to check for specific errors"
244+
245+
fmt.Println(ok1, ok2)
246+
}
247+
248+
// This should NOT be flagged - using errors.As with a pointer to a pointer
249+
func UsingErrorsAsWithPointerToPointer() {
250+
var err error = &MyError{}
251+
252+
me := new(*MyError)
253+
if errors.As(err, me) {
254+
fmt.Println(me)
255+
} else {
256+
fmt.Println("-")
257+
}
258+
}

errorlint/testdata/src/errorcompare/errorcompare.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,33 @@ func doAnotherThing() error {
2020
return io.EOF
2121
}
2222

23+
type MyError struct{}
24+
25+
func (*MyError) Error() string {
26+
return "my custom error"
27+
}
28+
29+
// CustomUnwrapError demonstrates custom unwrap implementation
30+
type CustomUnwrapError struct {
31+
inner error
32+
}
33+
34+
func (e *CustomUnwrapError) Error() string { return "custom: " + e.inner.Error() }
35+
func (e *CustomUnwrapError) Unwrap() error { return e.inner }
36+
37+
// CustomIsError demonstrates custom Is implementation
38+
type CustomIsError struct{}
39+
40+
func (*CustomIsError) Error() string { return "custom is error" }
41+
func (*CustomIsError) Is(target error) bool {
42+
_, ok := target.(*MyError)
43+
return ok
44+
}
45+
46+
func getCustomIsError() error {
47+
return &CustomIsError{}
48+
}
49+
2350
// This should be flagged - direct comparison with ==
2451
func CompareWithEquals() {
2552
err := doSomething()
@@ -131,3 +158,11 @@ func SwitchOnNonErrorValue() {
131158
fmt.Println("Unknown status")
132159
}
133160
}
161+
162+
// This should NOT be flagged - using errors.Is with custom Is method
163+
func UsingErrorsIsWithCustomIs() {
164+
err := getCustomIsError()
165+
if errors.Is(err, &MyError{}) {
166+
fmt.Println("is my error")
167+
}
168+
}

0 commit comments

Comments
 (0)