From 9e3e45e761fc47f4139ef6f2d2a282bbdce793b2 Mon Sep 17 00:00:00 2001 From: Denis Tingaikin Date: Sun, 13 Apr 2025 17:16:52 +0300 Subject: [PATCH 01/10] fix issues: 1. https://github.com/denis-tingaikin/go-header/issues/35 2. https://github.com/denis-tingaikin/go-header/issues/43 Signed-off-by: Denis Tingaikin --- analyzer.go | 49 +++++++- analyzer_test.go | 286 ++++++++++++++++++++++++++++++++++++++++++++++- reader.go | 26 ++--- 3 files changed, 341 insertions(+), 20 deletions(-) diff --git a/analyzer.go b/analyzer.go index c6b361f..85f0374 100644 --- a/analyzer.go +++ b/analyzer.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2024 Denis Tingaikin +// Copyright (c) 2020-2025 Denis Tingaikin // // SPDX-License-Identifier: Apache-2.0 // @@ -74,7 +74,7 @@ func (a *Analyzer) Analyze(target *Target) (i Issue) { } if err := a.processPerTargetValues(target); err != nil { - return &issue{msg: err.Error()} + return NewIssue(err.Error()) } file := target.File @@ -82,6 +82,17 @@ func (a *Analyzer) Analyze(target *Target) (i Issue) { var offset = Location{ Position: 1, } + + if isNewLinewRequired(file.Comments) { + return NewIssueWithLocation( + "Missing a newline after the header. Consider adding a newline separator right after the copyright header.", + Location{ + Position: int(file.Comments[0].End()), + Line: countLines(file.Comments[0].Text()), + }, + ) + } + if len(file.Comments) > 0 && file.Comments[0].Pos() < file.Package { if strings.HasPrefix(file.Comments[0].List[0].Text, "/*") { header = (&ast.CommentGroup{List: []*ast.Comment{file.Comments[0].List[0]}}).Text() @@ -254,3 +265,37 @@ func (a *Analyzer) generateFix(i Issue, file *ast.File, header string) (Fix, boo fix.Actual = append(fix.Actual, strings.Split(actual, "\n")...) return fix, true } + +func isNewLinewRequired(group []*ast.CommentGroup) bool { + if len(group[0].List) > 1 { + for _, item := range group[0].List { + if strings.HasPrefix(item.Text, "/*") { + return true + } + } + } + + if len(group) < 2 { + return false + } + return group[0].End()+group[0].Pos() >= group[1].Pos() +} + +func countLines(text string) int { + if text == "" { + return 0 + } + + lines := 1 + for i := 0; i < len(text); i++ { + if text[i] == '\n' { + lines++ + } else if text[i] == '\r' { + lines++ + if i+1 < len(text) && text[i+1] == '\n' { + i++ + } + } + } + return lines +} diff --git a/analyzer_test.go b/analyzer_test.go index 6a0f25f..a96b7c7 100644 --- a/analyzer_test.go +++ b/analyzer_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2024 Denis Tingaikin +// Copyright (c) 2020-2025 Denis Tingaikin // // SPDX-License-Identifier: Apache-2.0 // @@ -22,6 +22,7 @@ import ( "go/parser" "go/token" "os" + "path" "path/filepath" "testing" "time" @@ -31,7 +32,7 @@ import ( "github.com/stretchr/testify/require" ) -func header(header string) *goheader.Target { +func header(t *testing.T, header string) *goheader.Target { return &goheader.Target{ File: &ast.File{ Comments: []*ast.CommentGroup{ @@ -45,7 +46,7 @@ func header(header string) *goheader.Target { }, Package: token.Pos(len(header)), }, - Path: os.TempDir(), + Path: t.TempDir(), } } @@ -261,5 +262,282 @@ func TestAnalyzer_YearRangeValue_ShouldWorkWithComplexVariables(t *testing.T) { } var a = goheader.New(goheader.WithTemplate("A {{ my-val }}"), goheader.WithValues(vals)) - require.Nil(t, a.Analyze(header(fmt.Sprintf("A 2000-%v B", time.Now().Year())))) + require.Nil(t, a.Analyze(header(t, fmt.Sprintf("A 2000-%v B", time.Now().Year())))) +} + +func TestAnalyzer_UnicodeHeaders(t *testing.T) { + a := goheader.New( + goheader.WithTemplate("😊早安😊"), + ) + issue := a.Analyze(header(t, `😊早安😊`)) + require.Nil(t, issue) +} + +func TestAnalyzer_Analyze1(t *testing.T) { + a := goheader.New( + goheader.WithTemplate("A {{ YEAR }}\nB"), + goheader.WithValues(map[string]goheader.Value{ + "YEAR": &goheader.ConstValue{ + RawValue: "2020", + }, + })) + issue := a.Analyze(header(t, `A 2020 +B`)) + require.Nil(t, issue) +} + +func TestAnalyzer_Analyze2(t *testing.T) { + a := goheader.New( + goheader.WithTemplate("{{COPYRIGHT HOLDER}}TEXT"), + goheader.WithValues(map[string]goheader.Value{ + "COPYRIGHT HOLDER": &goheader.RegexpValue{ + RawValue: "(A {{ YEAR }}\n(.*)\n)+", + }, + "YEAR": &goheader.ConstValue{ + RawValue: "2020", + }, + })) + issue := a.Analyze(header(t, `A 2020 +B +A 2020 +B +TEXT +`)) + require.Nil(t, issue) +} + +func TestAnalyzer_Analyze3(t *testing.T) { + a := goheader.New( + goheader.WithTemplate("{{COPYRIGHT HOLDER}}TEXT"), + goheader.WithValues(map[string]goheader.Value{ + "COPYRIGHT HOLDER": &goheader.RegexpValue{ + RawValue: "(A {{ YEAR }}\n(.*)\n)+", + }, + "YEAR": &goheader.ConstValue{ + RawValue: "2020", + }, + })) + issue := a.Analyze(header(t, `A 2020 +B +A 2021 +B +TEXT +`)) + require.NotNil(t, issue) +} + +func TestAnalyzer_Analyze4(t *testing.T) { + a := goheader.New( + goheader.WithTemplate("{{ A }}"), + goheader.WithValues(map[string]goheader.Value{ + "A": &goheader.RegexpValue{ + RawValue: "[{{ B }}{{ C }}]{{D}}", + }, + "B": &goheader.ConstValue{ + RawValue: "a-", + }, + "C": &goheader.RegexpValue{ + RawValue: "z", + }, + "D": &goheader.ConstValue{ + RawValue: "{{E}}", + }, + "E": &goheader.ConstValue{ + RawValue: "{7}", + }, + })) + issue := a.Analyze(header(t, `abcdefg`)) + require.Nil(t, issue) +} + +func TestAnalyzer_Analyze5(t *testing.T) { + a := goheader.New(goheader.WithTemplate("abc")) + p := path.Join(os.TempDir(), t.Name()+".go") + defer func() { + _ = os.Remove(p) + }() + err := os.WriteFile(p, []byte("/*abc*/\n\n//comment\npackage abc"), os.ModePerm) + require.Nil(t, err) + s := token.NewFileSet() + f, err := parser.ParseFile(s, p, nil, parser.ParseComments) + require.Nil(t, err) + require.Nil(t, a.Analyze(&goheader.Target{File: f, Path: p})) +} + +func TestAnalyzer_Analyze6(t *testing.T) { + a := goheader.New(goheader.WithTemplate("abc")) + p := path.Join(t.TempDir(), t.Name()+".go") + defer func() { + _ = os.Remove(p) + }() + + err := os.WriteFile(p, []byte("//abc\n\n//comment\npackage abc"), os.ModePerm) + require.Nil(t, err) + s := token.NewFileSet() + f, err := parser.ParseFile(s, p, nil, parser.ParseComments) + require.Nil(t, err) + require.Nil(t, a.Analyze(&goheader.Target{File: f, Path: p})) +} + +func TestREADME(t *testing.T) { + a := goheader.New( + goheader.WithTemplate(`{{ MY COMPANY }} +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at: + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.`), + goheader.WithValues(map[string]goheader.Value{ + "MY COMPANY": &goheader.ConstValue{ + RawValue: "mycompany.com", + }, + })) + issue := a.Analyze(header(t, `mycompany.com +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at: + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.`)) + require.Nil(t, issue) +} + +func TestFix(t *testing.T) { + const pkg = ` + +// Package foo +package foo + +func Foo() { println("Foo") } +` + + analyze := func(header string) goheader.Issue { + a := goheader.New( + goheader.WithTemplate(`{{ MY COMPANY }} +SPDX-License-Identifier: Foo`), + goheader.WithValues(map[string]goheader.Value{ + "MY COMPANY": &goheader.ConstValue{ + RawValue: "mycompany.com", + }, + })) + + p := filepath.Join(t.TempDir(), t.Name()+".go") + err := os.WriteFile(p, []byte(header+pkg), os.ModePerm) + require.Nil(t, err) + fset := token.NewFileSet() + + file, err := parser.ParseFile(fset, p, nil, parser.ParseComments) + require.NoError(t, err) + + issue := a.Analyze(&goheader.Target{ + File: file, + Path: p, + }) + require.NotNil(t, issue) + require.NotNil(t, issue.Fix()) + return issue + } + + t.Run("Line comment", func(t *testing.T) { + issue := analyze(`// mycompany.net +// SPDX-License-Identifier: Foo`) + + require.Equal(t, []string{ + "// mycompany.net", + "// SPDX-License-Identifier: Foo", + }, issue.Fix().Actual) + require.Equal(t, []string{ + "// mycompany.com", + "// SPDX-License-Identifier: Foo", + }, issue.Fix().Expected) + }) + + t.Run("Block comment 1", func(t *testing.T) { + issue := analyze(`/* mycompany.net +SPDX-License-Identifier: Foo */`) + + require.Equal(t, []string{ + "/* mycompany.net", + "SPDX-License-Identifier: Foo */", + }, issue.Fix().Actual) + require.Equal(t, []string{ + "/* mycompany.com", + "SPDX-License-Identifier: Foo */", + }, issue.Fix().Expected) + }) + + t.Run("Block comment 2", func(t *testing.T) { + issue := analyze(`/* +mycompany.net +SPDX-License-Identifier: Foo */`) + + require.Equal(t, []string{ + "/*", + "mycompany.net", + "SPDX-License-Identifier: Foo */", + }, issue.Fix().Actual) + require.Equal(t, []string{ + "/*", + "mycompany.com", + "SPDX-License-Identifier: Foo */", + }, issue.Fix().Expected) + }) + + t.Run("Block comment 3", func(t *testing.T) { + issue := analyze(`/* mycompany.net +SPDX-License-Identifier: Foo +*/`) + + require.Equal(t, []string{ + "/* mycompany.net", + "SPDX-License-Identifier: Foo", + "*/", + }, issue.Fix().Actual) + require.Equal(t, []string{ + "/* mycompany.com", + "SPDX-License-Identifier: Foo", + "*/", + }, issue.Fix().Expected) + }) + + t.Run("Block comment 4", func(t *testing.T) { + issue := analyze(`/* + +mycompany.net +SPDX-License-Identifier: Foo + +*/`) + + require.Equal(t, []string{ + "/*", + "", + "mycompany.net", + "SPDX-License-Identifier: Foo", + "", + "*/", + }, issue.Fix().Actual) + require.Equal(t, []string{ + "/*", + "", + "mycompany.com", + "SPDX-License-Identifier: Foo", + "", + "*/", + }, issue.Fix().Expected) + }) } diff --git a/reader.go b/reader.go index 9c9e88a..4ef9320 100644 --- a/reader.go +++ b/reader.go @@ -1,5 +1,5 @@ /* -Copyright (c) 2020-2022 Denis Tingaikin +Copyright (c) 2020-2025 Denis Tingaikin SPDX-License-Identifier: Apache-2.0 @@ -15,14 +15,15 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + package goheader func NewReader(text string) *Reader { - return &Reader{source: text} + return &Reader{source: []rune(text)} } type Reader struct { - source string + source []rune position int location Location offset Location @@ -44,7 +45,7 @@ func (r *Reader) Peek() rune { if r.Done() { return rune(0) } - return rune(r.source[r.position]) + return r.source[r.position] } func (r *Reader) Done() bool { @@ -55,15 +56,15 @@ func (r *Reader) Next() rune { if r.Done() { return rune(0) } - reuslt := r.Peek() - if reuslt == '\n' { + result := r.Peek() + if result == '\n' { r.location.Line++ r.location.Position = 0 } else { r.location.Position++ } r.position++ - return reuslt + return result } func (r *Reader) Finish() string { @@ -71,7 +72,7 @@ func (r *Reader) Finish() string { return "" } defer r.till() - return r.source[r.position:] + return string(r.source[r.position:]) } func (r *Reader) SetPosition(pos int) { @@ -90,7 +91,7 @@ func (r *Reader) ReadWhile(match func(rune) bool) string { for !r.Done() && match(r.Peek()) { r.Next() } - return r.source[start:r.position] + return string(r.source[start:r.position]) } func (r *Reader) till() { @@ -99,12 +100,9 @@ func (r *Reader) till() { } func (r *Reader) calculateLocation() Location { - min := len(r.source) - if min > r.position { - min = r.position - } + minVal := min(len(r.source), r.position) x, y := 0, 0 - for i := 0; i < min; i++ { + for i := 0; i < minVal; i++ { if r.source[i] == '\n' { y++ x = 0 From 724119f1a8ccb5034fca35422c04285fd5e8308a Mon Sep 17 00:00:00 2001 From: Denis Tingaikin Date: Sun, 13 Apr 2025 17:50:28 +0300 Subject: [PATCH 02/10] fix ci Signed-off-by: Denis Tingaikin --- analyzer.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/analyzer.go b/analyzer.go index 85f0374..6c808c4 100644 --- a/analyzer.go +++ b/analyzer.go @@ -87,8 +87,7 @@ func (a *Analyzer) Analyze(target *Target) (i Issue) { return NewIssueWithLocation( "Missing a newline after the header. Consider adding a newline separator right after the copyright header.", Location{ - Position: int(file.Comments[0].End()), - Line: countLines(file.Comments[0].Text()), + Line: countLines(file.Comments[0].Text()), }, ) } @@ -278,7 +277,7 @@ func isNewLinewRequired(group []*ast.CommentGroup) bool { if len(group) < 2 { return false } - return group[0].End()+group[0].Pos() >= group[1].Pos() + return group[0].End() >= group[1].Pos() } func countLines(text string) int { From 123ab1fa8df4cc105354b27a3a323a49dd19432f Mon Sep 17 00:00:00 2001 From: Denis Tingaikin Date: Thu, 8 May 2025 18:14:51 +0300 Subject: [PATCH 03/10] rebase and add 'vars' Signed-off-by: Denis Tingaikin --- .github/workflows/ci.yml | 2 +- README.md | 15 +++++++++++++- analyzer_test.go | 8 ++++---- cmd/go-header/main.go | 2 +- config.go | 28 +++++++++++--------------- testdata/constvalue/constvalue.yml | 7 +++---- testdata/fix/fix.yml | 7 +++---- testdata/nestedvalues/nestedvalues.yml | 14 ++++++------- 8 files changed, 44 insertions(+), 39 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9257e4..c180e96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,4 +32,4 @@ jobs: run: go install ./... - name: Self-check - run: go-header $(git ls-files | grep -E '.*\.go$') + run: go-header $(git ls-files | grep -E '.*\.go$' | grep - E '.*testdata.*') diff --git a/README.md b/README.md index fa385db..38b53be 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,24 @@ For installation you can simply use `go install`. ```bash go install github.com/denis-tingaikin/go-header/cmd/go-header@latest ``` - ## Configuration To configuring `.go-header.yml` linter you simply need to fill the next fields: + +```yaml +--- +template: # expects header template string. +template-path: # expects path to file with license header string. +vars: # expects `const` or `regexp` node with values where values is a map string to string. + key1: value1 # const value just checks equality. Note `key1` should be used in template string as {{ key1 }} or {{ KEY1 }}. + key2: value2(.*) # regexp value just checks regex match. The value should be a valid regexp pattern. Note `key2` should be used in template string as {{ key2 }} or {{ KEY2 }}. +``` + +## Configuration (DEPRECATED) + +To configuring `.go-header.yml` linter you simply need to fill the next fields: + ```yaml --- template: # expects header template string. diff --git a/analyzer_test.go b/analyzer_test.go index a96b7c7..7d91339 100644 --- a/analyzer_test.go +++ b/analyzer_test.go @@ -85,7 +85,7 @@ func TestAnalyzer_Analyze(t *testing.T) { desc: "header comment", filename: "headercomment/headercomment.go", config: "headercomment/headercomment.yml", - assert: assert.Nil, + assert: assert.NotNil, }, { desc: "readme", @@ -97,7 +97,7 @@ func TestAnalyzer_Analyze(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - cfg := &goheader.Configuration{} + cfg := &goheader.Config{} err := cfg.Parse(filepath.Join("testdata", test.config)) require.NoError(t, err) @@ -223,7 +223,7 @@ func TestAnalyzer_Analyze_fix(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - cfg := &goheader.Configuration{} + cfg := &goheader.Config{} err := cfg.Parse(filepath.Join("testdata", test.config)) require.NoError(t, err) @@ -253,7 +253,7 @@ func TestAnalyzer_Analyze_fix(t *testing.T) { } func TestAnalyzer_YearRangeValue_ShouldWorkWithComplexVariables(t *testing.T) { - var conf goheader.Configuration + var conf goheader.Config var vals, err = conf.GetValues() require.NoError(t, err) diff --git a/cmd/go-header/main.go b/cmd/go-header/main.go index 58764ae..0494545 100644 --- a/cmd/go-header/main.go +++ b/cmd/go-header/main.go @@ -46,7 +46,7 @@ func main() { return } } - c := &goheader.Configuration{} + c := &goheader.Config{} if err := c.Parse(configPath); err != nil { log.Fatal(err.Error()) } diff --git a/config.go b/config.go index 8e0b7b2..c8a62c3 100644 --- a/config.go +++ b/config.go @@ -26,17 +26,20 @@ import ( "gopkg.in/yaml.v3" ) -// Configuration represents go-header linter setup parameters -type Configuration struct { +// Config represents go-header linter setup parameters +type Config struct { // Values is map of values. Supports two types 'const` and `regexp`. Values can be used recursively. + // DEPRECATED: Use Vars instead. Values map[string]map[string]string `yaml:"values"` // Template is template for checking. Uses values. Template string `yaml:"template"` // TemplatePath path to the template file. Useful if need to load the template from a specific file. TemplatePath string `yaml:"template-path"` + // Vars is map of values. Values can be used recursively. + Vars map[string]string `yaml:"vars"` } -func (c *Configuration) builtInValues() map[string]Value { +func (c *Config) builtInValues() map[string]Value { var result = make(map[string]Value) year := fmt.Sprint(time.Now().Year()) result["year-range"] = &RegexpValue{ @@ -48,7 +51,7 @@ func (c *Configuration) builtInValues() map[string]Value { return result } -func (c *Configuration) GetValues() (map[string]Value, error) { +func (c *Config) GetValues() (map[string]Value, error) { var result = c.builtInValues() createConst := func(raw string) Value { return &ConstValue{RawValue: raw} @@ -62,20 +65,13 @@ func (c *Configuration) GetValues() (map[string]Value, error) { result[key] = create(v) } } - for k, v := range c.Values { - switch k { - case "const": - appendValues(v, createConst) - case "regexp": - appendValues(v, createRegexp) - default: - return nil, fmt.Errorf("unknown value type %v", k) - } - } + appendValues(c.Values["const"], createConst) + appendValues(c.Values["regexp"], createRegexp) + appendValues(c.Vars, createRegexp) return result, nil } -func (c *Configuration) GetTemplate() (string, error) { +func (c *Config) GetTemplate() (string, error) { if c.Template != "" { return c.Template, nil } @@ -90,7 +86,7 @@ func (c *Configuration) GetTemplate() (string, error) { } } -func (c *Configuration) Parse(p string) error { +func (c *Config) Parse(p string) error { b, err := os.ReadFile(p) if err != nil { return err diff --git a/testdata/constvalue/constvalue.yml b/testdata/constvalue/constvalue.yml index 533edf0..d48f0d0 100644 --- a/testdata/constvalue/constvalue.yml +++ b/testdata/constvalue/constvalue.yml @@ -1,8 +1,7 @@ template: |- - A {{ YEAR }} + A {{ .YEAR }} B -values: - const: - 'YEAR': '2020' +vars: + 'YEAR': '2020' diff --git a/testdata/fix/fix.yml b/testdata/fix/fix.yml index 264e7b4..ffe7c6f 100644 --- a/testdata/fix/fix.yml +++ b/testdata/fix/fix.yml @@ -1,8 +1,7 @@ template: |- - {{ MY COMPANY }} + {{ .MY_COMPANY }} SPDX-License-Identifier: Foo -values: - const: - 'MY COMPANY': 'mycompany.com' +vars: + 'MY_COMPANY': 'mycompany.com' diff --git a/testdata/nestedvalues/nestedvalues.yml b/testdata/nestedvalues/nestedvalues.yml index bed8105..8e0b940 100644 --- a/testdata/nestedvalues/nestedvalues.yml +++ b/testdata/nestedvalues/nestedvalues.yml @@ -1,10 +1,8 @@ template: '{{ A }}' -values: - regexp: - 'A': '[{{ B }}{{ C }}]{{D}}' - const: - 'B': 'a-' - 'C': 'z' - 'D': '{{E}}' - 'E': '{7}' +vars: + 'A': '[{{ B }}{{ C }}]{{D}}' + 'B': 'a-' + 'C': 'z' + 'D': '{{E}}' + 'E': '{7}' From ebb64bf213a0e9f7ed109b691999ae532e50b291 Mon Sep 17 00:00:00 2001 From: Denis Tingaikin Date: Thu, 8 May 2025 18:31:21 +0300 Subject: [PATCH 04/10] fix tests and ci Signed-off-by: Denis Tingaikin --- .github/workflows/ci.yml | 2 +- README.md | 7 +++---- analyzer.go | 11 +++++++++-- analyzer_test.go | 2 +- go.mod | 2 +- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c180e96..d03790b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,4 +32,4 @@ jobs: run: go install ./... - name: Self-check - run: go-header $(git ls-files | grep -E '.*\.go$' | grep - E '.*testdata.*') + run: go-header $(git ls-files | grep -E '.*\.go$' | grep -v 'testdata') diff --git a/README.md b/README.md index 38b53be..595f7bf 100644 --- a/README.md +++ b/README.md @@ -72,11 +72,10 @@ Create configuration file `.go-header.yml` in the root of project. ```yaml --- -values: - const: - MY COMPANY: mycompany.com +vars: + MY_COMPANY: mycompany.com template: | - {{ MY COMPANY }} + {{ .MY_COMPANY }} SPDX-License-Identifier: Apache-2.0 Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/analyzer.go b/analyzer.go index 6c808c4..4f65e86 100644 --- a/analyzer.go +++ b/analyzer.go @@ -23,6 +23,7 @@ import ( "os/exec" "strings" "time" + "unicode" ) type Target struct { @@ -83,7 +84,7 @@ func (a *Analyzer) Analyze(target *Target) (i Issue) { Position: 1, } - if isNewLinewRequired(file.Comments) { + if isNewLineRequired(file.Comments) { return NewIssueWithLocation( "Missing a newline after the header. Consider adding a newline separator right after the copyright header.", Location{ @@ -157,6 +158,12 @@ func (a *Analyzer) readField(reader *Reader) string { _ = reader.Next() _ = reader.Next() + _ = reader.ReadWhile(unicode.IsSpace) + + if reader.Peek() == '.' { + _ = reader.Next() + } + r := reader.ReadWhile(func(r rune) bool { return r != '}' }) @@ -265,7 +272,7 @@ func (a *Analyzer) generateFix(i Issue, file *ast.File, header string) (Fix, boo return fix, true } -func isNewLinewRequired(group []*ast.CommentGroup) bool { +func isNewLineRequired(group []*ast.CommentGroup) bool { if len(group[0].List) > 1 { for _, item := range group[0].List { if strings.HasPrefix(item.Text, "/*") { diff --git a/analyzer_test.go b/analyzer_test.go index 7d91339..2de26fc 100644 --- a/analyzer_test.go +++ b/analyzer_test.go @@ -311,7 +311,7 @@ func TestAnalyzer_Analyze3(t *testing.T) { goheader.WithTemplate("{{COPYRIGHT HOLDER}}TEXT"), goheader.WithValues(map[string]goheader.Value{ "COPYRIGHT HOLDER": &goheader.RegexpValue{ - RawValue: "(A {{ YEAR }}\n(.*)\n)+", + RawValue: "(A {{ .YEAR }}\n(.*)\n)+", }, "YEAR": &goheader.ConstValue{ RawValue: "2020", diff --git a/go.mod b/go.mod index 27aa697..ad7d1d3 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/denis-tingaikin/go-header -go 1.21 +go 1.24 require ( github.com/stretchr/testify v1.7.0 From adc22d3936e194e8e7edb7a4861da41803297c6b Mon Sep 17 00:00:00 2001 From: Denis Tingaikin Date: Thu, 8 May 2025 18:33:36 +0300 Subject: [PATCH 05/10] fix self check Signed-off-by: Denis Tingaikin --- cmd/go-header/main.go | 2 +- config.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/go-header/main.go b/cmd/go-header/main.go index 0494545..375eae5 100644 --- a/cmd/go-header/main.go +++ b/cmd/go-header/main.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2022 Denis Tingaikin +// Copyright (c) 2020-2025 Denis Tingaikin // // SPDX-License-Identifier: Apache-2.0 // diff --git a/config.go b/config.go index c8a62c3..cc516be 100644 --- a/config.go +++ b/config.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2024 Denis Tingaikin +// Copyright (c) 2020-2025 Denis Tingaikin // // SPDX-License-Identifier: Apache-2.0 // From 0c76dad89fd56703697fc633d36680d62730616a Mon Sep 17 00:00:00 2001 From: Denis Tingaikin Date: Thu, 8 May 2025 19:18:26 +0300 Subject: [PATCH 06/10] add star-block like headers support Signed-off-by: Denis Tingaikin --- analyzer.go | 26 ++++++++++++++++++++++++++ analyzer_test.go | 6 ++++++ testdata/starcomment/starcomment.go | 6 ++++++ testdata/starcomment/starcomment.yml | 7 +++++++ 4 files changed, 45 insertions(+) create mode 100644 testdata/starcomment/starcomment.go create mode 100644 testdata/starcomment/starcomment.yml diff --git a/analyzer.go b/analyzer.go index 4f65e86..2dc3a83 100644 --- a/analyzer.go +++ b/analyzer.go @@ -96,6 +96,8 @@ func (a *Analyzer) Analyze(target *Target) (i Issue) { if len(file.Comments) > 0 && file.Comments[0].Pos() < file.Package { if strings.HasPrefix(file.Comments[0].List[0].Text, "/*") { header = (&ast.CommentGroup{List: []*ast.Comment{file.Comments[0].List[0]}}).Text() + + header = handleStarBlock(header) } else { header = file.Comments[0].Text() offset.Position += 3 @@ -112,6 +114,7 @@ func (a *Analyzer) Analyze(target *Target) (i Issue) { i = NewIssueWithFix(i.Message(), i.Location(), fix) }() header = strings.TrimSpace(header) + if header == "" { return NewIssue("Missed header for check") } @@ -305,3 +308,26 @@ func countLines(text string) int { } return lines } + +func handleStarBlock(header string) string { + return trimEachLine(header, func(s string) string { + var trimmed = strings.TrimSpace(s) + if !strings.HasPrefix(trimmed, "*") { + return s + } + if v, ok := strings.CutPrefix(trimmed, "* "); ok { + return v + } else { + var res, _ = strings.CutPrefix(trimmed, "*") + return res + } + }) +} + +func trimEachLine(input string, trimFunc func(string) string) string { + lines := strings.Split(input, "\n") + for i, line := range lines { + lines[i] = trimFunc(line) + } + return strings.Join(lines, "\n") +} diff --git a/analyzer_test.go b/analyzer_test.go index 2de26fc..0ba87f2 100644 --- a/analyzer_test.go +++ b/analyzer_test.go @@ -93,6 +93,12 @@ func TestAnalyzer_Analyze(t *testing.T) { config: "readme/readme.yml", assert: assert.Nil, }, + { + desc: "star-block like header", + filename: "starcomment/starcomment.go", + config: "starcomment/starcomment.yml", + assert: assert.Nil, + }, } for _, test := range testCases { diff --git a/testdata/starcomment/starcomment.go b/testdata/starcomment/starcomment.go new file mode 100644 index 0000000..c544232 --- /dev/null +++ b/testdata/starcomment/starcomment.go @@ -0,0 +1,6 @@ +/* + * A 2020 + * B + */ + +package constvalue diff --git a/testdata/starcomment/starcomment.yml b/testdata/starcomment/starcomment.yml new file mode 100644 index 0000000..d48f0d0 --- /dev/null +++ b/testdata/starcomment/starcomment.yml @@ -0,0 +1,7 @@ +template: |- + A {{ .YEAR }} + B + +vars: + 'YEAR': '2020' + From 8186b8e9c9ed5a294e728e6b6736185020b0b15b Mon Sep 17 00:00:00 2001 From: Denis Tingaikin Date: Thu, 8 May 2025 20:43:25 +0300 Subject: [PATCH 07/10] add analysis.Analyzer Signed-off-by: Denis Tingaikin --- .github/workflows/ci.yml | 6 +-- README.md | 5 +- analysis.go | 105 +++++++++++++++++++++++++++++++++++++++ analyzer.go | 41 ++++++++------- cmd/go-header/main.go | 47 ++++-------------- go.mod | 3 ++ go.sum | 8 +++ version/version.go | 4 +- 8 files changed, 157 insertions(+), 62 deletions(-) create mode 100644 analysis.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d03790b..d56a22e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,10 +7,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up Go 1.21 + - name: Set up Go 1.24 uses: actions/setup-go@v1 with: - go-version: 1.21 + go-version: 1.24 - name: Check out code into the Go module directory uses: actions/checkout@v2 @@ -32,4 +32,4 @@ jobs: run: go install ./... - name: Self-check - run: go-header $(git ls-files | grep -E '.*\.go$' | grep -v 'testdata') + run: go-header ./... diff --git a/README.md b/README.md index 595f7bf..2340d44 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,8 @@ Create configuration file `.go-header.yml` in the root of project. ```yaml --- vars: - MY_COMPANY: mycompany.com + DOMAIN: sales|product + MY_COMPANY: {{ .DOMAIN }}.mycompany.com template: | {{ .MY_COMPANY }} SPDX-License-Identifier: Apache-2.0 @@ -92,4 +93,4 @@ template: | ``` ### Step 2 -You are ready! Execute `go-header ${PATH_TO_FILES}` from the root of the project. +Run `go-header ./...` diff --git a/analysis.go b/analysis.go new file mode 100644 index 0000000..b84ecaf --- /dev/null +++ b/analysis.go @@ -0,0 +1,105 @@ +// Copyright (c) 2025 Denis Tingaikin +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package goheader + +import ( + "go/token" + "strings" + + "golang.org/x/tools/go/analysis" +) + +func NewAnalyzer(template string, vals map[string]Value) *analysis.Analyzer { + var goheader = New(WithTemplate(template), WithValues(vals)) + + return &analysis.Analyzer{ + Doc: "the_only_doc", + URL: "https://github.com/denis-tingaikin/go-header", + Name: "goheader", + Run: func(p *analysis.Pass) (any, error) { + for _, file := range p.Files { + filename := p.Fset.Position(file.Pos()).Filename + if !strings.HasSuffix(filename, ".go") { + continue + } + + issue := goheader.Analyze(&Target{ + File: file, + Path: filename, + }) + + if issue == nil { + continue + } + f := p.Fset.File(file.Pos()) + + commentLine := 1 + var offset int + + // Inspired by https://github.com/denis-tingaikin/go-header/blob/4c75a6a2332f025705325d6c71fff4616aedf48f/analyzer.go#L85-L92 + if len(file.Comments) > 0 && file.Comments[0].Pos() < file.Package { + if !strings.HasPrefix(file.Comments[0].List[0].Text, "/*") { + // When the comment is "//" there is a one character offset. + offset = 1 + } + commentLine = p.Fset.PositionFor(file.Comments[0].Pos(), true).Line + } + + // Skip issues related to build directives. + // https://github.com/denis-tingaikin/go-header/issues/18 + if issue.Location().Position-offset < 0 { + continue + } + + diag := analysis.Diagnostic{ + Pos: f.LineStart(issue.Location().Line+1) + token.Pos(issue.Location().Position-offset), // The position of the first divergence. + Message: issue.Message(), + } + + if fix := issue.Fix(); fix != nil { + current := len(fix.Actual) + for _, s := range fix.Actual { + current += len(s) + } + + start := f.LineStart(commentLine) + + end := start + token.Pos(current) + + header := strings.Join(fix.Expected, "\n") + "\n" + + // Adds an extra line between the package and the header. + if end == file.Package { + header += "\n" + } + + diag.SuggestedFixes = []analysis.SuggestedFix{{ + TextEdits: []analysis.TextEdit{{ + Pos: start, + End: end, + NewText: []byte(header), + }}, + }} + } + + p.Report(diag) + } + + return nil, nil + }, + } +} diff --git a/analyzer.go b/analyzer.go index 2dc3a83..360c52a 100644 --- a/analyzer.go +++ b/analyzer.go @@ -53,28 +53,35 @@ type Analyzer struct { template string } -func (a *Analyzer) processPerTargetValues(target *Target) error { - a.values["mod-year"] = a.values["year"] - a.values["mod-year-range"] = a.values["year-range"] +func (a *Analyzer) getPerTargetValues(target *Target) (map[string]Value, error) { + var res = make(map[string]Value) + + for k, v := range a.values { + res[k] = v + } + + res["mod-year"] = a.values["year"] + res["mod-year-range"] = a.values["year-range"] if t, err := target.ModTime(); err == nil { - a.values["mod-year"] = &ConstValue{RawValue: fmt.Sprint(t.Year())} - a.values["mod-year-range"] = &RegexpValue{RawValue: `((20\d\d\-{{mod-year}})|({{mod-year}}))`} + res["mod-year"] = &ConstValue{RawValue: fmt.Sprint(t.Year())} + res["mod-year-range"] = &RegexpValue{RawValue: `((20\d\d\-{{mod-year}})|({{mod-year}}))`} } - for _, v := range a.values { - if err := v.Calculate(a.values); err != nil { - return err + for _, v := range res { + if err := v.Calculate(res); err != nil { + return nil, err } } - return nil + + return res, nil } func (a *Analyzer) Analyze(target *Target) (i Issue) { if a.template == "" { return NewIssue("Missed template for check") } - - if err := a.processPerTargetValues(target); err != nil { + vals, err := a.getPerTargetValues(target) + if err != nil { return NewIssue(err.Error()) } @@ -107,7 +114,7 @@ func (a *Analyzer) Analyze(target *Target) (i Issue) { if i == nil { return } - fix, ok := a.generateFix(i, file, header) + fix, ok := a.generateFix(i, file, header, vals) if !ok { return } @@ -125,10 +132,10 @@ func (a *Analyzer) Analyze(target *Target) (i Issue) { templateCh := t.Peek() if templateCh == '{' { name := a.readField(t) - if a.values[name] == nil { + if vals[name] == nil { return NewIssue(fmt.Sprintf("Template has unknown value: %v", name)) } - if i := a.values[name].Read(s); i != nil { + if i := vals[name].Read(s); i != nil { return i } continue @@ -185,17 +192,17 @@ func New(options ...Option) *Analyzer { return a } -func (a *Analyzer) generateFix(i Issue, file *ast.File, header string) (Fix, bool) { +func (a *Analyzer) generateFix(i Issue, file *ast.File, header string, vals map[string]Value) (Fix, bool) { var expect string t := NewReader(a.template) for !t.Done() { ch := t.Peek() if ch == '{' { - f := a.values[a.readField(t)] + f := vals[a.readField(t)] if f == nil { return Fix{}, false } - if f.Calculate(a.values) != nil { + if f.Calculate(vals) != nil { return Fix{}, false } expect += f.Get() diff --git a/cmd/go-header/main.go b/cmd/go-header/main.go index 375eae5..ecf7337 100644 --- a/cmd/go-header/main.go +++ b/cmd/go-header/main.go @@ -18,22 +18,16 @@ package main import ( "fmt" - "go/parser" - "go/token" "os" "log" goheader "github.com/denis-tingaikin/go-header" "github.com/denis-tingaikin/go-header/version" + "golang.org/x/tools/go/analysis/singlechecker" ) -const configPath = ".go-header.yml" - -type issue struct { - goheader.Issue - filePath string -} +const defaultConfigPath = ".go-header.yml" func main() { paths := os.Args[1:] @@ -47,40 +41,17 @@ func main() { } } c := &goheader.Config{} - if err := c.Parse(configPath); err != nil { + if err := c.Parse(defaultConfigPath); err != nil { log.Fatal(err.Error()) } - v, err := c.GetValues() + tmpl, err := c.GetTemplate() if err != nil { - log.Fatalf("Can not get values: %v", err.Error()) + log.Fatal(err.Error()) } - t, err := c.GetTemplate() + vals, err := c.GetValues() if err != nil { - log.Fatalf("Can not get template: %v", err.Error()) - } - a := goheader.New(goheader.WithValues(v), goheader.WithTemplate(t)) - s := token.NewFileSet() - var issues []*issue - for _, p := range paths { - f, err := parser.ParseFile(s, p, nil, parser.ParseComments) - if err != nil { - log.Fatalf("File %v can not be parsed due compilation errors %v", p, err.Error()) - } - i := a.Analyze(&goheader.Target{ - Path: p, - File: f, - }) - if i != nil { - issues = append(issues, &issue{ - Issue: i, - filePath: p, - }) - } - } - if len(issues) > 0 { - for _, issue := range issues { - fmt.Printf("%v:%v\n%v\n", issue.filePath, issue.Location(), issue.Message()) - } - os.Exit(-1) + log.Fatal(err.Error()) } + + singlechecker.Main(goheader.NewAnalyzer(tmpl, vals)) } diff --git a/go.mod b/go.mod index ad7d1d3..8000bc3 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,13 @@ go 1.24 require ( github.com/stretchr/testify v1.7.0 + golang.org/x/tools v0.33.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/sync v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index e43413c..4bf9502 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,19 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/version/version.go b/version/version.go index c1c5eb2..6c48fc7 100644 --- a/version/version.go +++ b/version/version.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2024 Denis Tingaikin +// Copyright (c) 2020-2025 Denis Tingaikin // // SPDX-License-Identifier: Apache-2.0 // @@ -17,5 +17,5 @@ package version func Value() string { - return "v0.5.0" + return "v1.0.0" } From 7a4c9abbf4d1b21085d3745d3721854bda756908 Mon Sep 17 00:00:00 2001 From: Denis Tingaikin Date: Thu, 8 May 2025 21:33:15 +0300 Subject: [PATCH 08/10] add parallel execution support and plans Signed-off-by: Denis Tingaikin --- README.md | 20 +++++- analysis.go | 163 ++++++++++++++++++++++++++---------------- cmd/go-header/main.go | 24 +++---- 3 files changed, 131 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 2340d44..e8d60b1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,23 @@ # go-header [![ci](https://github.com/denis-tingaikin/go-header/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/denis-tingaikin/go-header/actions/workflows/ci.yml) -Go source code linter providing checks for license headers. +Go source code linter providing checks for license headers. + +## Features + +| Feature | Status | Details | +|-----------------------------|--------|------------------------------------------| +| ✅ **Copyright Headers** | ✔️ | Supports all standard formats | +| ✅ **Parallel Processing** | ✔️ | Processes files concurrently | +| ✅ **Comment Support** | ✔️ | `//`, `/* */`, `/* * */`| +| ✅ **Go/Analysis** | ✔️ | Native Go tooling integration | +| ✅ **Regex Customization** | ✔️ | User-defined pattern matching | +| ✅ **Automatic Year Checks** | ✔️ | Validates & updates copyright years | +| ✅ **Auto-Fix Files** | ✔️ | In-place header corrections | +| ⏳ **Go/Template Support** | ❌ | *In development* | +| ⏳ **Multi-License Support** | ❌ | *Planned* | + + ## Installation @@ -61,7 +77,7 @@ values: `go-header` linter expects file paths on input. If you want to run `go-header` only on diff files, then you can use this command: ```bash -go-header $(git diff --name-only | grep -E '.*\.go') +go-header ./... ``` ## Setup example diff --git a/analysis.go b/analysis.go index b84ecaf..6cefd44 100644 --- a/analysis.go +++ b/analysis.go @@ -17,88 +17,131 @@ package goheader import ( + "go/ast" "go/token" + "runtime" "strings" + "sync" "golang.org/x/tools/go/analysis" ) -func NewAnalyzer(template string, vals map[string]Value) *analysis.Analyzer { - var goheader = New(WithTemplate(template), WithValues(vals)) +func NewAnalyzer(config *string) *analysis.Analyzer { + var goheaderOncer sync.Once + var goheader *Analyzer return &analysis.Analyzer{ Doc: "the_only_doc", URL: "https://github.com/denis-tingaikin/go-header", Name: "goheader", Run: func(p *analysis.Pass) (any, error) { - for _, file := range p.Files { - filename := p.Fset.Position(file.Pos()).Filename - if !strings.HasSuffix(filename, ".go") { - continue + var err error + goheaderOncer.Do(func() { + var cfg Config + if err = cfg.Parse(*config); err != nil { + return } - - issue := goheader.Analyze(&Target{ - File: file, - Path: filename, - }) - - if issue == nil { - continue - } - f := p.Fset.File(file.Pos()) - - commentLine := 1 - var offset int - - // Inspired by https://github.com/denis-tingaikin/go-header/blob/4c75a6a2332f025705325d6c71fff4616aedf48f/analyzer.go#L85-L92 - if len(file.Comments) > 0 && file.Comments[0].Pos() < file.Package { - if !strings.HasPrefix(file.Comments[0].List[0].Text, "/*") { - // When the comment is "//" there is a one character offset. - offset = 1 - } - commentLine = p.Fset.PositionFor(file.Comments[0].Pos(), true).Line - } - - // Skip issues related to build directives. - // https://github.com/denis-tingaikin/go-header/issues/18 - if issue.Location().Position-offset < 0 { - continue + templ, err := cfg.GetTemplate() + if err != nil { + return } - - diag := analysis.Diagnostic{ - Pos: f.LineStart(issue.Location().Line+1) + token.Pos(issue.Location().Position-offset), // The position of the first divergence. - Message: issue.Message(), + vars, err := cfg.GetValues() + if err != nil { + return } + goheader = New(WithTemplate(templ), WithValues(vars)) + }) - if fix := issue.Fix(); fix != nil { - current := len(fix.Actual) - for _, s := range fix.Actual { - current += len(s) - } - - start := f.LineStart(commentLine) + if err != nil { + return nil, err + } - end := start + token.Pos(current) + var wg sync.WaitGroup - header := strings.Join(fix.Expected, "\n") + "\n" + var jobCh = make(chan *ast.File, len(p.Files)) - // Adds an extra line between the package and the header. - if end == file.Package { - header += "\n" + for _, file := range p.Files { + jobCh <- file + } + close(jobCh) + + for range runtime.NumCPU() { + wg.Add(1) + go func() { + defer wg.Done() + + for file := range jobCh { + + filename := p.Fset.Position(file.Pos()).Filename + if !strings.HasSuffix(filename, ".go") { + continue + } + + issue := goheader.Analyze(&Target{ + File: file, + Path: filename, + }) + + if issue == nil { + continue + } + f := p.Fset.File(file.Pos()) + + commentLine := 1 + var offset int + + // Inspired by https://github.com/denis-tingaikin/go-header/blob/4c75a6a2332f025705325d6c71fff4616aedf48f/analyzer.go#L85-L92 + if len(file.Comments) > 0 && file.Comments[0].Pos() < file.Package { + if !strings.HasPrefix(file.Comments[0].List[0].Text, "/*") { + // When the comment is "//" there is a one character offset. + offset = 1 + } + commentLine = p.Fset.PositionFor(file.Comments[0].Pos(), true).Line + } + + // Skip issues related to build directives. + // https://github.com/denis-tingaikin/go-header/issues/18 + if issue.Location().Position-offset < 0 { + continue + } + + diag := analysis.Diagnostic{ + Pos: f.LineStart(issue.Location().Line+1) + token.Pos(issue.Location().Position-offset), // The position of the first divergence. + Message: issue.Message(), + } + + if fix := issue.Fix(); fix != nil { + current := len(fix.Actual) + for _, s := range fix.Actual { + current += len(s) + } + + start := f.LineStart(commentLine) + + end := start + token.Pos(current) + + header := strings.Join(fix.Expected, "\n") + "\n" + + // Adds an extra line between the package and the header. + if end == file.Package { + header += "\n" + } + + diag.SuggestedFixes = []analysis.SuggestedFix{{ + TextEdits: []analysis.TextEdit{{ + Pos: start, + End: end, + NewText: []byte(header), + }}, + }} + } + + p.Report(diag) } - - diag.SuggestedFixes = []analysis.SuggestedFix{{ - TextEdits: []analysis.TextEdit{{ - Pos: start, - End: end, - NewText: []byte(header), - }}, - }} - } - - p.Report(diag) + }() } + wg.Wait() return nil, nil }, } diff --git a/cmd/go-header/main.go b/cmd/go-header/main.go index ecf7337..940bb0f 100644 --- a/cmd/go-header/main.go +++ b/cmd/go-header/main.go @@ -17,6 +17,7 @@ package main import ( + "flag" "fmt" "os" @@ -27,7 +28,9 @@ import ( "golang.org/x/tools/go/analysis/singlechecker" ) -const defaultConfigPath = ".go-header.yml" +var defaultConfigPath = ".go-header.yml" + +var flagSet flag.FlagSet func main() { paths := os.Args[1:] @@ -40,18 +43,11 @@ func main() { return } } - c := &goheader.Config{} - if err := c.Parse(defaultConfigPath); err != nil { - log.Fatal(err.Error()) - } - tmpl, err := c.GetTemplate() - if err != nil { - log.Fatal(err.Error()) - } - vals, err := c.GetValues() - if err != nil { - log.Fatal(err.Error()) - } - singlechecker.Main(goheader.NewAnalyzer(tmpl, vals)) + var configPath string + flagSet.StringVar(&configPath, "config", defaultConfigPath, "Path to config file") + var analyser = goheader.NewAnalyzer(&configPath) + analyser.Flags = flagSet + + singlechecker.Main(analyser) } From 167bd465f2645260c490512b54e6294ce86e9104 Mon Sep 17 00:00:00 2001 From: Denis Tingaikin Date: Thu, 8 May 2025 21:37:57 +0300 Subject: [PATCH 09/10] simplify readme Signed-off-by: Denis Tingaikin --- .go-header.yml | 9 ++++----- README.md | 10 +++++++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.go-header.yml b/.go-header.yml index 3aa6d06..ff3337e 100644 --- a/.go-header.yml +++ b/.go-header.yml @@ -1,7 +1,6 @@ -values: - regexp: - copyright-holder: Copyright \(c\) {{mod-year-range}} Denis Tingaikin -template: | +vars: + copyright-holder: Copyright \(c\) {{mod-year-range}} Denis Tingaikin +template: |- {{copyright-holder}} SPDX-License-Identifier: Apache-2.0 @@ -16,4 +15,4 @@ template: | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. diff --git a/README.md b/README.md index e8d60b1..f9e3b6e 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,18 @@ go install github.com/denis-tingaikin/go-header/cmd/go-header@latest To configuring `.go-header.yml` linter you simply need to fill the next fields: +Inline template: ```yaml --- template: # expects header template string. -template-path: # expects path to file with license header string. +vars: # expects valid key value paris where key is string, value is regexp. + key1: value1 # const value just checks equality. Note `key1` should be used in template string as {{ .key1 }} or {{ .KEY1 }}. + key2: value2(.*) # regexp value just checks regex match. The value should be a valid regexp pattern. Note `key2` should be used in template string as {{ .key2 }} or {{ .KEY2 }}. +``` +Filebased template: +```yaml +--- +template-path: # expects header template path string. vars: # expects `const` or `regexp` node with values where values is a map string to string. key1: value1 # const value just checks equality. Note `key1` should be used in template string as {{ key1 }} or {{ KEY1 }}. key2: value2(.*) # regexp value just checks regex match. The value should be a valid regexp pattern. Note `key2` should be used in template string as {{ key2 }} or {{ KEY2 }}. From c7b27507b2fd353f4513e0ffe4e29eb631a4e8cc Mon Sep 17 00:00:00 2001 From: Denis Tingaikin Date: Thu, 8 May 2025 21:56:26 +0300 Subject: [PATCH 10/10] add fixes to be compatible with go tools Signed-off-by: Denis Tingaikin --- analysis.go | 63 ++++++++++++++++++++++++++----------------- cmd/go-header/main.go | 2 +- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/analysis.go b/analysis.go index 6cefd44..799c89a 100644 --- a/analysis.go +++ b/analysis.go @@ -26,36 +26,14 @@ import ( "golang.org/x/tools/go/analysis" ) -func NewAnalyzer(config *string) *analysis.Analyzer { - var goheaderOncer sync.Once - var goheader *Analyzer - +// NewAnalyzer creates new analyzer based on template and goheader values +func NewAnalyzer(templ string, vars map[string]Value) *analysis.Analyzer { + var goheader = New(WithTemplate(templ), WithValues(vars)) return &analysis.Analyzer{ Doc: "the_only_doc", URL: "https://github.com/denis-tingaikin/go-header", Name: "goheader", Run: func(p *analysis.Pass) (any, error) { - var err error - goheaderOncer.Do(func() { - var cfg Config - if err = cfg.Parse(*config); err != nil { - return - } - templ, err := cfg.GetTemplate() - if err != nil { - return - } - vars, err := cfg.GetValues() - if err != nil { - return - } - goheader = New(WithTemplate(templ), WithValues(vars)) - }) - - if err != nil { - return nil, err - } - var wg sync.WaitGroup var jobCh = make(chan *ast.File, len(p.Files)) @@ -146,3 +124,38 @@ func NewAnalyzer(config *string) *analysis.Analyzer { }, } } + +// NewAnalyzerFromConfig creates a new analysis.Analyzer from goheader config file +func NewAnalyzerFromConfig(config *string) *analysis.Analyzer { + var goheaderOncer sync.Once + var goheader *analysis.Analyzer + + return &analysis.Analyzer{ + Doc: "the_only_doc", + URL: "https://github.com/denis-tingaikin/go-header", + Name: "goheader", + Run: func(p *analysis.Pass) (any, error) { + var err error + goheaderOncer.Do(func() { + var cfg Config + if err = cfg.Parse(*config); err != nil { + return + } + templ, err := cfg.GetTemplate() + if err != nil { + return + } + vars, err := cfg.GetValues() + if err != nil { + return + } + goheader = NewAnalyzer(templ, vars) + }) + + if err != nil { + return nil, err + } + return goheader.Run(p) + }, + } +} diff --git a/cmd/go-header/main.go b/cmd/go-header/main.go index 940bb0f..4f45fa0 100644 --- a/cmd/go-header/main.go +++ b/cmd/go-header/main.go @@ -46,7 +46,7 @@ func main() { var configPath string flagSet.StringVar(&configPath, "config", defaultConfigPath, "Path to config file") - var analyser = goheader.NewAnalyzer(&configPath) + var analyser = goheader.NewAnalyzerFromConfig(&configPath) analyser.Flags = flagSet singlechecker.Main(analyser)