diff --git a/.golangci.next.reference.yml b/.golangci.next.reference.yml index 37866ff00f2c..11c1a5a4fb38 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,79 @@ linters: # Default: "" 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: + # - `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. + # 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: /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. + # 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 + # 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: + regexp: "(.+)@mycompany\\.com" + project-name: + value: "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" + 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..427edd221583 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,96 @@ { "required": ["template-path"] } ] }, + "golicenserSettings": { + "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", + "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." + ] + }, + "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", + "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", + "additionalProperties": false, + "patternProperties": { + "^[a-z\\-]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "value": { + "description": "Variable value", + "type": "string" + }, + "regexp": { + "description": "Variable matcher regexp", + "type": "string" + } + } + } + } + }, + "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" + } + }, + "oneOf": [ + { "required": ["template", "author"] }, + { "required": ["template-path", "author"] } + ] + } + }, + "required": ["header"] + }, "goimportsSettings": { "type": "object", "additionalProperties": false, @@ -4399,6 +4490,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..c39762d3a5cc 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,28 @@ type GoHeaderSettings struct { TemplatePath string `mapstructure:"template-path"` } +type GoLicenserSettings struct { + Header GoLicenserHeaderSettings `mapstructure:"header"` + 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]GoLicenserVar `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..0c2f22f79b6b --- /dev/null +++ b/pkg/golinters/golicenser/golicenser.go @@ -0,0 +1,94 @@ +package golicenser + +import ( + "fmt" + "os" + "strings" + + "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" + +func New(settings *config.GoLicenserSettings, replacer *strings.Replacer) *goanalysis.Linter { + conf, err := createConfig(settings, replacer) + if err != nil { + internal.LinterLogger.Fatalf("%s: parse year mode: %v", linterName, err) + } + + analyzer, err := golicenser.NewAnalyzer(conf) + if err != nil { + internal.LinterLogger.Fatalf("%s: create analyzer: %v", linterName, err) + } + + return goanalysis.NewLinter( + linterName, + "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/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_author.go b/pkg/golinters/golicenser/testdata/fix/in/golicenser_author.go new file mode 100644 index 000000000000..9ab229621b38 --- /dev/null +++ b/pkg/golinters/golicenser/testdata/fix/in/golicenser_author.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_change_comment_style.go b/pkg/golinters/golicenser/testdata/fix/in/golicenser_change_comment_style.go new file mode 100644 index 000000000000..35b435aa0579 --- /dev/null +++ b/pkg/golinters/golicenser/testdata/fix/in/golicenser_change_comment_style.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/in/golicenser_no_changes.go b/pkg/golinters/golicenser/testdata/fix/in/golicenser_no_changes.go new file mode 100644 index 000000000000..a255ae4c02ae --- /dev/null +++ b/pkg/golinters/golicenser/testdata/fix/in/golicenser_no_changes.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_no_header.go b/pkg/golinters/golicenser/testdata/fix/in/golicenser_no_header.go new file mode 100644 index 000000000000..b20aa9063a26 --- /dev/null +++ b/pkg/golinters/golicenser/testdata/fix/in/golicenser_no_header.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_package_comment.go b/pkg/golinters/golicenser/testdata/fix/in/golicenser_package_comment.go new file mode 100644 index 000000000000..1923408bd058 --- /dev/null +++ b/pkg/golinters/golicenser/testdata/fix/in/golicenser_package_comment.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/out/golicenser_author.go b/pkg/golinters/golicenser/testdata/fix/out/golicenser_author.go new file mode 100644 index 000000000000..721ea8d5e851 --- /dev/null +++ b/pkg/golinters/golicenser/testdata/fix/out/golicenser_author.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_change_comment_style.go b/pkg/golinters/golicenser/testdata/fix/out/golicenser_change_comment_style.go new file mode 100644 index 000000000000..142b7a398a47 --- /dev/null +++ b/pkg/golinters/golicenser/testdata/fix/out/golicenser_change_comment_style.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_no_changes.go b/pkg/golinters/golicenser/testdata/fix/out/golicenser_no_changes.go new file mode 100644 index 000000000000..a255ae4c02ae --- /dev/null +++ b/pkg/golinters/golicenser/testdata/fix/out/golicenser_no_changes.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_no_header.go b/pkg/golinters/golicenser/testdata/fix/out/golicenser_no_header.go new file mode 100644 index 000000000000..142b7a398a47 --- /dev/null +++ b/pkg/golinters/golicenser/testdata/fix/out/golicenser_no_header.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_package_comment.go b/pkg/golinters/golicenser/testdata/fix/out/golicenser_package_comment.go new file mode 100644 index 000000000000..cde2984c8eff --- /dev/null +++ b/pkg/golinters/golicenser/testdata/fix/out/golicenser_package_comment.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/golicenser-fix.yml b/pkg/golinters/golicenser/testdata/golicenser-fix.yml new file mode 100644 index 000000000000..07fd90c9a2b7 --- /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: + value: "golangci-lint" 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..5590532eb655 --- /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: + value: "golangci-lint" diff --git a/pkg/golinters/golicenser/testdata/golicenser.yml b/pkg/golinters/golicenser/testdata/golicenser.yml new file mode 100644 index 000000000000..9db8e570bedf --- /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: + value: "golangci-lint" diff --git a/pkg/golinters/golicenser/testdata/golicenser_cgo.go b/pkg/golinters/golicenser/testdata/golicenser_cgo.go new file mode 100644 index 000000000000..9cd30c66f7ac --- /dev/null +++ b/pkg/golinters/golicenser/testdata/golicenser_cgo.go @@ -0,0 +1,28 @@ +//go:build ignore + +// 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_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/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/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 5ac3e5ba2fb0..8e701a0e1462 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, placeholderReplacer)). + WithSince("v2.2.0"). + WithAutoFix(). + WithURL("https://github.com/joshuasing/golicenser"), + linter.NewConfig(goimports.New(&cfg.Linters.Settings.GoImports)). WithSince("v1.20.0"). WithAutoFix().