# golangci-lint configuration. # # The enabled set is chosen for signal, not coverage. A linter that fires on # stylistic preferences nobody agreed to trains the team to ignore the output, which # costs more than the findings are worth. Every linter here catches a class of bug # or a readability problem we actually care about; the exclusions below are # deliberate and commented. version: "2" run: timeout: 5m # Tests are linted. A lint failure in a test is a lint failure. tests: true build-tags: [] modules-download-mode: readonly issues-exit-code: 1 formatters: enable: - gofmt - goimports - gci settings: gci: sections: - standard - default - prefix(github.com/urpi/go-bananas) no-lex-order: true goimports: local-prefixes: [github.com/urpi/go-bananas] linters: enable: # --- correctness ------------------------------------------------------- - errcheck # an unchecked error is a bug waiting for a bad day - govet - ineffassign - staticcheck # the staticcheck suite, at its default strictness - unused - bodyclose # an unclosed response body leaks a connection - rowserrcheck # a rows.Err() not checked hides a mid-iteration failure - sqlclosecheck - noctx # an HTTP request without a context cannot be cancelled - contextcheck # a context stored in a struct cannot be cancelled - errorlint # %w and errors.Is, so wrapping and matching actually work - nilerr # returning nil after checking err != nil is almost always wrong # --- readability ------------------------------------------------------- - revive # exported comments, naming, control-flow clarity - goconst # a repeated literal is a constant waiting to be named - godot # comments end in a period - misspell # a typo in a comment is a typo a reader will trust - unconvert - prealloc - whitespace - nolintlint # a nolint that no longer applies is misleading - copyloopvar # Go 1.22+ loop variables; the old idiom is now a bug # --- security ---------------------------------------------------------- - gosec # integer overflows, weak randomness, unsafe, etc. # --- testing ----------------------------------------------------------- - thelper # t.Helper() in a test helper - tparallel # a parallel test that does not call t.Parallel settings: errcheck: # A type assertion or conversion that cannot fail needs no check. check-type-assertions: false # A blank assignment to an error is a deliberate, documented decision, not an # oversight, so errcheck is asked to flag it too. The ones that remain are # marked with an explicit nolint and a reason. check-blank: true exclude-functions: # Writing to an http.ResponseWriter returns an error only when the client has # gone away, and there is nothing a handler can do about that: the response # is already committed and the connection is already being torn down. - (net/http.ResponseWriter).Write - (net/http.ResponseWriter).WriteHeader # A Rollback after a successful Commit is expected to report # sql.ErrTxDone, and a Rollback on the error path is immediately followed by # returning that error. Checking either produces a branch that can only ever # discard a result. - (*database/sql.Tx).Rollback # Deferred Close on a read-only database handle: the only error it can # report is already lost by the time the process exits. - (*database/sql.Rows).Close - (io.Closer).Close - (*os.File).Close - (*bytes.Buffer).Write - (*bytes.Buffer).WriteByte - (*bytes.Buffer).WriteString - (io.Writer).Write - (hash.Hash).Write - fmt.Fprintf - fmt.Fprint - fmt.Fprintln - fmt.Print - fmt.Println - fmt.Sprintf - sort.Slice govet: enable-all: true disable: # Field alignment is a micro-optimisation that makes every struct # declaration a puzzle. It is not worth the readability cost. - fieldalignment # The shadow linter fires on ordinary, readable `err :=` inside an if. - shadow revive: rules: # requireDoc is deliberately not set. Every exported item in this codebase # carries a doc comment by convention and by review, but the rule's argument # name is not "requireDoc" in this version of revive, so setting it disables # the whole revive rule set with a configuration error rather than enforcing # anything. - name: exported arguments: - checkPrivateReceivers - name: package-comments - name: blank-imports - name: context-as-argument - name: context-keys-type - name: dot-imports - name: error-return # No arguments: the rule takes none. Passing "error-strings" as an argument # makes revive look for a custom function of that name, fails to find it, and # then silently disables the rule along with every other revive rule. - name: error-strings - name: error-naming - name: increment-decrement # The allowlist has to be a slice. Given as a bare string it is a # configuration error, which stops every revive rule from loading rather # than enabling this one. - name: var-naming arguments: - [ID, VM, HTTP, URL, API, TLS, CSS, JSON, SQL, UUID, OIDC, ACL, CPU, RAM, WAC] - name: receiver-naming - name: indent-error-flow - name: range - name: unreachable-code - name: redefines-builtin-id goconst: min-len: 3 # Raised from 4 to 6, and the wire vocabulary below is exempted. The strings # that repeat in this codebase are JSON field names ("status", "version", # "slug"), theme token vocabulary ("primary-hover", "normal") and the auth # method "anonymous". Those are the wire and CSS contract rather than repeated # logic, and hoisting them into Go constants would make them less greppable # without changing anything the compiler checks. min-occurrences: 6 ignore-string-values: - anonymous - none - normal - primary - primary-hover - focus-ring - link - version - status - slug - items - must not be empty # Test data legitimately repeats literals like "200" and "published". ignore-tests: true godot: scope: declarations capital: false gosec: excludes: # G304: a file path from configuration is intentional. The path comes from # the operator's own configuration, not from a request. - G304 # Everything gosec ships is enabled by default; only the exclusions above # narrow it. G115 (integer overflow) is intentionally left on: it fires on # the token engine's length arithmetic, and the two or three places that # could genuinely overflow carry an explicit nolint with a reason. staticcheck: checks: - all # SA1019: deprecation notices for dependencies we control the version of. - -SA1019 misspell: # US, deliberately, and this is a real decision rather than a default. # # misspell's locale setting makes it *require* one dialect. With UK it # rewrites the checker's idea of a correct word, which means it flags every # occurrence of the US spelling -- including identifiers that are part of the # published API contract: the JSON error code "unauthorized", the CSS custom # property and colour-scheme vocabulary, the OpenAPI document. # # The codebase writes prose in British English, which misspell has nothing to # say about under a US locale (it only corrects genuine misspellings, it does # not enforce a dialect). So: British prose, US locale, and no linter # pressure to rename a wire-format string. locale: US # misspell enforces one dialect even under a locale setting, so every British # spelling in the prose has to be listed explicitly. This keeps the check # worth having -- a genuine typo in a comment is still caught -- while # accepting the dialect the documentation is written in. # # No identifier is in this list: exported names and JSON keys are American # throughout, because they are wire and API surface rather than prose. Only # comments, documentation and doc strings use the British forms. ignore-rules: - artefact - artefacts - authorises - behaviour - cancelled - colour - colours - defence - honoured - honouring - honours - initialise - licence - localise - marshalled - materialise - materialises - materialising - maximises - modelled - neutralises - recognised - recognises - sanitise - sanitised - sanitising - serialise - serialised - serialises - summarises exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling rules: # A test function legitimately ignores the error from a deliberate failure. - path: _test\.go linters: - gosec - errcheck # The SQL query constants and their arguments are checked by the store tests, # which assert on behaviour rather than on the statement text. - path: internal/store/ linters: - goconst paths: - third_party$ - builtin$ - examples$ issues: # 0 disables the cap. A linter that stops reporting after 50 findings is worse # than one that reports everything, because the tail is where the surprises are. max-issues-per-linter: 0 max-same-issues: 0 new: false fix: false