diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index af5e27c..776b454 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -10,20 +10,20 @@ jobs: - uses: actions/setup-go@v6 with: - go-version: '1.25' + go-version: '1.26' cache: false - name: Lint uses: golangci/golangci-lint-action@v9 with: - version: v2.10 + version: v2.11 only-new-issues: true test: runs-on: ubuntu-latest steps: - uses: actions/setup-go@v6 with: - go-version: '1.25' + go-version: '1.26' - uses: actions/checkout@v6 with: diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..7984c80 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,206 @@ +# https://golangci-lint.run/usage/configuration/ + +# options for analysis running +# https://golangci-lint.run/usage/configuration/#run-configuration +version: "2" + +run: + # include test files or not, default is true + tests: true + + # by default isn't set. If set we pass it to "go list -mod={option}". From "go + # help modules": If invoked with -mod=readonly, the go command is disallowed + # from the implicit automatic updating of go.mod described above. Instead, it + # fails when any changes to go.mod are needed. This setting is most useful to + # check that go.mod does not need updates, such as in a continuous integration + # and testing system. If invoked with -mod=vendor, the go command assumes that + # the vendor directory holds the correct copies of dependencies and ignores + # the dependency descriptions in go.mod. + modules-download-mode: vendor + + # Allow multiple parallel golangci-lint instances running. If false, + # golangci-lint acquires file lock on start. + allow-parallel-runners: true + + # Number of operating system threads (`GOMAXPROCS`) that can execute + # golangci-lint simultaneously. If it is explicitly set to 0 (i.e. not the + # default) then golangci-lint will automatically set the value to match Linux + # container CPU quota. + concurrency: 2 + +linters: + # Default set of linters. + # The value can be: `standard`, `all`, `none`, or `fast`. + # Default: standard + default: standard + + # Enable specific linters. + # https://golangci-lint.run/usage/linters/#enabled-by-default + enable: + - govet + - revive + - unused + - errcheck + - staticcheck + - ineffassign + - unconvert + - misspell + - lll + - nakedret + - gocritic + - nolintlint + - gocyclo + - copyloopvar + - usestdlibvars + + # all available settings of specific linters + settings: + # https://golangci-lint.run/usage/linters/#lll + lll: + # max line length, lines longer will be reported. Default is 120. '\t' is + # counted as 1 character by default and can be changed with the tab-width + # option + line-length: 120 + + # Tab width in spaces. + tab-width: 2 + + # https://golangci-lint.run/usage/linters/#gocyclo + gocyclo: + # Using a high number for now only to cover the existing codebase. We + # already have a plan to target a lower number. The value was chosen from + # the following command execution: + # + # gocyclo -top 1 . + min-complexity: 65 + + # https://golangci-lint.run/usage/linters/#revive + revive: + # Maximum number of open files at the same time. + max-open-files: 1024 + + # Run `GL_DEBUG=revive golangci-lint run --enable-only=revive` to see default, all available rules, and enabled rules. + rules: + # https://github.com/mgechev/revive/blob/HEAD/RULES_DESCRIPTIONS.md#constant-logical-expr + - name: constant-logical-expr + severity: warning + disabled: false + exclude: + - "" + + # https://github.com/mgechev/revive/blob/HEAD/RULES_DESCRIPTIONS.md#datarace + - name: datarace + severity: warning + disabled: false + exclude: + - "" + + # https://github.com/mgechev/revive/blob/HEAD/RULES_DESCRIPTIONS.md#exported + - name: exported + arguments: + - disableStutteringCheck + - disableChecksOnTypes + - disableChecksOnVariables + - disableChecksOnConstants + severity: warning + disabled: false + exclude: + - "" + + # https://github.com/mgechev/revive/blob/HEAD/RULES_DESCRIPTIONS.md#if-return + - name: if-return + severity: warning + disabled: false + exclude: + - "" + + # https://github.com/mgechev/revive/blob/HEAD/RULES_DESCRIPTIONS.md#redundant-import-alias + - name: redundant-import-alias + severity: warning + disabled: false + exclude: + - "" + + # https://github.com/mgechev/revive/blob/HEAD/RULES_DESCRIPTIONS.md#superfluous-else + - name: superfluous-else + arguments: + - preserveScope + severity: warning + disabled: false + exclude: + - "" + + # https://github.com/mgechev/revive/blob/HEAD/RULES_DESCRIPTIONS.md#unreachable-code + - name: unreachable-code + severity: warning + disabled: false + exclude: + - "" + + # https://github.com/mgechev/revive/blob/HEAD/RULES_DESCRIPTIONS.md#unused-parameter + - name: unused-parameter + arguments: + - allowRegex: ^_ + severity: warning + disabled: false + exclude: + - "" + + # https://golangci-lint.run/usage/linters/#unused + unused: + # Mark all struct fields that have been written to as used. + # Setting it to 'false' will report fields with writes but no reads (it might have false positives) + # Default: true + field-writes-are-uses: true + # Mark all exported fields as used. + # default: true + exported-fields-are-used: false + # Mark all local variables as used. + # default: true + local-variables-are-used: false + + # Defines a set of rules to ignore issues. + # It does not skip the analysis, and so does not ignore "typecheck" errors. + exclusions: + # Mode of the generated files analysis. + # + # - `strict`: sources are excluded by strictly following the Go generated file convention. + # Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$` + # This line must appear before the first non-comment, non-blank text in the file. + # https://go.dev/s/generatedcode + # - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc. + # - `disable`: disable the generated files exclusion. + # + # Default: lax + generated: lax + + # Log a warning if an exclusion rule is unused. + # Default: false + warn-unused: true + +issues: + # Maximum count of issues with the same text. Setting this to zero will disable + # any limits, meaning all errors will be reported. This is useful if a linter + # always produces the same error message, or else some errors might be filtered + max-same-issues: 0 + +formatters: + # Enable specific formatter. + # Default: [] (uses standard Go formatting) + enable: + - goimports + + # Defines a set of rules to ignore issues. + # It does not skip the analysis, and so does not ignore "typecheck" errors. + exclusions: + # Mode of the generated files analysis. + # + # - `strict`: sources are excluded by strictly following the Go generated file convention. + # Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$` + # This line must appear before the first non-comment, non-blank text in the file. + # https://go.dev/s/generatedcode + # - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc. + # - `disable`: disable the generated files exclusion. + # + # Default: lax + generated: lax diff --git a/docparse/docparse.go b/docparse/docparse.go index 3dd847b..0fbd43e 100644 --- a/docparse/docparse.go +++ b/docparse/docparse.go @@ -124,18 +124,18 @@ type Ref struct { // Main reason to store as a string (and Refs as a map) for now is so that // it looks pretties in the pretty.Print() output. May not want to keep // this. - Reference string //*Reference + Reference string // *Reference } // Param is a path, query, or form parameter. type Param struct { Name string // Parameter name - //Info string // Detailed description - //Required bool // Is this required to always be sent? - //Kind string // Type information - //KindEnum []string // Enum fields, only when Kind=enum. - //Format string // Format, such as "email", "date", etc. - //Ref string // Reference something else; for Kind=struct and Kind=array. + // Info string // Detailed description + // Required bool // Is this required to always be sent? + // Kind string // Type information + // KindEnum []string // Enum fields, only when Kind=enum. + // Format string // Format, such as "email", "date", etc. + // Ref string // Reference something else; for Kind=struct and Kind=array. KindField *ast.Field // Type information from struct field. } diff --git a/docparse/docparse_test.go b/docparse/docparse_test.go index fb869f4..1f819e6 100644 --- a/docparse/docparse_test.go +++ b/docparse/docparse_test.go @@ -291,7 +291,7 @@ Response 400 (w00t): {empty} }}, }, - //{"err-double-code", ` + // {"err-double-code", ` // POST /path // Response 200: {empty} diff --git a/docparse/find.go b/docparse/find.go index 94276e3..b951b0c 100644 --- a/docparse/find.go +++ b/docparse/find.go @@ -555,7 +555,7 @@ func GetReference(prog *Program, context string, isEmbed bool, lookup, filePath split := strings.Split(p.Reference, ".") lookupStruct := strings.Join(split[:len(split)-1], ".") if lookupStruct != "" { - lookupStruct = lookupStruct + "." + lookupStruct += "." } for i, f := range ref.Fields { diff --git a/docparse/find_test.go b/docparse/find_test.go index bf8ed6c..5369efd 100644 --- a/docparse/find_test.go +++ b/docparse/find_test.go @@ -3,9 +3,10 @@ package docparse import ( "io" "os" + "os/exec" "path/filepath" "reflect" - "runtime" + "strings" "testing" "github.com/teamwork/test" @@ -47,7 +48,7 @@ func TestFindType(t *testing.T) { if pkg != "net/http" { t.Fatalf("pkg == %v", pkg) } - if path != filepath.Join(runtime.GOROOT(), "src", "net", "http", "header.go") { + if path != filepath.Join(goroot(t), "src", "net", "http", "header.go") { t.Fatalf("path == %v", path) } @@ -126,3 +127,14 @@ func TestFindType(t *testing.T) { } }) } + +// goroot is a small helper to get the GOROOT environment variable. +func goroot(t *testing.T) string { + t.Helper() + + out, err := exec.Command("go", "env", "GOROOT").Output() + if err != nil { + t.Fatalf("go env GOROOT: %v", err) + } + return strings.TrimSpace(string(out)) +} diff --git a/docparse/jsonschema.go b/docparse/jsonschema.go index b262f18..a91984c 100644 --- a/docparse/jsonschema.go +++ b/docparse/jsonschema.go @@ -388,12 +388,10 @@ start: pkg = importPath } - switch resolvType := ts.Type.(type) { - case *ast.ArrayType: + if resolvType, ok := ts.Type.(*ast.ArrayType); ok { isEnum := p.Type == "enum" p.Type = "array" - err := resolveArray(prog, ref, pkg, &p, resolvType.Elt, isEnum, generics) - if err != nil { + if err = resolveArray(prog, ref, pkg, &p, resolvType.Elt, isEnum, generics); err != nil { return nil, err } @@ -489,7 +487,8 @@ start: default: return nil, fmt.Errorf("unknown generic type: %T", typ.X) } - if err := fillGenericsSchema(prog, &p, tagName, ref, genericsPkg, genericsIdent, generics, typ.Indices...); err != nil { + err = fillGenericsSchema(prog, &p, tagName, ref, genericsPkg, genericsIdent, generics, typ.Indices...) + if err != nil { return nil, fmt.Errorf("generic fieldToSchema: %v", err) } return &p, nil @@ -527,7 +526,7 @@ start: // fillGenericsSchema fills the schema with the generic type information. As the // types can be different for every generics declaration they will need to be a -// anonymos object in the schema output instead of a reusable reference. +// anonymous object in the schema output instead of a reusable reference. func fillGenericsSchema( prog *Program, p *Schema, diff --git a/docparse/test.go b/docparse/test.go index 5b4231b..9bf0998 100644 --- a/docparse/test.go +++ b/docparse/test.go @@ -1,9 +1,10 @@ -// nolint package docparse // For tests. We don't parse test files. // testObject general documentation. +// +//nolint:unused type testObject struct { // ID documentation {required}. ID int diff --git a/docparse/testdata/src/a/a.go b/docparse/testdata/src/a/a.go index f8dafce..0984578 100644 --- a/docparse/testdata/src/a/a.go +++ b/docparse/testdata/src/a/a.go @@ -1,4 +1,3 @@ -// nolint package a import "net/mail" @@ -38,7 +37,7 @@ type foo struct { // {enum: one two three // four five six seven} docs string - //m map[string]int + // m map[string]int } type nested struct { diff --git a/example/example.go b/example/example.go index 1581b4e..e50c59d 100644 --- a/example/example.go +++ b/example/example.go @@ -1,4 +1,4 @@ -// nolint +//nolint:unused,revive package example import ( diff --git a/example/unified.go b/example/unified.go index ca01599..06fe593 100644 --- a/example/unified.go +++ b/example/unified.go @@ -1,4 +1,4 @@ -// nolint +//nolint:unused,revive package example // Single entity response. diff --git a/go.mod b/go.mod index b794067..63621b6 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/teamwork/kommentaar -go 1.25.0 +go 1.26 require ( github.com/imdario/mergo v0.3.13 diff --git a/html/html.go b/html/html.go index 4638a74..8b44760 100644 --- a/html/html.go +++ b/html/html.go @@ -9,12 +9,12 @@ import ( "os" "github.com/teamwork/kommentaar/docparse" - yaml "gopkg.in/yaml.v3" + "gopkg.in/yaml.v3" ) var funcMap = template.FuncMap{ "add": func(a, b int) int { return a + b }, - "status": func(c int) string { return http.StatusText(c) }, + "status": http.StatusText, "schema": func(in interface{}) string { // TODO: link ref? d, err := yaml.Marshal(in) @@ -301,7 +301,7 @@ func WriteHTML(w io.Writer, prog *docparse.Program) error { // ServeHTML serves HTML documentation at addr. func ServeHTML(addr string) func(io.Writer, *docparse.Program) error { return func(_ io.Writer, prog *docparse.Program) error { - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + http.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { // Rescan, but first clear some fields so we don't end up with // duplicate data. prog.Config.Output = func(io.Writer, *docparse.Program) error { @@ -312,7 +312,7 @@ func ServeHTML(addr string) func(io.Writer, *docparse.Program) error { err := docparse.FindComments(os.Stdout, prog) if err != nil { - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) _, wErr := fmt.Fprintf(w, "could not parse comments: %v", err) if wErr != nil { _, _ = fmt.Fprintf(os.Stderr, "could not write response: %v", wErr) diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index dc6e041..2ab0d08 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -9,13 +9,14 @@ import ( "encoding/json" "fmt" "io" + "net/http" "sort" "strings" "github.com/imdario/mergo" "github.com/teamwork/kommentaar/docparse" "github.com/teamwork/utils/v2/goutil" - yaml "gopkg.in/yaml.v3" + "gopkg.in/yaml.v3" ) type ( @@ -362,7 +363,7 @@ func write(outFormat string, w io.Writer, prog *docparse.Program) error { Name: param, In: "path", Type: "integer", - //Format: "int64", + // Format: "int64", Required: true, }) } @@ -424,17 +425,17 @@ func write(outFormat string, w io.Writer, prog *docparse.Program) error { } switch e.Method { - case "GET": + case http.MethodGet: out.Paths[e.Path].Get = &op - case "POST": + case http.MethodPost: out.Paths[e.Path].Post = &op - case "PUT": + case http.MethodPut: out.Paths[e.Path].Put = &op - case "PATCH": + case http.MethodPatch: out.Paths[e.Path].Patch = &op - case "DELETE": + case http.MethodDelete: out.Paths[e.Path].Delete = &op - case "HEAD": + case http.MethodHead: out.Paths[e.Path].Head = &op default: return fmt.Errorf("unknown method: %#v", e.Method) @@ -498,7 +499,7 @@ func write(outFormat string, w io.Writer, prog *docparse.Program) error { func makeID(e *docparse.Endpoint) string { return strings.Replace(fmt.Sprintf("%v_%v", e.Method, - strings.Replace(e.Path, "/", "_", -1)), "__", "_", 1) + strings.ReplaceAll(e.Path, "/", "_")), "__", "_", 1) } func appendIfNotExists(xs []string, y string) []string { diff --git a/srvhttp/srvhttp.go b/srvhttp/srvhttp.go index 88a3b5d..80d7494 100644 --- a/srvhttp/srvhttp.go +++ b/srvhttp/srvhttp.go @@ -26,7 +26,7 @@ type Args struct { // YAML outputs as OpenAPI2 YAML. func YAML(args Args) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, _ *http.Request) { out, err := run(args, openapi2.WriteYAML, args.YAMLFile) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -78,7 +78,7 @@ func JSON(args Args) http.HandlerFunc { // HTML outputs as HTML documentation. func HTML(args Args) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, _ *http.Request) { out, err := run(args, html.WriteHTML, args.HTMLFile) if err != nil { w.WriteHeader(http.StatusInternalServerError) diff --git a/srvhttp/srvhttp_test.go b/srvhttp/srvhttp_test.go index 34c3e65..8aac721 100644 --- a/srvhttp/srvhttp_test.go +++ b/srvhttp/srvhttp_test.go @@ -1,6 +1,7 @@ package srvhttp import ( + "net/http" "net/http/httptest" "os" "testing" @@ -9,7 +10,7 @@ import ( ) func TestServe(t *testing.T) { - r := httptest.NewRequest("GET", "/", nil) + r := httptest.NewRequest(http.MethodGet, "/", nil) args := Args{ Packages: []string{"../example/..."}, } @@ -34,7 +35,7 @@ func TestServe(t *testing.T) { } func TestFromFile(t *testing.T) { - r := httptest.NewRequest("GET", "/", nil) + r := httptest.NewRequest(http.MethodGet, "/", nil) args := Args{ Packages: []string{"../example/..."}, NoScan: true,