···1111 extra-rules: true
12121313 golines:
1414- max-len: 140
1414+ max-len: 120
15151616linters:
1717 default: all
1818 disable:
1919+ - cyclop # gocognit is the single complexity linter, cognitive beats cyclomatic
1920 - decorder # Don't care about this
2020- - dupl # Basically every table driven test ever triggers this
2121- - dupword # Messes with test cases more often than not
2222- - err113 # Out of date
2323- - exhaustruct # No
2121+ - err113 # Not everything needs this
2222+ - exhaustruct # This is just a bad idea
2423 - forbidigo # Nothing to forbid
2524 - funlen # Bad metric for complexity
2626- - ginkgolinter # I don't use whatever this is
2727- - gochecknoglobals # Globals are fine sometimes, use common sense
2828- - gocyclo # cyclop does this instead
2525+ - goconst # Got significantly more annoying recently
2626+ - gocyclo # Prefer cognitive complexity over naive metrics
2927 - godox # "todo" and "fixme" comments are allowed
3030- - goheader # No need
3128 - gomodguard # Deprecated
3232- - gosmopolitan # No need
3333- - grouper # Imports take care of themselves, rest is common sense
3434- - ireturn # This is just not necessary or practical in a real codebase
2929+ - ireturn # AST is full of interfaces we need to return (e.g. Expression)
3530 - lll # Auto formatters do this and what they can't do I don't care about
3636- - maintidx # This is just the inverse of complexity... which is cyclop
3737- - nestif # cyclop does this
3838- - nlreturn # Similar to wsl, I think best left to judgement
3131+ - maintidx # Prefer cognitive
3232+ - mnd # Annoying and too sensitive
3333+ - nestif # Nesting depth is the dominant factor in gocognit's score already
3934 - noinlineerr # Inline errors are fine
4040- - nonamedreturns # Named returns are often helpful, it's naked returns that are the issue
3535+ - nonamedreturns # Named returns are often helpful documentation, it's naked returns that are the issue
4136 - paralleltest # I've never had Go tests take longer than a few seconds, it's fine
4237 - unparam # gopls is better and more subtle
4338 - varnamelen # Lots of false positives of things that are fine
4439 - wrapcheck # Not every error must be wrapped
4545- - wsl # Very aggressive, some of this I like but tend to do anyway
4646- - wsl_v5 # As above, just newer version
4040+ - wsl # Deprecated
47414842 exclusions:
4943 presets:
5044 # See https://golangci-lint.run/usage/false-positives/#exclusion-presets
5151- - comments # Revive in particular has lots of false positives
5252- - std-error-handling
5345 - common-false-positives
4646+ - std-error-handling
5447 rules:
5548 - path: _test\.go
5649 linters:
5757- - prealloc # These kinds of optimisations will make no difference to test code
5050+ - dupl # Common false positives with table driven tests
5151+ - dupword # Messes with test cases more often than not
5252+ - gochecknoglobals # Flags and env vars
5353+ - goconst # Sometimes repetition is okay in tests
5854 - gosec # Tests don't need security stuff
5555+ - prealloc # These kinds of optimisations will make no difference to test code
59566057 settings:
6161- cyclop:
6262- max-complexity: 20
6363-6458 depguard:
6559 rules:
6660 main:
···7266 desc: use math/rand/v2 instead
73677468 errcheck:
7575- check-type-assertions: true
7669 check-blank: true
7070+ check-type-assertions: true
77717872 exhaustive:
7973 check:
8080- - switch
8174 - map
7575+ - switch
8276 default-signifies-exhaustive: true
8383-8484- staticcheck:
8585- checks:
8686- - all
87778878 gosec:
8979 excludes:
···10090 require-explanation: true
10191 require-specific: true
102929393+ revive:
9494+ # every other revive rule overlaps one or more of the other
9595+ # linters. These are the ones nothing else covers.
9696+ enable-all-rules: false
9797+ rules:
9898+ - name: context-as-argument # ctx must be the first parameter
9999+ - name: deep-exit # os.Exit / log.Fatal outside main and init
100100+ - name: modifies-parameter # don't mutate value parameters
101101+ - name: modifies-value-receiver # value receiver mutations are lost
102102+ - name: unexported-return # exported func returning an unexported type
103103+104104+ staticcheck:
105105+ checks:
106106+ - all
107107+103108 usetesting:
104109 context-background: true
105110 context-todo: true
106111 os-chdir: true
112112+ os-create-temp: true
107113 os-mkdir-temp: true
108114 os-setenv: true
109109- os-create-temp: true
110115 os-temp-dir: true
111111-112112- revive:
113113- max-open-files: 256
114114- enable-all-rules: true
115115- rules:
116116- - name: add-constant
117117- disabled: true # goconst does this
118118-119119- - name: argument-limit
120120- arguments:
121121- - 5
122122-123123- - name: cognitive-complexity
124124- disabled: true # gocognit does this
125125-126126- - name: comment-spacings
127127- arguments:
128128- - "nolint:"
129129-130130- - name: cyclomatic
131131- disabled: true # cyclop does this
132132-133133- - name: exported
134134- arguments:
135135- - checkPrivateReceivers
136136- - checkPublicInterface
137137-138138- - name: function-length
139139- disabled: true # Bad proxy for complexity
140140-141141- - name: function-result-limit
142142- arguments:
143143- - 3
144144-145145- - name: import-shadowing
146146- disabled: true # predeclared does this
147147-148148- - name: line-length-limit
149149- disabled: true # gofmt/golines handles this well enough
150150-151151- - name: max-public-structs
152152- disabled: true # This is a dumb rule
153153-154154- - name: redefines-builtin-id
155155- disabled: true # predeclared does this
156156-157157- - name: unhandled-error
158158- arguments:
159159- - fmt\.(Fp|P)rint(ln|f)?
160160- - strings.Builder.Write(String|Byte)?
161161- - bytes.Buffer.Write(String|Byte)?
162162-163163- - name: flag-parameter
164164- disabled: true # As far as I can work out this just doesn't like bools
165165-166166- - name: unused-parameter
167167- disabled: true # The gopls unused analyzer covers this better
168168-169169- - name: unused-receiver
170170- disabled: true # As above
171171-172172- - name: var-naming
173173- disabled: true
-112
Taskfile.yml
···11-# https://taskfile.dev
22-33-version: "3"
44-55-vars:
66- COV_DATA: coverage.out
77-88-tasks:
99- default:
1010- desc: List all available tasks
1111- silent: true
1212- cmds:
1313- - task --list
1414-1515- tidy:
1616- desc: Tidy dependencies in go.mod and go.sum
1717- sources:
1818- - "**/*.go"
1919- - go.mod
2020- - go.sum
2121- cmds:
2222- - go mod tidy
2323-2424- fmt:
2525- desc: Run go fmt on all source files
2626- sources:
2727- - "**/*.go"
2828- - .golangci.yml
2929- cmds:
3030- - golangci-lint fmt
3131-3232- test:
3333- desc: Run the test suite
3434- sources:
3535- - "**/*.go"
3636- - go.mod
3737- - go.sum
3838- - "**/testdata/**/*"
3939- cmds:
4040- - go test -race ./... {{ .CLI_ARGS }}
4141-4242- bench:
4343- desc: Run all project benchmarks
4444- sources:
4545- - "**/*.go"
4646- cmds:
4747- - go test ./... -run None -benchmem -bench . {{ .CLI_ARGS }}
4848-4949- lint:
5050- desc: Run the linters and auto-fix if possible
5151- sources:
5252- - "**/*.go"
5353- - .golangci.yml
5454- preconditions:
5555- - sh: command -v golangci-lint
5656- msg: golangci-lint not installed, see https://golangci-lint.run/usage/install/#local-installation
5757-5858- - sh: command -v typos
5959- msg: requires typos-cli, run `brew install typos-cli`
6060- cmds:
6161- - golangci-lint run --fix
6262- - typos
6363-6464- doc:
6565- desc: Render the pkg docs locally
6666- preconditions:
6767- - sh: command -v pkgsite
6868- msg: pkgsite not installed, run `go install golang.org/x/pkgsite/cmd/pkgsite@latest`
6969- cmds:
7070- - pkgsite -open
7171-7272- demo:
7373- desc: Render the demo gifs
7474- sources:
7575- - ./docs/src/*.tape
7676- - "**/*.go"
7777- preconditions:
7878- - sh: command -v vhs
7979- msg: vhs not installed, see https://github.com/charmbracelet/vhs
8080- cmds:
8181- - for file in ./docs/src/*.tape; do vhs "$file"; done
8282-8383- cov:
8484- desc: Calculate test coverage and render the html
8585- generates:
8686- - "{{ .COV_DATA }}"
8787- cmds:
8888- - go test -race -cover -covermode atomic -coverprofile {{ .COV_DATA }} ./...
8989- - go tool cover -html {{ .COV_DATA }}
9090-9191- check:
9292- desc: Run tests and linting in one
9393- cmds:
9494- - task: test
9595- - task: lint
9696-9797- sloc:
9898- desc: Print lines of code
9999- cmds:
100100- - fd . -e go | xargs wc -l | sort -nr | head
101101-102102- clean:
103103- desc: Remove build artifacts and other clutter
104104- cmds:
105105- - go clean ./...
106106- - rm -rf {{ .COV_DATA }}
107107-108108- update:
109109- desc: Updates dependencies in go.mod and go.sum
110110- cmds:
111111- - go get -u ./...
112112- - go mod tidy
+78
mise.toml
···11+# Dev tasks. Run `mise tasks` to list, `mise run <task>` to execute.
22+# Extra args after `--` are appended to the task's command:
33+# mise run test -- -run TestFoo
44+# mise run bench -- -benchtime 5s
55+66+[tools]
77+typos = "latest"
88+"go:golang.org/x/pkgsite/cmd/pkgsite" = "latest"
99+golangci-lint = "latest"
1010+1111+[vars]
1212+COV_DATA = "coverage.out"
1313+1414+[tasks.tidy]
1515+description = "Tidy dependencies in go.mod and go.sum"
1616+sources = ["**/*.go", "go.mod", "go.sum"]
1717+run = "go mod tidy"
1818+1919+[tasks.gen]
2020+description = "Run go generate"
2121+sources = ["**/*.go"]
2222+run = "go generate ./..."
2323+2424+[tasks.fmt]
2525+description = "Run go fmt on all source files"
2626+sources = ["**/*.go", ".golangci.yml"]
2727+run = "golangci-lint fmt ./..."
2828+2929+[tasks.test]
3030+description = "Run the test suite"
3131+sources = [
3232+ "**/*.go",
3333+ "go.mod",
3434+ "go.sum",
3535+ "**/testdata/**/*",
3636+]
3737+# -race needs CGO (https://go.dev/doc/articles/race_detector#Requirements)
3838+env = { CGO_ENABLED = "1" }
3939+run = "go test -race ./..."
4040+4141+[tasks.bench]
4242+description = "Run all project benchmarks"
4343+sources = ["**/*.go"]
4444+run = "go test ./... -run None -benchmem -bench ."
4545+4646+[tasks.lint]
4747+description = "Run linting"
4848+depends = ["fmt"]
4949+sources = ["**/*.go", ".golangci.yml"]
5050+run = ["golangci-lint run --fix", "typos"]
5151+5252+[tasks.doc]
5353+description = "Render the pkg docs locally"
5454+run = "pkgsite -open"
5555+5656+[tasks.cov]
5757+description = "Calculate test coverage and render the html"
5858+outputs = ["{{ vars.COV_DATA }}"]
5959+run = [
6060+ "go test -race -cover -covermode atomic -coverprofile {{ vars.COV_DATA }} ./...",
6161+ "go tool cover -html {{ vars.COV_DATA }}",
6262+]
6363+6464+[tasks.check]
6565+description = "Run tests and linting in one"
6666+depends = ["test", "lint"]
6767+6868+[tasks.sloc]
6969+description = "Print lines of code"
7070+run = "fd . -e go | xargs wc -l | sort -nr | head"
7171+7272+[tasks.clean]
7373+description = "Remove build artifacts and other clutter"
7474+run = ["go clean ./...", "rm -rf {{ vars.COV_DATA }} ./bin ./dist"]
7575+7676+[tasks.update]
7777+description = "Updates dependencies in go.mod and go.sum"
7878+run = ["go get -u ./...", "go mod tidy"]
+3
spin.go
···7777 // Store the frames and the index locally so no need for synchronisation.
7878 frames := [...]string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
7979 current := 0
8080+8081 ticker := time.NewTicker(frameRate)
8182 defer ticker.Stop()
8383+8284 for {
8385 select {
8486 case <-s.stop:
···109111func (s *Spinner) Do(fn func()) {
110112 s.Start()
111113 defer s.Stop()
114114+112115 fn()
113116}
114117
+10
spin_test.go
···2424 t.Run(tt.name, func(t *testing.T) {
2525 synctest.Test(t, func(t *testing.T) {
2626 var buf bytes.Buffer
2727+2728 s := spin.New(&buf, tt.msg, spin.WithForceEnabled())
2829 s.Start()
2930 time.Sleep(250 * time.Millisecond)
···3435 if !strings.Contains(output, tt.msg) {
3536 t.Errorf("output %q does not contain message %q", output, tt.msg)
3637 }
3838+3739 if !strings.HasSuffix(output, eraseSeq) {
3840 t.Errorf("output %q does not end with ANSI erase sequence", output)
3941 }
···44464547func TestSpinnerStopWhenNotRunningIsNoOp(t *testing.T) {
4648 var buf bytes.Buffer
4949+4750 s := spin.New(&buf, "Loading", spin.WithForceEnabled())
4851 s.Stop() // must not panic or write anything
4952···5558func TestSpinnerStopCalledTwiceIsNoOp(t *testing.T) {
5659 synctest.Test(t, func(t *testing.T) {
5760 var buf bytes.Buffer
6161+5862 s := spin.New(&buf, "Loading", spin.WithForceEnabled())
5963 s.Start()
6064 time.Sleep(250 * time.Millisecond)
···7478func TestSpinnerStartWhenAlreadyRunningIsNoOp(t *testing.T) {
7579 synctest.Test(t, func(t *testing.T) {
7680 var buf bytes.Buffer
8181+7782 s := spin.New(&buf, "Loading", spin.WithForceEnabled())
7883 s.Start()
7984 s.Start() // second call must not start a second goroutine
···90959196func TestSpinnerDoExecutesFunction(t *testing.T) {
9297 var buf bytes.Buffer
9898+9399 s := spin.New(&buf, "Loading", spin.WithForceEnabled())
9410095101 ran := false
102102+96103 s.Do(func() { ran = true })
9710498105 if !ran {
···103110func TestSpinnerDoStartsAndStopsAroundFunction(t *testing.T) {
104111 synctest.Test(t, func(t *testing.T) {
105112 var buf bytes.Buffer
113113+106114 s := spin.New(&buf, "Loading", spin.WithForceEnabled())
107115108116 s.Do(func() { time.Sleep(250 * time.Millisecond) })
···111119 if !strings.Contains(output, "Loading") {
112120 t.Errorf("output %q does not contain message", output)
113121 }
122122+114123 if !strings.HasSuffix(output, eraseSeq) {
115124 t.Errorf("output %q does not end with erase sequence", output)
116125 }
···120129func TestSpinnerNonTerminalWriterProducesNoOutput(t *testing.T) {
121130 synctest.Test(t, func(t *testing.T) {
122131 var buf bytes.Buffer
132132+123133 s := spin.New(&buf, "Loading") // no WithForceEnabled — Start is a no-op
124134 s.Start()
125135 time.Sleep(250 * time.Millisecond)