Skip to content

Commit 9240234

Browse files
authored
Add .go-version support for the linter configuration (#12)
* Added the linter config template to `dev-tools/templates/.golangci.yml` * Added the `UpdateGoVersion` target to the magefile in order to apply required Go Version changes * Added assertion before linting that makes sure the linter config does not have uncommitted changes In order to change the Go version in the future, it's enough to edit in the `.go-version` file and then run `mage -v UpdateGoVersion` and commit the changes.
1 parent fd72cf7 commit 9240234

File tree

3 files changed

+244
-14
lines changed

3 files changed

+244
-14
lines changed

.golangci.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# DO NOT EDIT!
2+
# This file is a rendered template, the source can be found in "./dev-tools/templates/.golangci.yml"
3+
#
14
# options for analysis running
25
run:
36
# timeout for analysis, e.g. 30s, 5m, default is 1m
@@ -100,7 +103,7 @@ linters-settings:
100103

101104
gosimple:
102105
# Select the Go version to target. The default is '1.13'.
103-
go: "1.17"
106+
go: "1.17.6"
104107

105108
misspell:
106109
# Correct spellings using locale preferences for US or UK.
@@ -135,11 +138,11 @@ linters-settings:
135138

136139
staticcheck:
137140
# Select the Go version to target. The default is '1.13'.
138-
go: "1.17"
141+
go: "1.17.6"
139142

140143
stylecheck:
141144
# Select the Go version to target. The default is '1.13'.
142-
go: "1.17"
145+
go: "1.17.6"
143146

144147
unparam:
145148
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
@@ -150,4 +153,4 @@ linters-settings:
150153

151154
unused:
152155
# Select the Go version to target. The default is '1.13'.
153-
go: "1.17"
156+
go: "1.17.6"

dev-tools/templates/.golangci.yml

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# options for analysis running
2+
run:
3+
# timeout for analysis, e.g. 30s, 5m, default is 1m
4+
timeout: 1m
5+
6+
issues:
7+
# Maximum count of issues with the same text.
8+
# Set to 0 to disable.
9+
# Default: 3
10+
max-same-issues: 0
11+
# Maximum issues count per one linter.
12+
# Set to 0 to disable.
13+
# Default: 50
14+
max-issues-per-linter: 0
15+
16+
output:
17+
sort-results: true
18+
19+
# Uncomment and add a path if needed to exclude
20+
# skip-dirs:
21+
# - some/path
22+
# skip-files:
23+
# - ".*\\.my\\.go$"
24+
# - lib/bad.go
25+
26+
# Find the whole list here https://golangci-lint.run/usage/linters/
27+
linters:
28+
disable-all: true
29+
enable:
30+
- deadcode # finds unused code
31+
- errcheck # checking for unchecked errors in go programs
32+
- errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13.
33+
- goconst # finds repeated strings that could be replaced by a constant
34+
- dupl # tool for code clone detection
35+
- forbidigo # forbids identifiers matched by reg exps
36+
- gomoddirectives # manage the use of 'replace', 'retract', and 'excludes' directives in go.mod.
37+
- gosimple # linter for Go source code that specializes in simplifying a code
38+
- misspell # finds commonly misspelled English words in comments
39+
- nakedret # finds naked returns in functions greater than a specified function length
40+
- prealloc # finds slice declarations that could potentially be preallocated
41+
- nolintlint # reports ill-formed or insufficient nolint directives
42+
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks
43+
- stylecheck # a replacement for golint
44+
- unparam # reports unused function parameters
45+
- unused # checks Go code for unused constants, variables, functions and types
46+
47+
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
48+
- ineffassign # detects when assignments to existing variables are not used
49+
- structcheck # finds unused struct fields
50+
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code
51+
- varcheck # Finds unused global variables and constants
52+
- asciicheck # simple linter to check that your code does not contain non-ASCII identifiers
53+
- bodyclose # checks whether HTTP response body is closed successfully
54+
- durationcheck # check for two durations multiplied together
55+
- exportloopref # checks for pointers to enclosing loop variables
56+
- goimports # Goimports does everything that gofmt does. Additionally it checks unused imports
57+
- gosec # inspects source code for security problems
58+
- importas # enforces consistent import aliases
59+
- nilerr # finds the code that returns nil even if it checks that the error is not nil.
60+
- noctx # noctx finds sending http request without context.Context
61+
- unconvert # Remove unnecessary type conversions
62+
- wastedassign # wastedassign finds wasted assignment statements.
63+
- godox # tool for detection of FIXME, TODO and other comment keywords
64+
65+
# all available settings of specific linters
66+
linters-settings:
67+
errcheck:
68+
# report about not checking of errors in type assertions: `a := b.(MyStruct)`;
69+
# default is false: such cases aren't reported by default.
70+
check-type-assertions: true
71+
72+
errorlint:
73+
# Check whether fmt.Errorf uses the %w verb for formatting errors. See the readme for caveats
74+
errorf: true
75+
# Check for plain type assertions and type switches
76+
asserts: true
77+
# Check for plain error comparisons
78+
comparison: true
79+
80+
goconst:
81+
# minimal length of string constant, 3 by default
82+
min-len: 3
83+
# minimal occurrences count to trigger, 3 by default
84+
min-occurrences: 2
85+
86+
dupl:
87+
# tokens count to trigger issue, 150 by default
88+
threshold: 100
89+
90+
forbidigo:
91+
# Forbid the following identifiers
92+
forbid:
93+
- fmt.Print.* # too much log noise
94+
# Exclude godoc examples from forbidigo checks. Default is true.
95+
exclude_godoc_examples: true
96+
97+
gomoddirectives:
98+
# Allow local `replace` directives. Default is false.
99+
replace-local: false
100+
101+
gosimple:
102+
# Select the Go version to target. The default is '1.13'.
103+
go: "{{.GoVersion}}"
104+
105+
misspell:
106+
# Correct spellings using locale preferences for US or UK.
107+
# Default is to use a neutral variety of English.
108+
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
109+
# locale: US
110+
# ignore-words:
111+
# - IdP
112+
113+
nakedret:
114+
# make an issue if func has more lines of code than this setting and it has naked returns; default is 30
115+
max-func-lines: 0
116+
117+
prealloc:
118+
# Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
119+
# True by default.
120+
simple: true
121+
range-loops: true # Report preallocation suggestions on range loops, true by default
122+
for-loops: false # Report preallocation suggestions on for loops, false by default
123+
124+
nolintlint:
125+
# Enable to ensure that nolint directives are all used. Default is true.
126+
allow-unused: false
127+
# Disable to ensure that nolint directives don't have a leading space. Default is true.
128+
allow-leading-space: true
129+
# Exclude following linters from requiring an explanation. Default is [].
130+
allow-no-explanation: []
131+
# Enable to require an explanation of nonzero length after each nolint directive. Default is false.
132+
require-explanation: true
133+
# Enable to require nolint directives to mention the specific linter being suppressed. Default is false.
134+
require-specific: true
135+
136+
staticcheck:
137+
# Select the Go version to target. The default is '1.13'.
138+
go: "{{.GoVersion}}"
139+
140+
stylecheck:
141+
# Select the Go version to target. The default is '1.13'.
142+
go: "{{.GoVersion}}"
143+
144+
unparam:
145+
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
146+
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
147+
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
148+
# with golangci-lint call it on a directory with the changed file.
149+
check-exported: false
150+
151+
unused:
152+
# Select the Go version to target. The default is '1.13'.
153+
go: "{{.GoVersion}}"

magefile.go

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,26 @@ import (
2626
"errors"
2727
"fmt"
2828
"io"
29+
"io/ioutil"
2930
"log"
3031
"net/http"
3132
"os"
3233
"path/filepath"
34+
"strings"
35+
"text/template"
3336

3437
"github.com/magefile/mage/mg"
3538
"github.com/magefile/mage/sh"
3639
)
3740

3841
const (
39-
linterInstallURL = "https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
40-
linterInstallFilename = "./build/intall-golang-ci.sh"
41-
linterBinaryFilename = "./build/golangci-lint"
42-
linterVersion = "v1.44.0"
42+
linterInstallURL = "https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
43+
linterInstallFilename = "./build/intall-golang-ci.sh"
44+
linterBinaryFilename = "./build/golangci-lint"
45+
linterVersion = "v1.44.0"
46+
linterConfigFilename = "./.golangci.yml"
47+
linterConfigTemplateFilename = "./dev-tools/templates/.golangci.yml"
48+
goVersionFilename = "./.go-version"
4349
)
4450

4551
// Aliases are shortcuts to long target names.
@@ -52,6 +58,57 @@ var Aliases = map[string]interface{}{
5258
// Linter contains targets related to linting the Go code
5359
type Linter mg.Namespace
5460

61+
// UpdateGoVersion updates the linter configuration with the new version of Go.
62+
func (Linter) UpdateGoVersion() error {
63+
goVersionBytes, err := ioutil.ReadFile(goVersionFilename)
64+
if err != nil {
65+
return fmt.Errorf("failed to read the %q file: %w", goVersionFilename, err)
66+
}
67+
goVersion := strings.TrimSpace(string(goVersionBytes))
68+
log.Printf("The Go version is %s\n", goVersion)
69+
70+
templateContext := struct{ GoVersion string }{GoVersion: goVersion}
71+
template, err := template.ParseFiles(linterConfigTemplateFilename)
72+
if err != nil {
73+
return fmt.Errorf("failed to read the template file %q: %w", linterConfigTemplateFilename, err)
74+
}
75+
76+
configFile, err := os.OpenFile(linterConfigFilename, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0700)
77+
if err != nil {
78+
return fmt.Errorf("failed to create/replace the linter config %q: %w", linterConfigFilename, err)
79+
}
80+
defer configFile.Close()
81+
82+
warning := fmt.Sprintf("# DO NOT EDIT!\n# This file is a rendered template, the source can be found in %q\n#\n", linterConfigTemplateFilename)
83+
_, err = configFile.WriteString(warning)
84+
if err != nil {
85+
return fmt.Errorf("failed to write into the linter config %q: %w", linterConfigFilename, err)
86+
}
87+
88+
err = template.Execute(configFile, templateContext)
89+
if err != nil {
90+
return fmt.Errorf("failed to execute the template %q: %w", linterConfigTemplateFilename, err)
91+
}
92+
93+
err = assertUnchanged(linterConfigFilename)
94+
if err != nil {
95+
log.Printf("Successfully updated the linter configuration %q to Go version %s, please commit the changes now", linterConfigFilename, goVersion)
96+
} else {
97+
log.Printf("The linter configuration %q is up to date, no changes made", linterConfigFilename)
98+
}
99+
100+
return nil
101+
}
102+
103+
// CheckConfig makes sure that the `.golangci.yml` does not have uncommitted changes
104+
func (Linter) CheckConfig() error {
105+
err := assertUnchanged(linterConfigFilename)
106+
if err != nil {
107+
return fmt.Errorf("linter configuration has uncommitted changes: %w", err)
108+
}
109+
return nil
110+
}
111+
55112
// Install installs golangci-lint (https://golangci-lint.run) to `./build`
56113
// using the official installation script downloaded from GitHub.
57114
// If the linter binary already exists does nothing.
@@ -64,22 +121,22 @@ func (Linter) Install() error {
64121

65122
_, err = os.Stat(linterBinaryFilename)
66123
if err == nil {
67-
log.Println("the linter has been already installed, skipping...")
124+
log.Println("The linter has been already installed, skipping...")
68125
return nil
69126
}
70127
if !errors.Is(err, os.ErrNotExist) {
71128
return fmt.Errorf("failed check if file %q exists: %w", linterBinaryFilename, err)
72129
}
73130

74-
log.Println("preparing the installation script file...")
131+
log.Println("Preparing the installation script file...")
75132

76133
installScript, err := os.OpenFile(linterInstallFilename, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0700)
77134
if err != nil {
78135
return fmt.Errorf("failed to create file %q: %w", linterInstallFilename, err)
79136
}
80137
defer installScript.Close()
81138

82-
log.Println("downloading the linter installation script...")
139+
log.Println("Downloading the linter installation script...")
83140
// nolint: noctx // valid use since there is no context
84141
resp, err := http.Get(linterInstallURL)
85142
if err != nil {
@@ -110,14 +167,14 @@ func (Linter) Install() error {
110167

111168
// All runs the linter against the entire codebase
112169
func (l Linter) All() error {
113-
mg.Deps(l.Install)
170+
mg.Deps(l.Install, l.CheckConfig)
114171
return runLinter()
115172
}
116173

117174
// LastChange runs the linter against all files changed since the fork point from `main`.
118175
// If the current branch is `main` then runs against the files changed in the last commit.
119176
func (l Linter) LastChange() error {
120-
mg.Deps(l.Install)
177+
mg.Deps(l.Install, l.CheckConfig)
121178

122179
branch, err := sh.Output("git", "branch", "--show-current")
123180
if err != nil {
@@ -146,6 +203,13 @@ func Check() error {
146203
return nil
147204
}
148205

206+
// UpdateGoVersion makes required changes in order to switch to a new version of Go set in `./.go-version`.
207+
// nolint: deadcode,unparam // it's used as a `mage` target and requires returning an error
208+
func UpdateGoVersion() error {
209+
mg.Deps(Linter.UpdateGoVersion)
210+
return nil
211+
}
212+
149213
// Deps contains targets related to checking dependencies
150214
type Deps mg.Namespace
151215

@@ -182,14 +246,23 @@ func (Deps) CheckModuleTidy() error {
182246
if err != nil {
183247
return err
184248
}
185-
err = sh.Run("git", "diff", "--exit-code", "go.mod")
249+
err = assertUnchanged("go.mod")
186250
if err != nil {
187251
return fmt.Errorf("`go mod tidy` was not called before the last commit: %w", err)
188252
}
189253

190254
return nil
191255
}
192256

257+
func assertUnchanged(path string) error {
258+
err := sh.Run("git", "diff", "--exit-code", path)
259+
if err != nil {
260+
return fmt.Errorf("failed to assert the unchanged file %q: %w", path, err)
261+
}
262+
263+
return nil
264+
}
265+
193266
// runWithStdErr runs a command redirecting its stderr to the console instead of discarding it
194267
func runWithStdErr(command string, args ...string) error {
195268
_, err := sh.Exec(nil, os.Stdout, os.Stderr, command, args...)
@@ -207,6 +280,7 @@ func runLinter(runFlags ...string) error {
207280

208281
args = append(args, "run")
209282
args = append(args, runFlags...)
283+
args = append(args, "-c", linterConfigFilename)
210284
args = append(args, "./...")
211285

212286
return runWithStdErr(linterBinaryFilename, args...)

0 commit comments

Comments
 (0)