From 27ea03af6df11315fcb7709173834fc59cf11bb7 Mon Sep 17 00:00:00 2001 From: Joshua Sing Date: Sat, 19 Apr 2025 15:42:10 +1000 Subject: [PATCH 1/3] feat(golinters): add golicenser --- .golangci.next.reference.yml | 86 ++++++++++++++ go.mod | 2 + go.sum | 4 + jsonschema/golangci.next.jsonschema.json | 105 ++++++++++++++++++ pkg/config/linters_settings.go | 24 ++++ pkg/golinters/golicenser/golicenser.go | 88 +++++++++++++++ .../golicenser/golicenser_integration_test.go | 19 ++++ .../testdata/fix/in/golicenser_1.go | 4 + .../testdata/fix/in/golicenser_2.go | 13 +++ .../testdata/fix/in/golicenser_3.go | 7 ++ .../testdata/fix/in/golicenser_4.go | 8 ++ .../testdata/fix/in/golicenser_5.go | 9 ++ .../testdata/fix/out/golicenser_1.go | 7 ++ .../testdata/fix/out/golicenser_2.go | 13 +++ .../testdata/fix/out/golicenser_3.go | 10 ++ .../testdata/fix/out/golicenser_4.go | 8 ++ .../testdata/fix/out/golicenser_5.go | 7 ++ .../golicenser/testdata/golicenser-fix.yml | 16 +++ .../golicenser/testdata/golicenser.yml | 20 ++++ .../golicenser/testdata/golicenser_cgo.go | 35 ++++++ .../testdata/golicenser_different.go | 8 ++ .../golicenser/testdata/golicenser_good.go | 7 ++ .../golicenser/testdata/golicenser_missing.go | 5 + .../testdata/golicenser_outdated.go | 12 ++ pkg/lint/lintersdb/builder_linter.go | 6 + 25 files changed, 523 insertions(+) create mode 100644 pkg/golinters/golicenser/golicenser.go create mode 100644 pkg/golinters/golicenser/golicenser_integration_test.go create mode 100644 pkg/golinters/golicenser/testdata/fix/in/golicenser_1.go create mode 100644 pkg/golinters/golicenser/testdata/fix/in/golicenser_2.go create mode 100644 pkg/golinters/golicenser/testdata/fix/in/golicenser_3.go create mode 100644 pkg/golinters/golicenser/testdata/fix/in/golicenser_4.go create mode 100644 pkg/golinters/golicenser/testdata/fix/in/golicenser_5.go create mode 100644 pkg/golinters/golicenser/testdata/fix/out/golicenser_1.go create mode 100644 pkg/golinters/golicenser/testdata/fix/out/golicenser_2.go create mode 100644 pkg/golinters/golicenser/testdata/fix/out/golicenser_3.go create mode 100644 pkg/golinters/golicenser/testdata/fix/out/golicenser_4.go create mode 100644 pkg/golinters/golicenser/testdata/fix/out/golicenser_5.go create mode 100644 pkg/golinters/golicenser/testdata/golicenser-fix.yml create mode 100644 pkg/golinters/golicenser/testdata/golicenser.yml create mode 100644 pkg/golinters/golicenser/testdata/golicenser_cgo.go create mode 100644 pkg/golinters/golicenser/testdata/golicenser_different.go create mode 100644 pkg/golinters/golicenser/testdata/golicenser_good.go create mode 100644 pkg/golinters/golicenser/testdata/golicenser_missing.go create mode 100644 pkg/golinters/golicenser/testdata/golicenser_outdated.go diff --git a/.golangci.next.reference.yml b/.golangci.next.reference.yml index 37866ff00f2c..71420e425744 100644 --- a/.golangci.next.reference.yml +++ b/.golangci.next.reference.yml @@ -57,6 +57,7 @@ linters: - godot - godox - goheader + - golicenser - gomoddirectives - gomodguard - goprintffuncname @@ -165,6 +166,7 @@ linters: - godot - godox - goheader + - golicenser - gomoddirectives - gomodguard - goprintffuncname @@ -1279,6 +1281,90 @@ linters: # Default: "" template-path: /path/to/my/template.tmpl + golicenser: + header: + # License header template (using Go text/template: https://pkg.go.dev/text/template). + # More information: https://github.com/joshuasing/golicenser#templates + # Built-in variables: + # - `year`: Copyright year(s) for the file. The format is configurable with `year-mode`. + # - `author`: The copyright author (provided by `author`). + # - `filename`: The current filename. + # Custom variables can be provided in `variables` below. + # Required value. + template: |- + Copyright (c) {{.year}} {{.author}}. All rights reserved. + Use of this source code is governed by a BSD-style + license that can be found in the LICENSE file. + + # License header match regexp. + # More information: https://github.com/joshuasing/golicenser#matcher + # Matched headers will be updated or replaced by the header template. + # Default: regexp-escaped value of `template`. + matcher: |- + Copyright \(c\) {{.year}} {{.author}}.(\s*All rights reserved.)? + Use of this source code is governed by a BSD-style + license that can be found in the LICENSE file. + + # Enable to regexp-escape the header matcher. + # Useful if you want to provide a license header instead of regexp expression. + # Default: false + matcher-escape: false + + # The project author name used in the `author` template variable. + # Required value. + author: "My Name" + + # Regexp used to match the project author (`author` variable). + # Default: regexp-escaped value of `author`. + author-regexp: "(My Name|Your Name)" + + # Variables to provide to the header template. + # Due to a limitation of Viper, the configuration loader used by golangci-lint, variable names will always be + # provided as lowercase to the template. + # Default: {} + variables: + email: + value: "me@mycompany.com" + regexp: "(.+)@mycompany\\.com" + projectname: "My project" + + # Copyright year formatting mode. + # More information: https://github.com/joshuasing/golicenser#year-modes + # Available year modes are: + # - `preserve`: Current year in new headers, existing year in updated headers. + # - `preserve-this-year-range`: Current year in new headers, existing year to current year in updated headers. + # - `preserve-modified-range`: Last modified year in new headers, existing year to last modified year in updated headers. + # - `this-year`: Current year. + # - `last-modified`: File last modified year. + # - `git-range`: Git history creation year to last modified year (e.g. `2022-2025`) + # - `git-modified-list`: List of all modified years from Git history (e.g. `2022, 2024, 2025`) + # Default: "preserve" + year-mode: "git-range" + + # Style of comment to use in license headers. + # More information: https://github.com/joshuasing/golicenser#comment-styles + # Valid options are: `line`, `block`. + # Default: "line" + comment-style: "line" + + # List of file globs (doublestar) or regexps to match excluded paths. + # To use regexp, prefix the string with `r!`. + # Default: ["**/testdata/**"] + exclude: + - "**/testdata/**" + - "r!(.+)_test\\.go" + + # Maximum number of Go routines to use when analysing files. + # Default: 2 * the number of logical CPUs usable by the current process. + max-concurrent: 100 + + # Regexp used to detect license/copyright headers. + # This will be used to detect the existence of a copyright header in a file. + # If the first comment in a Go source file does not match this, then a license header will be generated. + # For the license header to be updated, it must also match the header matcher. + # Default: "(?i)copyright" + copyright-header-matcher: "(?i)copyright" + gomoddirectives: # Allow local `replace` directives. # Default: false diff --git a/go.mod b/go.mod index 00695fdf37ad..d578ca60a048 100644 --- a/go.mod +++ b/go.mod @@ -60,6 +60,7 @@ require ( github.com/jgautheron/goconst v1.8.1 github.com/jingyugao/rowserrcheck v1.1.1 github.com/jjti/go-spancheck v0.6.4 + github.com/joshuasing/golicenser v0.3.1 github.com/julz/importas v0.2.0 github.com/karamaru-alpha/copyloopvar v1.2.1 github.com/kisielk/errcheck v1.9.0 @@ -141,6 +142,7 @@ require ( github.com/Masterminds/semver/v3 v3.3.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect github.com/ccojocar/zxcvbn-go v1.0.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect diff --git a/go.sum b/go.sum index 9bc80fb7cd9b..34110c6920fc 100644 --- a/go.sum +++ b/go.sum @@ -96,6 +96,8 @@ github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5 github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo= github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= +github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= +github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ= github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg= github.com/breml/bidichk v0.3.3 h1:WSM67ztRusf1sMoqH6/c4OBCUlRVTKq+CbSeo0R17sE= @@ -337,6 +339,8 @@ github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjz github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= github.com/jjti/go-spancheck v0.6.4 h1:Tl7gQpYf4/TMU7AT84MN83/6PutY21Nb9fuQjFTpRRc= github.com/jjti/go-spancheck v0.6.4/go.mod h1:yAEYdKJ2lRkDA8g7X+oKUHXOWVAXSBJRv04OhF+QUjk= +github.com/joshuasing/golicenser v0.3.1 h1:M05PUCcA+CTTZJCcsyI/tAyAAEzu0JBV1f/iChfmIFE= +github.com/joshuasing/golicenser v0.3.1/go.mod h1:v1ivFA0z+ZXvm/GMpfBygQHk8I7YgGJw6EUPeQAkoFs= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= diff --git a/jsonschema/golangci.next.jsonschema.json b/jsonschema/golangci.next.jsonschema.json index 0f2ef5fcc50b..2d894a1687fa 100644 --- a/jsonschema/golangci.next.jsonschema.json +++ b/jsonschema/golangci.next.jsonschema.json @@ -757,6 +757,7 @@ "godox", "err113", "goheader", + "golicenser", "gomoddirectives", "gomodguard", "goprintffuncname", @@ -1935,6 +1936,107 @@ { "required": ["template-path"] } ] }, + "golicenserSettings": { + "type": "object", + "additionalProperties": false, + "properties": { + "header": { + "description": "License header settings.", + "type": "object", + "properties": { + "template": { + "description": "License header template (using Go text/template)\nhttps://github.com/joshuasing/golicenser#templates", + "type": "string", + "examples": [ + "Copyright (c) {{.year}} {{.author}}. All rights reserved.\nUse of this source code is governed by a BSD-style\nlicense that can be found in the LICENSE file." + ] + }, + "matcher": { + "description": "License header matcher regexp\nhttps://github.com/joshuasing/golicenser#matcher", + "type": "string", + "examples": [ + "Copyright \\(c\\) {{.year}} {{.author}}(.|\\s<.+@example.com>)" + ] + }, + "matcher-escape": { + "description": "Enable to regexp-escape the header matcher. Useful for providing a license template instead of regexp.", + "type": "boolean", + "default": false + }, + "author": { + "description": "Project author name (used in the `author` template variable).", + "type": "string" + }, + "author-regexp": { + "description": "Regexp used to match the project author (`author` variable)", + "type": "string" + }, + "variables": { + "description": "Custom variables to provide to the header template\nNote: Due to a limitation of Viper, the configuration loader used by golangci-lint, variable names will always be provided as lowercase to the template.", + "type": "object", + "patternProperties": { + "^.+$": { + "oneOf": [ + { + "description": "Variable value", + "type": "string" + }, + { + "type": "object", + "properties": { + "value": { + "description": "Variable value", + "type": "string" + }, + "regexp": { + "description": "Variable matcher regexp", + "type": "string" + } + }, + "required": ["value"] + } + ] + } + } + }, + "year-mode": { + "description": "Copyright year formatting mode.\nhttps://github.com/joshuasing/golicenser#year-modes", + "type": "string", + "enum": ["preserve", "preserve-this-year-range", "preserve-modified-range", "this-year", "last-modified", "git-range", "git-modified-list"], + "default": "preserve" + }, + "comment-style": { + "description": "Style of comment to use in license headers.\nhttps://github.com/joshuasing/golicenser#comment-styles", + "type": "string", + "enum": ["line", "block"], + "default": "line" + } + }, + "required": ["template", "author"] + }, + "exclude": { + "description": "List of file globs (doublestar) or regexps to match excluded paths. To use regexp, prefix the string with `r!`.", + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "**/testdata/**" + ] + }, + "max-concurrent": { + "description": "Maximum number of Go routines to use when analysing files.", + "type": "integer", + "minimum": 0 + }, + "copyright-header-regexp": { + "description": "Regexp used to detect license/copyright headers. This is used to detect the existence of any copyright header in a file.", + "type": "string", + "default": "(?i)copyright" + } + }, + "required": ["header"] + }, "goimportsSettings": { "type": "object", "additionalProperties": false, @@ -4399,6 +4501,9 @@ "goheader": { "$ref": "#/definitions/settings/definitions/goheaderSettings" }, + "golicenser": { + "$ref": "#/definitions/settings/definitions/golicenserSettings" + }, "gomoddirectives": { "$ref": "#/definitions/settings/definitions/gomoddirectivesSettings" }, diff --git a/pkg/config/linters_settings.go b/pkg/config/linters_settings.go index 16a8bb9501b9..57853dfed27a 100644 --- a/pkg/config/linters_settings.go +++ b/pkg/config/linters_settings.go @@ -232,6 +232,7 @@ type LintersSettings struct { Godot GodotSettings `mapstructure:"godot"` Godox GodoxSettings `mapstructure:"godox"` Goheader GoHeaderSettings `mapstructure:"goheader"` + Golicenser GoLicenserSettings `mapstructure:"golicenser"` GoModDirectives GoModDirectivesSettings `mapstructure:"gomoddirectives"` Gomodguard GoModGuardSettings `mapstructure:"gomodguard"` Gosec GoSecSettings `mapstructure:"gosec"` @@ -510,6 +511,29 @@ type GoHeaderSettings struct { TemplatePath string `mapstructure:"template-path"` } +type GoLicenserSettings struct { + Header GoLicenserHeaderSettings `mapstructure:"header"` + Exclude []string `mapstructure:"exclude"` + MaxConcurrent int `mapstructure:"max-concurrent"` + CopyrightHeaderMatcher string `mapstructure:"copyright-header-matcher"` +} + +type GoLicenserHeaderSettings struct { + Template string `mapstructure:"template"` + Matcher string `mapstructure:"matcher"` + MatcherEscape bool `mapstructure:"matcher-escape"` + Author string `mapstructure:"author"` + AuthorRegexp string `mapstructure:"author-regexp"` + Variables map[string]any `mapstructure:"variables"` + YearMode string `mapstructure:"year-mode"` + CommentStyle string `mapstructure:"comment-style"` +} + +type GoLicenserVar struct { + Value string `mapstructure:"value"` + Regexp string `mapstructure:"regexp"` +} + type GoModDirectivesSettings struct { ReplaceAllowList []string `mapstructure:"replace-allow-list"` ReplaceLocal bool `mapstructure:"replace-local"` diff --git a/pkg/golinters/golicenser/golicenser.go b/pkg/golinters/golicenser/golicenser.go new file mode 100644 index 000000000000..c06820671cc8 --- /dev/null +++ b/pkg/golinters/golicenser/golicenser.go @@ -0,0 +1,88 @@ +package golicenser + +import ( + "github.com/go-viper/mapstructure/v2" + "github.com/joshuasing/golicenser" + "golang.org/x/tools/go/analysis" + + "github.com/golangci/golangci-lint/v2/pkg/config" + "github.com/golangci/golangci-lint/v2/pkg/goanalysis" + "github.com/golangci/golangci-lint/v2/pkg/golinters/internal" +) + +const ( + linterName = "golicenser" + linterDesc = "Powerful license header linter" +) + +func New(settings *config.GoLicenserSettings) *goanalysis.Linter { + var conf golicenser.Config + if settings != nil { + var err error + var yearMode golicenser.YearMode + if ym := settings.Header.YearMode; ym != "" { + yearMode, err = golicenser.ParseYearMode(ym) + if err != nil { + internal.LinterLogger.Fatalf("%s: parse year mode: %v", linterName, err) + } + } + + var commentStyle golicenser.CommentStyle + if cs := settings.Header.CommentStyle; cs != "" { + commentStyle, err = golicenser.ParseCommentStyle(cs) + if err != nil { + internal.LinterLogger.Fatalf("%s: parse comment style: %v", linterName, err) + } + } + + vars := make(map[string]*golicenser.Var, len(settings.Header.Variables)) + for k, v := range settings.Header.Variables { + if s, ok := v.(string); ok { + vars[k] = &golicenser.Var{Value: s} + continue + } + + var glVar config.GoLicenserVar + if err := mapstructure.Decode(v, &glVar); err != nil { + internal.LinterLogger.Fatalf("%s: decode variable %s: %v", linterName, k, err) + } + vars[k] = &golicenser.Var{ + Value: glVar.Value, + Regexp: glVar.Regexp, + } + } + + conf = golicenser.Config{ + Header: golicenser.HeaderOpts{ + Template: settings.Header.Template, + Matcher: settings.Header.Matcher, + MatcherEscape: settings.Header.MatcherEscape, + Author: settings.Header.Author, + AuthorRegexp: settings.Header.AuthorRegexp, + Variables: vars, + YearMode: yearMode, + CommentStyle: commentStyle, + }, + Exclude: settings.Exclude, + MaxConcurrent: settings.MaxConcurrent, + CopyrightHeaderMatcher: settings.CopyrightHeaderMatcher, + } + } + + if conf.Header.Template == "" || conf.Header.Author == "" { + // User did not set template or author, disable golicenser. + return goanalysis.NewLinter(linterName, linterDesc, nil, nil) + } + + analyzer, err := golicenser.NewAnalyzer(conf) + if err != nil { + internal.LinterLogger.Fatalf("%s: create analyzer: %v", linterName, err) + } + + return goanalysis.NewLinter( + linterName, + linterDesc, + []*analysis.Analyzer{analyzer}, + nil, + ).WithLoadMode(goanalysis.LoadModeSyntax) +} diff --git a/pkg/golinters/golicenser/golicenser_integration_test.go b/pkg/golinters/golicenser/golicenser_integration_test.go new file mode 100644 index 000000000000..b4325fc46c65 --- /dev/null +++ b/pkg/golinters/golicenser/golicenser_integration_test.go @@ -0,0 +1,19 @@ +package golicenser + +import ( + "testing" + + "github.com/golangci/golangci-lint/v2/test/testshared/integration" +) + +func TestFromTestdata(t *testing.T) { + integration.RunTestdata(t) +} + +func TestFix(t *testing.T) { + integration.RunFix(t) +} + +func TestFixPathPrefix(t *testing.T) { + integration.RunFixPathPrefix(t) +} diff --git a/pkg/golinters/golicenser/testdata/fix/in/golicenser_1.go b/pkg/golinters/golicenser/testdata/fix/in/golicenser_1.go new file mode 100644 index 000000000000..b20aa9063a26 --- /dev/null +++ b/pkg/golinters/golicenser/testdata/fix/in/golicenser_1.go @@ -0,0 +1,4 @@ +//golangcitest:args -Egolicenser +//golangcitest:expected_exitcode 0 +//golangcitest:config_path testdata/golicenser-fix.yml +package testdata diff --git a/pkg/golinters/golicenser/testdata/fix/in/golicenser_2.go b/pkg/golinters/golicenser/testdata/fix/in/golicenser_2.go new file mode 100644 index 000000000000..9ab229621b38 --- /dev/null +++ b/pkg/golinters/golicenser/testdata/fix/in/golicenser_2.go @@ -0,0 +1,13 @@ +// Copyright (c) 2025 golangci-lint . +// This file is a part of golangci-lint. + +//golangcitest:args -Egolicenser +//golangcitest:expected_exitcode 0 +//golangcitest:config_path testdata/golicenser-fix.yml +package testdata + +import "fmt" + +func main() { + fmt.Println("Hello, world!") +} diff --git a/pkg/golinters/golicenser/testdata/fix/in/golicenser_3.go b/pkg/golinters/golicenser/testdata/fix/in/golicenser_3.go new file mode 100644 index 000000000000..1923408bd058 --- /dev/null +++ b/pkg/golinters/golicenser/testdata/fix/in/golicenser_3.go @@ -0,0 +1,7 @@ +// Package testdata is used for testing whether golicenser works correctly. +// golicenser should ignore this package comment and add a license header above it. +// +//golangcitest:args -Egolicenser +//golangcitest:expected_exitcode 0 +//golangcitest:config_path testdata/golicenser-fix.yml +package testdata diff --git a/pkg/golinters/golicenser/testdata/fix/in/golicenser_4.go b/pkg/golinters/golicenser/testdata/fix/in/golicenser_4.go new file mode 100644 index 000000000000..a255ae4c02ae --- /dev/null +++ b/pkg/golinters/golicenser/testdata/fix/in/golicenser_4.go @@ -0,0 +1,8 @@ +// Copyright (c) 2025 Someone else +// This file has a different license header which matches the copyright header +// matcher but not the license header matcher, therefore it should not be updated. + +//golangcitest:args -Egolicenser +//golangcitest:expected_exitcode 0 +//golangcitest:config_path testdata/golicenser-fix.yml +package testdata diff --git a/pkg/golinters/golicenser/testdata/fix/in/golicenser_5.go b/pkg/golinters/golicenser/testdata/fix/in/golicenser_5.go new file mode 100644 index 000000000000..35b435aa0579 --- /dev/null +++ b/pkg/golinters/golicenser/testdata/fix/in/golicenser_5.go @@ -0,0 +1,9 @@ +/* +Copyright (c) 2025 golangci-lint . +This file is a part of golangci-lint. +*/ + +//golangcitest:args -Egolicenser +//golangcitest:expected_exitcode 0 +//golangcitest:config_path testdata/golicenser-fix.yml +package testdata diff --git a/pkg/golinters/golicenser/testdata/fix/out/golicenser_1.go b/pkg/golinters/golicenser/testdata/fix/out/golicenser_1.go new file mode 100644 index 000000000000..142b7a398a47 --- /dev/null +++ b/pkg/golinters/golicenser/testdata/fix/out/golicenser_1.go @@ -0,0 +1,7 @@ +// Copyright (c) 2025 golangci-lint . +// This file is a part of golangci-lint. + +//golangcitest:args -Egolicenser +//golangcitest:expected_exitcode 0 +//golangcitest:config_path testdata/golicenser-fix.yml +package testdata diff --git a/pkg/golinters/golicenser/testdata/fix/out/golicenser_2.go b/pkg/golinters/golicenser/testdata/fix/out/golicenser_2.go new file mode 100644 index 000000000000..721ea8d5e851 --- /dev/null +++ b/pkg/golinters/golicenser/testdata/fix/out/golicenser_2.go @@ -0,0 +1,13 @@ +// Copyright (c) 2025 golangci-lint . +// This file is a part of golangci-lint. + +//golangcitest:args -Egolicenser +//golangcitest:expected_exitcode 0 +//golangcitest:config_path testdata/golicenser-fix.yml +package testdata + +import "fmt" + +func main() { + fmt.Println("Hello, world!") +} diff --git a/pkg/golinters/golicenser/testdata/fix/out/golicenser_3.go b/pkg/golinters/golicenser/testdata/fix/out/golicenser_3.go new file mode 100644 index 000000000000..cde2984c8eff --- /dev/null +++ b/pkg/golinters/golicenser/testdata/fix/out/golicenser_3.go @@ -0,0 +1,10 @@ +// Copyright (c) 2025 golangci-lint . +// This file is a part of golangci-lint. + +// Package testdata is used for testing whether golicenser works correctly. +// golicenser should ignore this package comment and add a license header above it. +// +//golangcitest:args -Egolicenser +//golangcitest:expected_exitcode 0 +//golangcitest:config_path testdata/golicenser-fix.yml +package testdata diff --git a/pkg/golinters/golicenser/testdata/fix/out/golicenser_4.go b/pkg/golinters/golicenser/testdata/fix/out/golicenser_4.go new file mode 100644 index 000000000000..a255ae4c02ae --- /dev/null +++ b/pkg/golinters/golicenser/testdata/fix/out/golicenser_4.go @@ -0,0 +1,8 @@ +// Copyright (c) 2025 Someone else +// This file has a different license header which matches the copyright header +// matcher but not the license header matcher, therefore it should not be updated. + +//golangcitest:args -Egolicenser +//golangcitest:expected_exitcode 0 +//golangcitest:config_path testdata/golicenser-fix.yml +package testdata diff --git a/pkg/golinters/golicenser/testdata/fix/out/golicenser_5.go b/pkg/golinters/golicenser/testdata/fix/out/golicenser_5.go new file mode 100644 index 000000000000..142b7a398a47 --- /dev/null +++ b/pkg/golinters/golicenser/testdata/fix/out/golicenser_5.go @@ -0,0 +1,7 @@ +// Copyright (c) 2025 golangci-lint . +// This file is a part of golangci-lint. + +//golangcitest:args -Egolicenser +//golangcitest:expected_exitcode 0 +//golangcitest:config_path testdata/golicenser-fix.yml +package testdata diff --git a/pkg/golinters/golicenser/testdata/golicenser-fix.yml b/pkg/golinters/golicenser/testdata/golicenser-fix.yml new file mode 100644 index 000000000000..6bc2393644ca --- /dev/null +++ b/pkg/golinters/golicenser/testdata/golicenser-fix.yml @@ -0,0 +1,16 @@ +version: "2" + +linters: + settings: + golicenser: + header: + author: "golangci-lint" + template: |- + Copyright (c) {{.year}} {{.author}} <{{.email}}>. + This file is a part of {{.projectname}}. + variables: + email: + value: "test@example.com" + regexp: "(.+)@example\\.com" + projectname: "golangci-lint" + exclude: [] # By default, testdata is excluded. diff --git a/pkg/golinters/golicenser/testdata/golicenser.yml b/pkg/golinters/golicenser/testdata/golicenser.yml new file mode 100644 index 000000000000..3d20fc6c4eb9 --- /dev/null +++ b/pkg/golinters/golicenser/testdata/golicenser.yml @@ -0,0 +1,20 @@ +version: "2" + +linters: + settings: + golicenser: + header: + author: "golangci-lint" + template: |- + Copyright (c) {{.year}} {{.author}} <{{.email}}>. + This file is a part of {{.projectname}}. + # Allow matcher to match "// want" comments, otherwise they break matching. + matcher: |- + Copyright \(c\) {{.year}} {{.author}} <{{.email}}>\.(\s// want.+)? + This file is a part of {{.projectname}}\. + variables: + email: + value: "test@example.com" + regexp: "(.+)@example\\.com" + projectname: "golangci-lint" + exclude: [] # By default, testdata is excluded. diff --git a/pkg/golinters/golicenser/testdata/golicenser_cgo.go b/pkg/golinters/golicenser/testdata/golicenser_cgo.go new file mode 100644 index 000000000000..f8049653b733 --- /dev/null +++ b/pkg/golinters/golicenser/testdata/golicenser_cgo.go @@ -0,0 +1,35 @@ +//go:build ignore + +// TODO(joshuasing): golicenser doesn't currently support cgo, for a few reasons: +// - source file will be copied to go-build cache +// - copied source file will contain generated comment, causing it to be excluded +// - modifying the copied go-build cache file does not result in the actual source file being fixed +// I would like to eventually support cgo, however for now, if using cgo, it is recommended to add to exclude: +// - */go-build/* + +// Copyright (c) 2025 golangci-lint . // want "invalid license header" +// This file is a part of golangci-lint. + +//golangcitest:args -Egolicenser +//golangcitest:config_path testdata/golicenser.yml +package testdata + +/* + #include + #include + + void myprint(char* s) { + printf("%d\n", s); + } +*/ +import "C" + +import ( + "unsafe" +) + +func _() { + cs := C.CString("Hello from stdio\n") + C.myprint(cs) + C.free(unsafe.Pointer(cs)) +} diff --git a/pkg/golinters/golicenser/testdata/golicenser_different.go b/pkg/golinters/golicenser/testdata/golicenser_different.go new file mode 100644 index 000000000000..1ee781e3d6a0 --- /dev/null +++ b/pkg/golinters/golicenser/testdata/golicenser_different.go @@ -0,0 +1,8 @@ +// Copyright (c) 2025 Someone else +// This file has a different license header which matches the copyright header +// matcher but not the license header matcher, therefore it should not be updated. + +//golangcitest:args -Egolicenser +//golangcitest:config_path testdata/golicenser.yml +//golangcitest:expected_exitcode 0 +package testdata diff --git a/pkg/golinters/golicenser/testdata/golicenser_good.go b/pkg/golinters/golicenser/testdata/golicenser_good.go new file mode 100644 index 000000000000..1e4f25d66703 --- /dev/null +++ b/pkg/golinters/golicenser/testdata/golicenser_good.go @@ -0,0 +1,7 @@ +// Copyright (c) 2025 golangci-lint . +// This file is a part of golangci-lint. + +//golangcitest:args -Egolicenser +//golangcitest:config_path testdata/golicenser.yml +//golangcitest:expected_exitcode 0 +package testdata diff --git a/pkg/golinters/golicenser/testdata/golicenser_missing.go b/pkg/golinters/golicenser/testdata/golicenser_missing.go new file mode 100644 index 000000000000..754a6f1e1edb --- /dev/null +++ b/pkg/golinters/golicenser/testdata/golicenser_missing.go @@ -0,0 +1,5 @@ +// want "missing license header" + +//golangcitest:args -Egolicenser +//golangcitest:config_path testdata/golicenser.yml +package testdata diff --git a/pkg/golinters/golicenser/testdata/golicenser_outdated.go b/pkg/golinters/golicenser/testdata/golicenser_outdated.go new file mode 100644 index 000000000000..bd6ff941d60c --- /dev/null +++ b/pkg/golinters/golicenser/testdata/golicenser_outdated.go @@ -0,0 +1,12 @@ +// Copyright (c) 2025 golangci-lint . // want "invalid license header" +// This file is a part of golangci-lint. + +//golangcitest:args -Egolicenser +//golangcitest:config_path testdata/golicenser.yml +package testdata + +import "fmt" + +func main() { + fmt.Println("Hello, world!") +} diff --git a/pkg/lint/lintersdb/builder_linter.go b/pkg/lint/lintersdb/builder_linter.go index 5ac3e5ba2fb0..71a517f81e60 100644 --- a/pkg/lint/lintersdb/builder_linter.go +++ b/pkg/lint/lintersdb/builder_linter.go @@ -47,6 +47,7 @@ import ( "github.com/golangci/golangci-lint/v2/pkg/golinters/gofumpt" "github.com/golangci/golangci-lint/v2/pkg/golinters/goheader" "github.com/golangci/golangci-lint/v2/pkg/golinters/goimports" + "github.com/golangci/golangci-lint/v2/pkg/golinters/golicenser" "github.com/golangci/golangci-lint/v2/pkg/golinters/golines" "github.com/golangci/golangci-lint/v2/pkg/golinters/gomoddirectives" "github.com/golangci/golangci-lint/v2/pkg/golinters/gomodguard" @@ -353,6 +354,11 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) { WithAutoFix(). WithURL("https://github.com/denis-tingaikin/go-header"), + linter.NewConfig(golicenser.New(&cfg.Linters.Settings.Golicenser)). + WithSince("v2.2.0"). + WithAutoFix(). + WithURL("https://github.com/joshuasing/golicenser"), + linter.NewConfig(goimports.New(&cfg.Linters.Settings.GoImports)). WithSince("v1.20.0"). WithAutoFix(). From 4f6bb90966250e01490f778cc7cda5b2fc8c158d Mon Sep 17 00:00:00 2001 From: Joshua Sing Date: Sun, 20 Apr 2025 04:20:28 +1000 Subject: [PATCH 2/3] refactor: add template-path to golicenser config --- .golangci.next.reference.yml | 8 ++++++++ jsonschema/golangci.next.jsonschema.json | 13 ++++++++++++- pkg/config/linters_settings.go | 1 + pkg/golinters/golicenser/golicenser.go | 18 ++++++++++++++++-- .../testdata/golicenser-template-path.yml | 14 ++++++++++++++ .../testdata/golicenser_missing_tp.go | 5 +++++ .../golicenser/testdata/license-example.txt | 2 ++ pkg/lint/lintersdb/builder_linter.go | 2 +- 8 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 pkg/golinters/golicenser/testdata/golicenser-template-path.yml create mode 100644 pkg/golinters/golicenser/testdata/golicenser_missing_tp.go create mode 100644 pkg/golinters/golicenser/testdata/license-example.txt diff --git a/.golangci.next.reference.yml b/.golangci.next.reference.yml index 71420e425744..78dac00b0724 100644 --- a/.golangci.next.reference.yml +++ b/.golangci.next.reference.yml @@ -1296,6 +1296,14 @@ linters: Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. + # Path to file containing license header template. + # This is an alternative to `template`, and `template` will take priority if set. + # By default, if a path is relative, it is relative to the directory where the golangci-lint command is executed. + # The placeholder '${base-path}' is substituted with a path relative to the mode defined with `run.relative-path-mode`. + # The placeholder '${config-path}' is substituted with a path relative to the configuration file. + # Default: "" + template-path: "${config-path}/license.txt" + # License header match regexp. # More information: https://github.com/joshuasing/golicenser#matcher # Matched headers will be updated or replaced by the header template. diff --git a/jsonschema/golangci.next.jsonschema.json b/jsonschema/golangci.next.jsonschema.json index 2d894a1687fa..f9741fe2f6c0 100644 --- a/jsonschema/golangci.next.jsonschema.json +++ b/jsonschema/golangci.next.jsonschema.json @@ -1951,6 +1951,14 @@ "Copyright (c) {{.year}} {{.author}}. All rights reserved.\nUse of this source code is governed by a BSD-style\nlicense that can be found in the LICENSE file." ] }, + "template-path": { + "description": "Path to file containing license header template", + "type": "string", + "examples": [ + "license.txt", + "license-header.txt" + ] + }, "matcher": { "description": "License header matcher regexp\nhttps://github.com/joshuasing/golicenser#matcher", "type": "string", @@ -2012,7 +2020,10 @@ "default": "line" } }, - "required": ["template", "author"] + "oneOf": [ + { "required": ["template", "author"] }, + { "required": ["template-path", "author"] } + ] }, "exclude": { "description": "List of file globs (doublestar) or regexps to match excluded paths. To use regexp, prefix the string with `r!`.", diff --git a/pkg/config/linters_settings.go b/pkg/config/linters_settings.go index 57853dfed27a..3e43219edc83 100644 --- a/pkg/config/linters_settings.go +++ b/pkg/config/linters_settings.go @@ -520,6 +520,7 @@ type GoLicenserSettings struct { type GoLicenserHeaderSettings struct { Template string `mapstructure:"template"` + TemplatePath string `mapstructure:"template-path"` Matcher string `mapstructure:"matcher"` MatcherEscape bool `mapstructure:"matcher-escape"` Author string `mapstructure:"author"` diff --git a/pkg/golinters/golicenser/golicenser.go b/pkg/golinters/golicenser/golicenser.go index c06820671cc8..2643193cfdc5 100644 --- a/pkg/golinters/golicenser/golicenser.go +++ b/pkg/golinters/golicenser/golicenser.go @@ -1,6 +1,9 @@ package golicenser import ( + "os" + "strings" + "github.com/go-viper/mapstructure/v2" "github.com/joshuasing/golicenser" "golang.org/x/tools/go/analysis" @@ -15,9 +18,20 @@ const ( linterDesc = "Powerful license header linter" ) -func New(settings *config.GoLicenserSettings) *goanalysis.Linter { +func New(settings *config.GoLicenserSettings, replacer *strings.Replacer) *goanalysis.Linter { var conf golicenser.Config if settings != nil { + // Template from config takes priority over template from template-path. + template := settings.Header.Template + if template == "" && settings.Header.TemplatePath != "" { + b, err := os.ReadFile(replacer.Replace(settings.Header.TemplatePath)) + if err != nil { + internal.LinterLogger.Fatalf("%s: read template file: %v", linterName, err) + } + // Use template from file (trim newline from end of file) + template = strings.TrimSuffix(string(b), "\n") + } + var err error var yearMode golicenser.YearMode if ym := settings.Header.YearMode; ym != "" { @@ -54,7 +68,7 @@ func New(settings *config.GoLicenserSettings) *goanalysis.Linter { conf = golicenser.Config{ Header: golicenser.HeaderOpts{ - Template: settings.Header.Template, + Template: template, Matcher: settings.Header.Matcher, MatcherEscape: settings.Header.MatcherEscape, Author: settings.Header.Author, diff --git a/pkg/golinters/golicenser/testdata/golicenser-template-path.yml b/pkg/golinters/golicenser/testdata/golicenser-template-path.yml new file mode 100644 index 000000000000..5367845363c9 --- /dev/null +++ b/pkg/golinters/golicenser/testdata/golicenser-template-path.yml @@ -0,0 +1,14 @@ +version: "2" + +linters: + settings: + golicenser: + header: + author: "golangci-lint" + template-path: "${config-path}/license-example.txt" + variables: + email: + value: "test@example.com" + regexp: "(.+)@example\\.com" + projectname: "golangci-lint" + exclude: [] # By default, testdata is excluded. diff --git a/pkg/golinters/golicenser/testdata/golicenser_missing_tp.go b/pkg/golinters/golicenser/testdata/golicenser_missing_tp.go new file mode 100644 index 000000000000..2d6de7710f0d --- /dev/null +++ b/pkg/golinters/golicenser/testdata/golicenser_missing_tp.go @@ -0,0 +1,5 @@ +// want "missing license header" + +//golangcitest:args -Egolicenser +//golangcitest:config_path testdata/golicenser-template-path.yml +package testdata diff --git a/pkg/golinters/golicenser/testdata/license-example.txt b/pkg/golinters/golicenser/testdata/license-example.txt new file mode 100644 index 000000000000..e668545c2548 --- /dev/null +++ b/pkg/golinters/golicenser/testdata/license-example.txt @@ -0,0 +1,2 @@ +Copyright (c) {{.year}} {{.author}} <{{.email}}>. +This file is a part of {{.projectname}}. diff --git a/pkg/lint/lintersdb/builder_linter.go b/pkg/lint/lintersdb/builder_linter.go index 71a517f81e60..8e701a0e1462 100644 --- a/pkg/lint/lintersdb/builder_linter.go +++ b/pkg/lint/lintersdb/builder_linter.go @@ -354,7 +354,7 @@ func (LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) { WithAutoFix(). WithURL("https://github.com/denis-tingaikin/go-header"), - linter.NewConfig(golicenser.New(&cfg.Linters.Settings.Golicenser)). + linter.NewConfig(golicenser.New(&cfg.Linters.Settings.Golicenser, placeholderReplacer)). WithSince("v2.2.0"). WithAutoFix(). WithURL("https://github.com/joshuasing/golicenser"), From a314c0edeb8b50159bb1b65d21a0483eca9d2f44 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Sat, 19 Apr 2025 23:35:38 +0200 Subject: [PATCH 3/3] review --- .golangci.next.reference.yml | 61 +++----- jsonschema/golangci.next.jsonschema.json | 52 ++----- pkg/config/linters_settings.go | 20 ++- pkg/golinters/golicenser/golicenser.go | 138 +++++++++--------- .../{golicenser_2.go => golicenser_author.go} | 0 ....go => golicenser_change_comment_style.go} | 0 ...licenser_4.go => golicenser_no_changes.go} | 0 ...olicenser_1.go => golicenser_no_header.go} | 0 ...ser_3.go => golicenser_package_comment.go} | 0 .../{golicenser_2.go => golicenser_author.go} | 0 ....go => golicenser_change_comment_style.go} | 0 ...licenser_4.go => golicenser_no_changes.go} | 0 ...olicenser_5.go => golicenser_no_header.go} | 0 ...ser_3.go => golicenser_package_comment.go} | 0 .../golicenser/testdata/golicenser-fix.yml | 4 +- .../testdata/golicenser-template-path.yml | 4 +- .../golicenser/testdata/golicenser.yml | 4 +- .../golicenser/testdata/golicenser_cgo.go | 7 - 18 files changed, 116 insertions(+), 174 deletions(-) rename pkg/golinters/golicenser/testdata/fix/in/{golicenser_2.go => golicenser_author.go} (100%) rename pkg/golinters/golicenser/testdata/fix/in/{golicenser_5.go => golicenser_change_comment_style.go} (100%) rename pkg/golinters/golicenser/testdata/fix/in/{golicenser_4.go => golicenser_no_changes.go} (100%) rename pkg/golinters/golicenser/testdata/fix/in/{golicenser_1.go => golicenser_no_header.go} (100%) rename pkg/golinters/golicenser/testdata/fix/in/{golicenser_3.go => golicenser_package_comment.go} (100%) rename pkg/golinters/golicenser/testdata/fix/out/{golicenser_2.go => golicenser_author.go} (100%) rename pkg/golinters/golicenser/testdata/fix/out/{golicenser_1.go => golicenser_change_comment_style.go} (100%) rename pkg/golinters/golicenser/testdata/fix/out/{golicenser_4.go => golicenser_no_changes.go} (100%) rename pkg/golinters/golicenser/testdata/fix/out/{golicenser_5.go => golicenser_no_header.go} (100%) rename pkg/golinters/golicenser/testdata/fix/out/{golicenser_3.go => golicenser_package_comment.go} (100%) diff --git a/.golangci.next.reference.yml b/.golangci.next.reference.yml index 78dac00b0724..11c1a5a4fb38 100644 --- a/.golangci.next.reference.yml +++ b/.golangci.next.reference.yml @@ -1282,7 +1282,25 @@ linters: template-path: /path/to/my/template.tmpl golicenser: + # Regexp used to detect license/copyright headers. + # This will be used to detect the existence of a copyright header in a file. + # If the first comment in a Go source file does not match this, then a license header will be generated. + # For the license header to be updated, it must also match the header matcher. + # Default: "(?i)copyright" + copyright-header-matcher: "(?i)copyright" + header: + # The project author name used in the `author` template variable. + # Required value. + author: "My Name" + # Regexp used to match the project author (`author` variable). + # Default: regexp-escaped value of `author`. + author-regexp: "(My Name|Your Name)" + # Style of comment to use in license headers. + # More information: https://github.com/joshuasing/golicenser#comment-styles + # Valid options are: `line`, `block`. + # Default: "line" + comment-style: "line" # License header template (using Go text/template: https://pkg.go.dev/text/template). # More information: https://github.com/joshuasing/golicenser#templates # Built-in variables: @@ -1295,15 +1313,13 @@ linters: Copyright (c) {{.year}} {{.author}}. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. - # Path to file containing license header template. # This is an alternative to `template`, and `template` will take priority if set. # By default, if a path is relative, it is relative to the directory where the golangci-lint command is executed. # The placeholder '${base-path}' is substituted with a path relative to the mode defined with `run.relative-path-mode`. # The placeholder '${config-path}' is substituted with a path relative to the configuration file. # Default: "" - template-path: "${config-path}/license.txt" - + template-path: /path/to/my/template.tmpl # License header match regexp. # More information: https://github.com/joshuasing/golicenser#matcher # Matched headers will be updated or replaced by the header template. @@ -1312,30 +1328,19 @@ linters: Copyright \(c\) {{.year}} {{.author}}.(\s*All rights reserved.)? Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. - # Enable to regexp-escape the header matcher. # Useful if you want to provide a license header instead of regexp expression. # Default: false matcher-escape: false - - # The project author name used in the `author` template variable. - # Required value. - author: "My Name" - - # Regexp used to match the project author (`author` variable). - # Default: regexp-escaped value of `author`. - author-regexp: "(My Name|Your Name)" - # Variables to provide to the header template. # Due to a limitation of Viper, the configuration loader used by golangci-lint, variable names will always be # provided as lowercase to the template. # Default: {} variables: email: - value: "me@mycompany.com" regexp: "(.+)@mycompany\\.com" - projectname: "My project" - + project-name: + value: "My project" # Copyright year formatting mode. # More information: https://github.com/joshuasing/golicenser#year-modes # Available year modes are: @@ -1349,30 +1354,6 @@ linters: # Default: "preserve" year-mode: "git-range" - # Style of comment to use in license headers. - # More information: https://github.com/joshuasing/golicenser#comment-styles - # Valid options are: `line`, `block`. - # Default: "line" - comment-style: "line" - - # List of file globs (doublestar) or regexps to match excluded paths. - # To use regexp, prefix the string with `r!`. - # Default: ["**/testdata/**"] - exclude: - - "**/testdata/**" - - "r!(.+)_test\\.go" - - # Maximum number of Go routines to use when analysing files. - # Default: 2 * the number of logical CPUs usable by the current process. - max-concurrent: 100 - - # Regexp used to detect license/copyright headers. - # This will be used to detect the existence of a copyright header in a file. - # If the first comment in a Go source file does not match this, then a license header will be generated. - # For the license header to be updated, it must also match the header matcher. - # Default: "(?i)copyright" - copyright-header-matcher: "(?i)copyright" - gomoddirectives: # Allow local `replace` directives. # Default: false diff --git a/jsonschema/golangci.next.jsonschema.json b/jsonschema/golangci.next.jsonschema.json index f9741fe2f6c0..427edd221583 100644 --- a/jsonschema/golangci.next.jsonschema.json +++ b/jsonschema/golangci.next.jsonschema.json @@ -1940,6 +1940,11 @@ "type": "object", "additionalProperties": false, "properties": { + "copyright-header-matcher": { + "description": "Regexp used to detect license/copyright headers. This is used to detect the existence of any copyright header in a file.", + "type": "string", + "default": "(?i)copyright" + }, "header": { "description": "License header settings.", "type": "object", @@ -1982,28 +1987,21 @@ "variables": { "description": "Custom variables to provide to the header template\nNote: Due to a limitation of Viper, the configuration loader used by golangci-lint, variable names will always be provided as lowercase to the template.", "type": "object", + "additionalProperties": false, "patternProperties": { - "^.+$": { - "oneOf": [ - { + "^[a-z\\-]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "value": { "description": "Variable value", "type": "string" }, - { - "type": "object", - "properties": { - "value": { - "description": "Variable value", - "type": "string" - }, - "regexp": { - "description": "Variable matcher regexp", - "type": "string" - } - }, - "required": ["value"] + "regexp": { + "description": "Variable matcher regexp", + "type": "string" } - ] + } } } }, @@ -2024,26 +2022,6 @@ { "required": ["template", "author"] }, { "required": ["template-path", "author"] } ] - }, - "exclude": { - "description": "List of file globs (doublestar) or regexps to match excluded paths. To use regexp, prefix the string with `r!`.", - "type": "array", - "items": { - "type": "string" - }, - "default": [ - "**/testdata/**" - ] - }, - "max-concurrent": { - "description": "Maximum number of Go routines to use when analysing files.", - "type": "integer", - "minimum": 0 - }, - "copyright-header-regexp": { - "description": "Regexp used to detect license/copyright headers. This is used to detect the existence of any copyright header in a file.", - "type": "string", - "default": "(?i)copyright" } }, "required": ["header"] diff --git a/pkg/config/linters_settings.go b/pkg/config/linters_settings.go index 3e43219edc83..c39762d3a5cc 100644 --- a/pkg/config/linters_settings.go +++ b/pkg/config/linters_settings.go @@ -513,21 +513,19 @@ type GoHeaderSettings struct { type GoLicenserSettings struct { Header GoLicenserHeaderSettings `mapstructure:"header"` - Exclude []string `mapstructure:"exclude"` - MaxConcurrent int `mapstructure:"max-concurrent"` CopyrightHeaderMatcher string `mapstructure:"copyright-header-matcher"` } type GoLicenserHeaderSettings struct { - Template string `mapstructure:"template"` - TemplatePath string `mapstructure:"template-path"` - Matcher string `mapstructure:"matcher"` - MatcherEscape bool `mapstructure:"matcher-escape"` - Author string `mapstructure:"author"` - AuthorRegexp string `mapstructure:"author-regexp"` - Variables map[string]any `mapstructure:"variables"` - YearMode string `mapstructure:"year-mode"` - CommentStyle string `mapstructure:"comment-style"` + Template string `mapstructure:"template"` + TemplatePath string `mapstructure:"template-path"` + Matcher string `mapstructure:"matcher"` + MatcherEscape bool `mapstructure:"matcher-escape"` + Author string `mapstructure:"author"` + AuthorRegexp string `mapstructure:"author-regexp"` + Variables map[string]GoLicenserVar `mapstructure:"variables"` + YearMode string `mapstructure:"year-mode"` + CommentStyle string `mapstructure:"comment-style"` } type GoLicenserVar struct { diff --git a/pkg/golinters/golicenser/golicenser.go b/pkg/golinters/golicenser/golicenser.go index 2643193cfdc5..0c2f22f79b6b 100644 --- a/pkg/golinters/golicenser/golicenser.go +++ b/pkg/golinters/golicenser/golicenser.go @@ -1,10 +1,10 @@ package golicenser import ( + "fmt" "os" "strings" - "github.com/go-viper/mapstructure/v2" "github.com/joshuasing/golicenser" "golang.org/x/tools/go/analysis" @@ -13,79 +13,12 @@ import ( "github.com/golangci/golangci-lint/v2/pkg/golinters/internal" ) -const ( - linterName = "golicenser" - linterDesc = "Powerful license header linter" -) +const linterName = "golicenser" func New(settings *config.GoLicenserSettings, replacer *strings.Replacer) *goanalysis.Linter { - var conf golicenser.Config - if settings != nil { - // Template from config takes priority over template from template-path. - template := settings.Header.Template - if template == "" && settings.Header.TemplatePath != "" { - b, err := os.ReadFile(replacer.Replace(settings.Header.TemplatePath)) - if err != nil { - internal.LinterLogger.Fatalf("%s: read template file: %v", linterName, err) - } - // Use template from file (trim newline from end of file) - template = strings.TrimSuffix(string(b), "\n") - } - - var err error - var yearMode golicenser.YearMode - if ym := settings.Header.YearMode; ym != "" { - yearMode, err = golicenser.ParseYearMode(ym) - if err != nil { - internal.LinterLogger.Fatalf("%s: parse year mode: %v", linterName, err) - } - } - - var commentStyle golicenser.CommentStyle - if cs := settings.Header.CommentStyle; cs != "" { - commentStyle, err = golicenser.ParseCommentStyle(cs) - if err != nil { - internal.LinterLogger.Fatalf("%s: parse comment style: %v", linterName, err) - } - } - - vars := make(map[string]*golicenser.Var, len(settings.Header.Variables)) - for k, v := range settings.Header.Variables { - if s, ok := v.(string); ok { - vars[k] = &golicenser.Var{Value: s} - continue - } - - var glVar config.GoLicenserVar - if err := mapstructure.Decode(v, &glVar); err != nil { - internal.LinterLogger.Fatalf("%s: decode variable %s: %v", linterName, k, err) - } - vars[k] = &golicenser.Var{ - Value: glVar.Value, - Regexp: glVar.Regexp, - } - } - - conf = golicenser.Config{ - Header: golicenser.HeaderOpts{ - Template: template, - Matcher: settings.Header.Matcher, - MatcherEscape: settings.Header.MatcherEscape, - Author: settings.Header.Author, - AuthorRegexp: settings.Header.AuthorRegexp, - Variables: vars, - YearMode: yearMode, - CommentStyle: commentStyle, - }, - Exclude: settings.Exclude, - MaxConcurrent: settings.MaxConcurrent, - CopyrightHeaderMatcher: settings.CopyrightHeaderMatcher, - } - } - - if conf.Header.Template == "" || conf.Header.Author == "" { - // User did not set template or author, disable golicenser. - return goanalysis.NewLinter(linterName, linterDesc, nil, nil) + conf, err := createConfig(settings, replacer) + if err != nil { + internal.LinterLogger.Fatalf("%s: parse year mode: %v", linterName, err) } analyzer, err := golicenser.NewAnalyzer(conf) @@ -95,8 +28,67 @@ func New(settings *config.GoLicenserSettings, replacer *strings.Replacer) *goana return goanalysis.NewLinter( linterName, - linterDesc, + "Powerful license header linter", []*analysis.Analyzer{analyzer}, nil, ).WithLoadMode(goanalysis.LoadModeSyntax) } + +func createConfig(settings *config.GoLicenserSettings, replacer *strings.Replacer) (golicenser.Config, error) { + if settings == nil { + return golicenser.Config{}, nil + } + + header := golicenser.HeaderOpts{ + Matcher: settings.Header.Matcher, + MatcherEscape: settings.Header.MatcherEscape, + Author: settings.Header.Author, + AuthorRegexp: settings.Header.AuthorRegexp, + Template: settings.Header.Template, + Variables: make(map[string]*golicenser.Var, len(settings.Header.Variables)), + } + + // Template from config takes priority over template from 'template-path'. + if header.Template == "" && settings.Header.TemplatePath != "" { + b, err := os.ReadFile(replacer.Replace(settings.Header.TemplatePath)) + if err != nil { + return golicenser.Config{}, fmt.Errorf("read the template file: %w", err) + } + + // Use template from a file (trim newline from the end of file) + header.Template = strings.TrimSuffix(strings.TrimSuffix(string(b), "\n"), "\r") + } + + var err error + if settings.Header.YearMode != "" { + header.YearMode, err = golicenser.ParseYearMode(settings.Header.YearMode) + if err != nil { + return golicenser.Config{}, fmt.Errorf("parse year mode: %w", err) + } + } + + if settings.Header.CommentStyle != "" { + header.CommentStyle, err = golicenser.ParseCommentStyle(settings.Header.CommentStyle) + if err != nil { + return golicenser.Config{}, fmt.Errorf("parse comment style: %w", err) + } + } + + for k, v := range settings.Header.Variables { + header.Variables[k] = &golicenser.Var{ + Value: v.Value, + Regexp: v.Regexp, + } + } + + return golicenser.Config{ + Header: header, + CopyrightHeaderMatcher: settings.CopyrightHeaderMatcher, + + // NOTE(ldez): not wanted for now. + // And an empty slice is used because of a wrong default inside the linter. + Exclude: []string{}, + // NOTE(ldez): golangci-lint already handles concurrency. + MaxConcurrent: 1, + }, nil +} diff --git a/pkg/golinters/golicenser/testdata/fix/in/golicenser_2.go b/pkg/golinters/golicenser/testdata/fix/in/golicenser_author.go similarity index 100% rename from pkg/golinters/golicenser/testdata/fix/in/golicenser_2.go rename to pkg/golinters/golicenser/testdata/fix/in/golicenser_author.go diff --git a/pkg/golinters/golicenser/testdata/fix/in/golicenser_5.go b/pkg/golinters/golicenser/testdata/fix/in/golicenser_change_comment_style.go similarity index 100% rename from pkg/golinters/golicenser/testdata/fix/in/golicenser_5.go rename to pkg/golinters/golicenser/testdata/fix/in/golicenser_change_comment_style.go diff --git a/pkg/golinters/golicenser/testdata/fix/in/golicenser_4.go b/pkg/golinters/golicenser/testdata/fix/in/golicenser_no_changes.go similarity index 100% rename from pkg/golinters/golicenser/testdata/fix/in/golicenser_4.go rename to pkg/golinters/golicenser/testdata/fix/in/golicenser_no_changes.go diff --git a/pkg/golinters/golicenser/testdata/fix/in/golicenser_1.go b/pkg/golinters/golicenser/testdata/fix/in/golicenser_no_header.go similarity index 100% rename from pkg/golinters/golicenser/testdata/fix/in/golicenser_1.go rename to pkg/golinters/golicenser/testdata/fix/in/golicenser_no_header.go diff --git a/pkg/golinters/golicenser/testdata/fix/in/golicenser_3.go b/pkg/golinters/golicenser/testdata/fix/in/golicenser_package_comment.go similarity index 100% rename from pkg/golinters/golicenser/testdata/fix/in/golicenser_3.go rename to pkg/golinters/golicenser/testdata/fix/in/golicenser_package_comment.go diff --git a/pkg/golinters/golicenser/testdata/fix/out/golicenser_2.go b/pkg/golinters/golicenser/testdata/fix/out/golicenser_author.go similarity index 100% rename from pkg/golinters/golicenser/testdata/fix/out/golicenser_2.go rename to pkg/golinters/golicenser/testdata/fix/out/golicenser_author.go diff --git a/pkg/golinters/golicenser/testdata/fix/out/golicenser_1.go b/pkg/golinters/golicenser/testdata/fix/out/golicenser_change_comment_style.go similarity index 100% rename from pkg/golinters/golicenser/testdata/fix/out/golicenser_1.go rename to pkg/golinters/golicenser/testdata/fix/out/golicenser_change_comment_style.go diff --git a/pkg/golinters/golicenser/testdata/fix/out/golicenser_4.go b/pkg/golinters/golicenser/testdata/fix/out/golicenser_no_changes.go similarity index 100% rename from pkg/golinters/golicenser/testdata/fix/out/golicenser_4.go rename to pkg/golinters/golicenser/testdata/fix/out/golicenser_no_changes.go diff --git a/pkg/golinters/golicenser/testdata/fix/out/golicenser_5.go b/pkg/golinters/golicenser/testdata/fix/out/golicenser_no_header.go similarity index 100% rename from pkg/golinters/golicenser/testdata/fix/out/golicenser_5.go rename to pkg/golinters/golicenser/testdata/fix/out/golicenser_no_header.go diff --git a/pkg/golinters/golicenser/testdata/fix/out/golicenser_3.go b/pkg/golinters/golicenser/testdata/fix/out/golicenser_package_comment.go similarity index 100% rename from pkg/golinters/golicenser/testdata/fix/out/golicenser_3.go rename to pkg/golinters/golicenser/testdata/fix/out/golicenser_package_comment.go diff --git a/pkg/golinters/golicenser/testdata/golicenser-fix.yml b/pkg/golinters/golicenser/testdata/golicenser-fix.yml index 6bc2393644ca..07fd90c9a2b7 100644 --- a/pkg/golinters/golicenser/testdata/golicenser-fix.yml +++ b/pkg/golinters/golicenser/testdata/golicenser-fix.yml @@ -12,5 +12,5 @@ linters: email: value: "test@example.com" regexp: "(.+)@example\\.com" - projectname: "golangci-lint" - exclude: [] # By default, testdata is excluded. + projectname: + value: "golangci-lint" diff --git a/pkg/golinters/golicenser/testdata/golicenser-template-path.yml b/pkg/golinters/golicenser/testdata/golicenser-template-path.yml index 5367845363c9..5590532eb655 100644 --- a/pkg/golinters/golicenser/testdata/golicenser-template-path.yml +++ b/pkg/golinters/golicenser/testdata/golicenser-template-path.yml @@ -10,5 +10,5 @@ linters: email: value: "test@example.com" regexp: "(.+)@example\\.com" - projectname: "golangci-lint" - exclude: [] # By default, testdata is excluded. + projectname: + value: "golangci-lint" diff --git a/pkg/golinters/golicenser/testdata/golicenser.yml b/pkg/golinters/golicenser/testdata/golicenser.yml index 3d20fc6c4eb9..9db8e570bedf 100644 --- a/pkg/golinters/golicenser/testdata/golicenser.yml +++ b/pkg/golinters/golicenser/testdata/golicenser.yml @@ -16,5 +16,5 @@ linters: email: value: "test@example.com" regexp: "(.+)@example\\.com" - projectname: "golangci-lint" - exclude: [] # By default, testdata is excluded. + projectname: + value: "golangci-lint" diff --git a/pkg/golinters/golicenser/testdata/golicenser_cgo.go b/pkg/golinters/golicenser/testdata/golicenser_cgo.go index f8049653b733..9cd30c66f7ac 100644 --- a/pkg/golinters/golicenser/testdata/golicenser_cgo.go +++ b/pkg/golinters/golicenser/testdata/golicenser_cgo.go @@ -1,12 +1,5 @@ //go:build ignore -// TODO(joshuasing): golicenser doesn't currently support cgo, for a few reasons: -// - source file will be copied to go-build cache -// - copied source file will contain generated comment, causing it to be excluded -// - modifying the copied go-build cache file does not result in the actual source file being fixed -// I would like to eventually support cgo, however for now, if using cgo, it is recommended to add to exclude: -// - */go-build/* - // Copyright (c) 2025 golangci-lint . // want "invalid license header" // This file is a part of golangci-lint.