Skip to content

Commit f549c32

Browse files
committed
Release 0.0.6
- check for error wrapping related methods - support for Go 1.26 new and errrors.AsType - make variable heuristic optional - copy flags from detect to errortype - move suggestion logic to analyzer Signed-off-by: Oliver Eikemeier <[email protected]>
1 parent 75cda30 commit f549c32

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+1977
-1172
lines changed

.custom-gcl.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
version: v2.4.0
2+
version: v2.5.0
33

44
name: golangci-lint
55
destination: .

.github/workflows/test.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,11 @@ jobs:
3939
- name: 🧸 golangci-lint
4040
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
4141
with:
42-
version: v2.4.0
42+
version: v2.5.0
4343
- name: 🔨 Test
4444
run: |
4545
(cd ./internal/analyze/testdata && go mod download)
4646
go test -coverprofile=cover.out ./...
47-
env:
48-
GOEXPERIMENT: aliastypeparams
4947
- name: 🧑🏻‍💻 codecov
5048
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
5149
if: ${{ matrix.update-meta }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
!/.mockery.yaml
1111
!/.pre-commit-config.yaml
1212
!/.prettierrc.yaml
13+
!/.woodpecker/
1314
!/.yamlfmt
1415
!/.yamllint
1516
/bazel-*

.golangci.yaml

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,27 @@ linters:
6565
exclude-functions:
6666
- (*os.File).Close
6767
- (io/fs.File).Close
68+
goheader:
69+
values:
70+
const:
71+
AUTHOR: Oliver Eikemeier
72+
RANGE: "2025"
73+
template: |-
74+
Copyright {{ RANGE }} {{ AUTHOR }}. All Rights Reserved.
75+
76+
Licensed under the Apache License, Version 2.0 (the "License");
77+
you may not use this file except in compliance with the License.
78+
You may obtain a copy of the License at
79+
80+
http://www.apache.org/licenses/LICENSE-2.0
81+
82+
Unless required by applicable law or agreed to in writing, software
83+
distributed under the License is distributed on an "AS IS" BASIS,
84+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
85+
See the License for the specific language governing permissions and
86+
limitations under the License.
87+
88+
SPDX-License-Identifier: Apache-2.0
6889
govet:
6990
enable-all: true
7091
disable:
@@ -94,9 +115,6 @@ linters:
94115
- name: error-strings
95116
- name: errorf
96117
- name: exported
97-
- name: file-header
98-
arguments:
99-
- "Copyright 2025 Oliver Eikemeier. All Rights Reserved."
100118
- name: increment-decrement
101119
- name: indent-error-flow
102120
- name: range

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ repos:
1414
hooks:
1515
- id: markdownlint-cli2
1616
- repo: https://github.com/golangci/golangci-lint
17-
rev: v2.4.0
17+
rev: v2.5.0
1818
hooks:
1919
- id: golangci-lint

.woodpecker/test.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
when:
3+
- event: push
4+
branch: [main, dev]
5+
6+
matrix:
7+
GO_VERSION:
8+
- 1.25
9+
- 1.24
10+
11+
steps:
12+
- name: test
13+
image: golang:${GO_VERSION}
14+
environment:
15+
GOTOOLCHAIN: local
16+
commands:
17+
- go mod download && (cd ./internal/analyze/testdata && go mod download)
18+
- echo "🔨 Testing on $(go version):"
19+
- go test ./...

README.md

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
[![CodeQL](https://github.com/fillmore-labs/errortype/actions/workflows/github-code-scanning/codeql/badge.svg?branch=main)](https://github.com/fillmore-labs/errortype/actions/workflows/github-code-scanning/codeql)
66
[![Coverage](https://codecov.io/gh/fillmore-labs/errortype/branch/main/graph/badge.svg?token=MMLHL14ZP6)](https://codecov.io/gh/fillmore-labs/errortype)
77
[![Go Report Card](https://goreportcard.com/badge/fillmore-labs.com/errortype)](https://goreportcard.com/report/fillmore-labs.com/errortype)
8+
[![Codeberg CI](https://ci.codeberg.org/api/badges/15305/status.svg?branch=main)](https://ci.codeberg.org/repos/15305)
89
[![License](https://img.shields.io/github/license/fillmore-labs/errortype)](https://www.apache.org/licenses/LICENSE-2.0)
910

1011
`errortype` is a Go static analysis tool (linter) that helps prevent subtle bugs in error handling and other areas. It
@@ -69,8 +70,9 @@ Usage: `errortype [-flag] [package]`
6970
- **-c** `<N>`: Display N lines of context around each issue (default: -1 for no context, 0 for only the offending
7071
line).
7172
- **-test**: Analyze test files in addition to source files (default: true).
72-
- **-heuristics**: (Experimental) List of heuristics used (default: “usage,receivers”, “off” to disable).
73-
- **-trace**: (Experimental) Output information for result tracing.
73+
- **-heuristics**: `<list>` (Experimental) List of heuristics used (default: “var,usage,receivers”, “off” to disable).
74+
- **-tracetypes**: `<regex>` (Experimental) Trace error type detection in packages with names matching the regular
75+
expression.
7476

7577
## Inconsistent Error Type Usage
7678

@@ -135,7 +137,15 @@ The linter determines an error type's intended use _(pointer vs. value)_ by anal
135137
1. **Overrides** (the highest priority): User-defined overrides (see [below](#override-file)) are applied, overriding
136138
any detected usage.
137139

138-
2. **Package-Level Variable Assignments**: If present, `var _ error = ...` assignments are used as explicit declarations
140+
2. **`Unwrap` related methods**: If present, `Is`, `As` and `Unwrap` methods with pointer receiver would be not visible
141+
from a value usage. If an error type would be used as a value, methods with pointer receivers are not in its
142+
[method set](https://go.dev/ref/spec#Method_sets).
143+
144+
```go
145+
func (e *PointerError) Unwrap error { /* ...*/ } // Unwrap is only visible from error(&PointerError{}).
146+
```
147+
148+
3. **Package-Level Variable Assignments**: If present, `var _ error = ...` assignments are used as explicit declarations
139149
of intent.
140150

141151
```go
@@ -144,7 +154,7 @@ The linter determines an error type's intended use _(pointer vs. value)_ by anal
144154
var _ error = (*PointerError)(nil) // Determines PointerError is a "pointer" type.
145155
```
146156

147-
3. **Usage Within Functions**: If still undecided, the linter analyzes usage within top-level functions (in `return`
157+
4. **Usage Within Functions**: If still undecided, the linter analyzes usage within top-level functions (in `return`
148158
statements or type assertions). Consistent usage can determine the type.
149159

150160
```go
@@ -155,7 +165,7 @@ The linter determines an error type's intended use _(pointer vs. value)_ by anal
155165

156166
Note: This heuristic is a fallback and should not be relied upon for defining a type's contract.
157167

158-
4. **Consistent Method Receivers** (lowest priority): As a final heuristic, if all methods on a type have a consistent
168+
5. **Consistent Method Receivers** (lowest priority): As a final heuristic, if all methods on a type have a consistent
159169
receiver (all-value or all-pointer), that style is used.
160170

161171
### Designing Linter-Friendly Packages
@@ -181,21 +191,8 @@ var (
181191

182192
### Overriding Detected Types
183193

184-
When the linter reports types from an imported package that have ambiguous or inconsistent usage, you can guide the
185-
linter in two ways:
186-
187-
1. **Local Overrides**: For a one-off fix within a single package, add a `var` block to a source file in that package.
188-
This overrides the detected usage for that type _within this package only_.
189-
190-
```go
191-
// In your code, force a specific usage for an imported type.
192-
var _ error = imported.ValueError{}
193-
194-
var _ error = (*imported.PointerError)(nil)
195-
```
196-
197-
2. **Global Override File**: For project-wide overrides, use an `errortypes.yaml` file, see
198-
_[“Override File”](#override-file)_.
194+
When the linter reports ambiguous or inconsistent usage from types of an imported package that you can not change, you
195+
can guide the linter with an override file, see _[“Override File”](#override-file)_.
199196

200197
## Pointless Comparisons
201198

@@ -377,6 +374,12 @@ specific problems.
377374
- **`et:emb` (Embedded/Ambiguous Usage)**: The linter could not determine if an error is a pointer or value type. This
378375
can be resolved with an [override](#overriding-detected-types).
379376

377+
- **`et:var` (Variable Mismatch)**: An error type is assigned incorrectly in a
378+
[variable declaration](https://go.dev/ref/spec#Variable_declarations) starting with `Err` or `err`.
379+
380+
- **`et:rcv` (Receiver Mismatch)**: An `Unwrap` related method on a value error should be implemented with a value
381+
receiver, not a pointer, otherwise it wouldn't be visible.
382+
380383
### Pointer Comparison Issues
381384

382385
- **`et:cmp` (Pointless Error Comparison)**: A pointer is compared against the address of a newly created value in
@@ -458,7 +461,10 @@ specific problems.
458461
type implementing `error`). This is also flagged by the standard Go
459462
[`errorsas`](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/errorsas) linter.
460463

461-
- **`et:auc` (Unchecked Type Assert)**: An unchecked type assert might lead to a run-time panic on a wrapped error.
464+
- **`et:sig` (Wrong Signature)**: An `Unwrap` related method has the wrong signature. This is also flagged by the
465+
standard Go [`stdmethods`](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/stdmethods) linter.
466+
467+
- **`et:uca` (Unchecked Type Assert)**: An unchecked type assert might lead to a run-time panic on a wrapped error.
462468

463469
```go
464470
if err.(*net.AddrError).Err == "missing port in address" { /* ... */ }
@@ -474,15 +480,15 @@ Add a file `.custom-gcl.yaml` to your source with
474480

475481
```yaml
476482
---
477-
version: v2.4.0
483+
version: v2.5.0
478484
479485
name: golangci-lint
480486
destination: .
481487
482488
plugins:
483489
- module: fillmore-labs.com/errortype
484490
import: fillmore-labs.com/errortype/gclplugin
485-
version: v0.0.5
491+
version: v0.0.6
486492
```
487493

488494
then run `golangci-lint custom` from your project root. You get a custom `golangci-lint` executable that can be

analyzer/analyzer.go

Lines changed: 3 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -17,67 +17,18 @@
1717
package analyzer
1818

1919
import (
20-
"reflect"
21-
2220
"golang.org/x/tools/go/analysis"
23-
"golang.org/x/tools/go/analysis/passes/inspect"
2421

2522
"fillmore-labs.com/errortype/detect"
26-
"fillmore-labs.com/errortype/internal/analyze"
27-
)
28-
29-
// Documentation constants.
30-
const (
31-
Name = "errortype"
32-
Doc = `errortype is a Go static analysis tool that helps prevent subtle bugs in error handling.
33-
34-
It performs two main checks:
35-
36-
1. Inconsistent Error Type Usage: It analyzes function returns, type assertions,
37-
and calls to functions like errors.As to ensure that custom error types
38-
are used consistently as either pointers or values.
39-
40-
2. Pointless Pointer Comparisons: It detects comparisons of pointers against
41-
the address of a newly created value (e.g., 'ptr == &MyStruct{}'), which
42-
are almost always incorrect.
43-
44-
For inconsistent error type usage, it automatically determines the correct usage
45-
for most error types but may require a configuration file for ambiguous cases.`
46-
47-
URL = "https://pkg.go.dev/fillmore-labs.com/errortype/analyzer"
4823
)
4924

5025
// New creates a new instance of the errortype analyzer.
51-
// It allows for programmatic configuration using [Option]s, which is useful
26+
// It allows for programmatic configuration using [Options], which is useful
5227
// for integrating the analyzer into other tools. For command-line use, the
5328
// pre-configured [Analyzer] variable is typically sufficient.
5429
func New(opts ...Option) *analysis.Analyzer {
55-
o := makeOptions(opts)
56-
57-
if o.DetectTypes == nil {
58-
o.DetectTypes = detect.New()
59-
}
60-
61-
a := &analysis.Analyzer{
62-
Name: Name,
63-
Doc: Doc,
64-
URL: URL,
65-
Run: o.Run,
66-
Requires: []*analysis.Analyzer{inspect.Analyzer, o.DetectTypes},
67-
ResultType: reflect.TypeFor[analyze.Result](),
68-
}
69-
70-
a.Flags.BoolVar(&o.StyleCheck, "style-check", o.StyleCheck, "check for confusing uses of errors.As")
71-
72-
a.Flags.BoolVar(&o.CheckIs, "check-is", o.CheckIs,
73-
`suppress compare diagnostic on errors.Is if the compared type has an "Is(error) bool" method`)
74-
75-
a.Flags.BoolVar(&o.DeepIsCheck, "deep-is-check", o.DeepIsCheck, `diagnose all "Unwrap" functions in "Is" methods, not only on target`)
76-
77-
a.Flags.BoolVar(&o.UncheckedAssert, "unchecked-assert", o.UncheckedAssert, `report unchecked type asserts on errors`)
78-
79-
return a
30+
return makeOptions(opts).Analyzer()
8031
}
8132

82-
// Analyzer is a pre-configured *analysis.Analyzer for detecting and enforcing consistent error type usage in Go programs.
33+
// Analyzer is a pre-configured *[analysis.Analyzer] for detecting and enforcing consistent error type usage in Go programs.
8334
var Analyzer = New(WithDetectTypes(detect.Analyzer))

analyzer/options.go

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func makeOptions(opts Options) *analyze.Options {
3535
// Option configures specific behavior of a [New] errortype [analysis.Analyzer].
3636
type Option interface {
3737
apply(opts *analyze.Options)
38-
SlogAttr() slog.Attr
38+
LogAttr() slog.Attr
3939
}
4040

4141
// Options is a list of [Option] values that also satisfies the [Option] interface.
@@ -51,28 +51,28 @@ func (o Options) apply(opts *analyze.Options) {
5151
func (o Options) LogValue() slog.Value {
5252
as := make([]slog.Attr, 0, len(o))
5353
for _, opt := range o {
54-
as = append(as, opt.SlogAttr())
54+
as = append(as, opt.LogAttr())
5555
}
5656

5757
return slog.GroupValue(as...)
5858
}
5959

60-
// SlogAttr returns a [slog.Attr] for logging.
61-
func (o Options) SlogAttr() slog.Attr {
60+
// LogAttr returns a [slog.Attr] for logging.
61+
func (o Options) LogAttr() slog.Attr {
6262
return slog.Any("options", o)
6363
}
6464

6565
// WithDetectTypes sets a custom *[analysis.Analyzer] for detecting error types.
66-
func WithDetectTypes(detecttypes *analysis.Analyzer) Option {
67-
return detectTypesOption{detecttypes: detecttypes}
66+
func WithDetectTypes(detectTypes *analysis.Analyzer) Option {
67+
return detectTypesOption{detectTypes: detectTypes}
6868
}
6969

70-
type detectTypesOption struct{ detecttypes *analysis.Analyzer }
70+
type detectTypesOption struct{ detectTypes *analysis.Analyzer }
7171

72-
func (o detectTypesOption) apply(opts *analyze.Options) { opts.DetectTypes = o.detecttypes }
72+
func (o detectTypesOption) apply(opts *analyze.Options) { opts.DetectTypes = o.detectTypes }
7373

74-
func (o detectTypesOption) SlogAttr() slog.Attr {
75-
return slog.String("detect", o.detecttypes.Name)
74+
func (o detectTypesOption) LogAttr() slog.Attr {
75+
return slog.String("detect", o.detectTypes.Name)
7676
}
7777

7878
// WithStyleCheck is an [Option] to configure the style check.
@@ -82,7 +82,7 @@ type styleCheckOption struct{ styleCheck bool }
8282

8383
func (o styleCheckOption) apply(opts *analyze.Options) { opts.StyleCheck = o.styleCheck }
8484

85-
func (o styleCheckOption) SlogAttr() slog.Attr {
85+
func (o styleCheckOption) LogAttr() slog.Attr {
8686
return slog.Bool("styleCheck", o.styleCheck)
8787
}
8888

@@ -97,13 +97,13 @@ type checkIsOption struct{ checkIs bool }
9797

9898
func (o checkIsOption) apply(opts *analyze.Options) { opts.CheckIs = o.checkIs }
9999

100-
func (o checkIsOption) SlogAttr() slog.Attr {
100+
func (o checkIsOption) LogAttr() slog.Attr {
101101
return slog.Bool("checkIs", o.checkIs)
102102
}
103103

104104
// WithDeepIsCheck is an [Option] to configure `Is` method analysis.
105105
// If deepIsCheck is true, the analyzer will flag every method calling `Unwrap`.
106-
// The default behavior is to flag only applications to `target.
106+
// The default behavior is to flag only applications to `target`.
107107
func WithDeepIsCheck(deepIsCheck bool) Option {
108108
return deepIsCheckOption{deepIsCheck: deepIsCheck}
109109
}
@@ -112,7 +112,7 @@ type deepIsCheckOption struct{ deepIsCheck bool }
112112

113113
func (o deepIsCheckOption) apply(opts *analyze.Options) { opts.DeepIsCheck = o.deepIsCheck }
114114

115-
func (o deepIsCheckOption) SlogAttr() slog.Attr {
115+
func (o deepIsCheckOption) LogAttr() slog.Attr {
116116
return slog.Bool("deepIsCheck", o.deepIsCheck)
117117
}
118118

@@ -125,6 +125,6 @@ type uncheckedAssertOption struct{ uncheckedAssert bool }
125125

126126
func (o uncheckedAssertOption) apply(opts *analyze.Options) { opts.UncheckedAssert = o.uncheckedAssert }
127127

128-
func (o uncheckedAssertOption) SlogAttr() slog.Attr {
128+
func (o uncheckedAssertOption) LogAttr() slog.Attr {
129129
return slog.Bool("uncheckedAssert", o.uncheckedAssert)
130130
}

analyzer/options_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func TestLogValue(t *testing.T) {
7878

7979
var sb strings.Builder
8080
logger := slog.New(slog.NewJSONHandler(&sb, nil))
81-
logger.LogAttrs(t.Context(), slog.LevelInfo, "test", tt.option.SlogAttr())
81+
logger.LogAttrs(t.Context(), slog.LevelInfo, "test", tt.option.LogAttr())
8282

8383
got := sb.String()
8484
if !strings.Contains(got, tt.expected) {

0 commit comments

Comments
 (0)