Skip to content

Commit ef0cd0b

Browse files
authored
feat: Add compile-time safety linters to golangci-lint config (#185)
1 parent 7d0ef32 commit ef0cd0b

File tree

10 files changed

+226
-150
lines changed

10 files changed

+226
-150
lines changed

.golangci.yml

Lines changed: 191 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -38,135 +38,209 @@ linters:
3838
- sqlclosecheck # Check SQL resources are closed
3939
- rowserrcheck # Check database rows errors
4040

41+
# Compile-time safety linters
42+
- nilnil # Prevent returning (nil, nil) which hides errors
43+
- forcetypeassert # Require checked type assertions (v, ok := x.(T))
44+
- gocritic # Additional safety diagnostics
45+
- bodyclose # HTTP response bodies must be closed
46+
4147
disable:
4248
- varnamelen # Too strict for production code
4349
- testpackage # Allow internal testing
4450
- paralleltest # Not always appropriate
4551
- wrapcheck # Disabled: gRPC services return terminal errors
4652

53+
exclusions:
54+
# v2 format: path-based exclusions
55+
rules:
56+
# Exclude some linters for test files
57+
- path: _test\.go
58+
linters:
59+
- gocyclo
60+
- gocognit
61+
- errcheck
62+
- goconst
63+
- unparam
64+
- wrapcheck
65+
- bodyclose
66+
- forcetypeassert # Type assertions in tests are acceptable for cleaner test code
67+
- gocritic # Style checks less critical in tests
68+
- nilnil # Tests may use (nil, nil) for mock implementations
69+
- govet # unusedwrite and nilness checks too strict for test scaffolding
70+
71+
# Exclude linters for testdb package (test support utilities)
72+
- path: internal/platform/testdb/
73+
linters:
74+
- gocyclo
75+
- gocognit
76+
- gocritic
77+
78+
# Exclude complexity linters for gRPC service implementations
79+
# These have inherently complex request handling with validation, conversion, and persistence
80+
- path: internal/.*/service/.*\.go
81+
linters:
82+
- gocyclo
83+
- gocognit
84+
85+
# Exclude complexity linters for repository implementations (complex query building)
86+
- path: internal/.*/repository/.*\.go
87+
linters:
88+
- gocyclo
89+
- gocognit
90+
91+
# Exclude complexity linters for adapter implementations (mapping logic)
92+
- path: internal/.*/adapters/.*\.go
93+
linters:
94+
- gocyclo
95+
- gocognit
96+
97+
# Exclude complexity linters for Kafka implementations (complex message handling)
98+
- path: internal/platform/kafka/.*\.go
99+
linters:
100+
- gocyclo
101+
- gocognit
102+
103+
# Exclude complexity linters for auth config (complex switch statements for auth modes)
104+
- path: internal/platform/auth/.*\.go
105+
linters:
106+
- gocyclo
107+
- gocognit
108+
109+
# Exclude all linters for integration tests (require external services)
110+
- path: test/integration/
111+
linters:
112+
- all
113+
114+
# Exclude some linters for generated files
115+
- path: \.pb\.go$
116+
linters:
117+
- all
118+
119+
# Exclude typecheck for tools.go (imports main packages for tool dependency tracking)
120+
- path: tools\.go$
121+
linters:
122+
- typecheck
123+
124+
# Exclude some linters for mock files
125+
- path: mock_.*\.go
126+
linters:
127+
- all
128+
129+
# Allow main() to have unchecked errors for flag parsing
130+
- path: cmd/
131+
text: "Error return value of.*is not checked"
132+
linters:
133+
- errcheck
134+
135+
# Allow long functions in main packages for setup
136+
- path: cmd/
137+
linters:
138+
- gocyclo
139+
- gocognit
140+
141+
# Exclude misspell for protobuf enum references (CANCELLED is the official enum name)
142+
- text: "CANCELLED.*is a misspelling of.*CANCELED"
143+
linters:
144+
- misspell
145+
146+
# Exclude misspell for British English variations in comments/strings
147+
# The codebase uses British English for user-facing terminology
148+
- text: "cancelled.*is a misspelling of.*canceled"
149+
linters:
150+
- misspell
151+
- text: "Cancelled.*is a misspelling of.*Canceled"
152+
linters:
153+
- misspell
154+
- text: "cancelling.*is a misspelling of.*canceling"
155+
linters:
156+
- misspell
157+
158+
settings:
159+
govet:
160+
enable-all: true
161+
disable:
162+
- shadow # Too many false positives
163+
- fieldalignment # Struct field alignment is informative but too noisy
164+
165+
errcheck:
166+
check-type-assertions: true
167+
check-blank: false # Allow _ = to explicitly ignore errors
168+
exclude-functions:
169+
- fmt.Print*
170+
- fmt.Fprint*
171+
172+
staticcheck:
173+
checks: ["all"]
174+
175+
gocyclo:
176+
min-complexity: 15
177+
178+
gocognit:
179+
min-complexity: 20
180+
181+
goconst:
182+
min-len: 3
183+
min-occurrences: 3
184+
185+
misspell:
186+
locale: US
187+
188+
nakedret:
189+
max-func-lines: 30
190+
191+
unparam:
192+
check-exported: false
193+
194+
revive:
195+
confidence: 0.8
196+
rules:
197+
- name: blank-imports
198+
- name: context-as-argument
199+
- name: context-keys-type
200+
- name: dot-imports
201+
- name: error-return
202+
- name: error-strings
203+
- name: error-naming
204+
- name: exported
205+
- name: if-return
206+
- name: increment-decrement
207+
- name: var-naming
208+
- name: var-declaration
209+
- name: package-comments
210+
- name: range
211+
- name: receiver-naming
212+
- name: time-naming
213+
- name: unexported-return
214+
- name: indent-error-flow
215+
- name: errorf
216+
- name: empty-block
217+
- name: superfluous-else
218+
- name: unused-parameter
219+
- name: unreachable-code
220+
- name: redefines-builtin-id
221+
222+
errorlint:
223+
errorf: true
224+
asserts: true
225+
comparison: true
226+
227+
gocritic:
228+
# Enable only diagnostic checks (safety), disable style checks
229+
enabled-tags:
230+
- diagnostic
231+
disabled-tags:
232+
- style
233+
- performance
234+
- opinionated
235+
# Note: ifElseChain, elseif, unlambda, exitAfterDefer, assignOp are
236+
# already disabled by disabled-tags: [style]
237+
47238
formatters:
48239
enable:
49240
- gofmt # Standard Go formatting
50241
- goimports # Manage imports
51242
- gofumpt # Stricter gofmt
52243

53-
linters-settings:
54-
govet:
55-
enable-all: true
56-
disable:
57-
- shadow # Too many false positives
58-
59-
errcheck:
60-
check-type-assertions: true
61-
check-blank: true
62-
exclude-functions:
63-
- fmt.Print*
64-
- fmt.Fprint*
65-
66-
staticcheck:
67-
checks: ["all"]
68-
69-
gocyclo:
70-
min-complexity: 15
71-
72-
gocognit:
73-
min-complexity: 20
74-
75-
goconst:
76-
min-len: 3
77-
min-occurrences: 3
78-
79-
misspell:
80-
locale: US
81-
82-
nakedret:
83-
max-func-lines: 30
84-
85-
unparam:
86-
check-exported: false
87-
88-
revive:
89-
confidence: 0.8
90-
rules:
91-
- name: blank-imports
92-
- name: context-as-argument
93-
- name: context-keys-type
94-
- name: dot-imports
95-
- name: error-return
96-
- name: error-strings
97-
- name: error-naming
98-
- name: exported
99-
- name: if-return
100-
- name: increment-decrement
101-
- name: var-naming
102-
- name: var-declaration
103-
- name: package-comments
104-
- name: range
105-
- name: receiver-naming
106-
- name: time-naming
107-
- name: unexported-return
108-
- name: indent-error-flow
109-
- name: errorf
110-
- name: empty-block
111-
- name: superfluous-else
112-
- name: unused-parameter
113-
- name: unreachable-code
114-
- name: redefines-builtin-id
115-
116-
errorlint:
117-
errorf: true
118-
asserts: true
119-
comparison: true
120-
121-
issues:
122-
exclude-use-default: false
123-
max-issues-per-linter: 0
124-
max-same-issues: 0
125-
uniq-by-line: true
126-
127-
exclude-rules:
128-
# Exclude some linters for test files
129-
- path: _test\.go
130-
linters:
131-
- gocyclo
132-
- gocognit
133-
- errcheck
134-
- goconst
135-
- unparam
136-
- wrapcheck
137-
138-
# Exclude all linters for integration tests (require external services)
139-
- path: test/integration/
140-
linters:
141-
- all
142-
143-
# Exclude some linters for generated files
144-
- path: \.pb\.go$
145-
linters:
146-
- all
147-
148-
# Exclude typecheck for tools.go (imports main packages for tool dependency tracking)
149-
- path: tools\.go$
150-
linters:
151-
- typecheck
152-
153-
# Exclude some linters for mock files
154-
- path: mock_.*\.go
155-
linters:
156-
- all
157-
158-
# Allow main() to have unchecked errors for flag parsing
159-
- path: cmd/
160-
text: "Error return value of.*is not checked"
161-
linters:
162-
- errcheck
163-
164-
# Allow long functions in main packages for setup
165-
- path: cmd/
166-
linters:
167-
- gocyclo
168-
- gocognit
169-
170244
output:
171245
formats:
172246
colored-line-number:

cmd/horizon-demo/clients.go

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -114,27 +114,26 @@ func NewClients(cfg *ClientsConfig) (*Clients, error) {
114114
// Close terminates all gRPC connections.
115115
// It attempts to close all connections and returns the first error encountered.
116116
func (c *Clients) Close() error {
117-
var firstErr error
117+
var errs []error
118118

119119
if c.currentAccountConn != nil {
120120
if err := c.currentAccountConn.Close(); err != nil {
121121
c.logger.Warn("failed to close CurrentAccountService connection", "error", err)
122-
if firstErr == nil {
123-
firstErr = fmt.Errorf("failed to close CurrentAccountService connection: %w", err)
124-
}
122+
errs = append(errs, fmt.Errorf("failed to close CurrentAccountService connection: %w", err))
125123
}
126124
}
127125

128126
if c.paymentOrderConn != nil {
129127
if err := c.paymentOrderConn.Close(); err != nil {
130128
c.logger.Warn("failed to close PaymentOrderService connection", "error", err)
131-
if firstErr == nil {
132-
firstErr = fmt.Errorf("failed to close PaymentOrderService connection: %w", err)
133-
}
129+
errs = append(errs, fmt.Errorf("failed to close PaymentOrderService connection: %w", err))
134130
}
135131
}
136132

137-
return firstErr
133+
if len(errs) > 0 {
134+
return errs[0] // Return first error
135+
}
136+
return nil
138137
}
139138

140139
// CheckHealth verifies that both services are reachable.
@@ -159,7 +158,7 @@ func (c *Clients) CheckHealth(_ context.Context) error {
159158
return nil
160159
}
161160

162-
// WaitForReady blocks until both services are ready or the context is cancelled.
161+
// WaitForReady blocks until both services are ready or the context is canceled.
163162
// This is useful for startup health checks before beginning the demo.
164163
func (c *Clients) WaitForReady(ctx context.Context) error {
165164
// Wait for CurrentAccountService
@@ -168,11 +167,7 @@ func (c *Clients) WaitForReady(ctx context.Context) error {
168167
}
169168

170169
// Wait for PaymentOrderService
171-
if err := c.waitForConnReady(ctx, c.paymentOrderConn, "PaymentOrderService"); err != nil {
172-
return err
173-
}
174-
175-
return nil
170+
return c.waitForConnReady(ctx, c.paymentOrderConn, "PaymentOrderService")
176171
}
177172

178173
// waitForConnReady waits for a single connection to reach Ready state.
@@ -197,7 +192,7 @@ func (c *Clients) waitForConnReady(ctx context.Context, conn *grpc.ClientConn, s
197192

198193
// TransientFailure is temporary; keep waiting unless context expires
199194
if !conn.WaitForStateChange(ctx, state) {
200-
// Context was cancelled or timed out
195+
// Context was canceled or timed out
201196
return fmt.Errorf("%w: context expired while waiting for %s: %w",
202197
ErrHealthCheckFailed, serviceName, ctx.Err())
203198
}

0 commit comments

Comments
 (0)