From 01b566a646058772ea79bb91796503bfef00e55a Mon Sep 17 00:00:00 2001 From: Denis Tingaikin <49399980+denis-tingajkin@users.noreply.github.com> Date: Sun, 5 Jul 2020 02:03:37 +0700 Subject: [PATCH] Add go-header linter (#1181) * add go-header linter * apply review comments: add goheader example into .golangci.example.yml * apply review comments: correctly handle multiline comments --- .golangci.example.yml | 27 ++++++++++ go.mod | 1 + go.sum | 2 + pkg/config/config.go | 7 +++ pkg/golinters/goheader.go | 78 +++++++++++++++++++++++++++++ pkg/lint/lintersdb/manager.go | 4 ++ test/linters_test.go | 11 +++- test/testdata/configs/go-header.yml | 6 +++ test/testdata/go-header.go | 4 ++ 9 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 pkg/golinters/goheader.go create mode 100644 test/testdata/configs/go-header.yml create mode 100644 test/testdata/go-header.go diff --git a/.golangci.example.yml b/.golangci.example.yml index 32f23a9f1aaa..28e2715d897f 100644 --- a/.golangci.example.yml +++ b/.golangci.example.yml @@ -157,6 +157,33 @@ linters-settings: gofmt: # simplify code: gofmt with `-s` option, true by default simplify: true + goheader: + values: + const: + # define here const type values in format k:v, for example: + # YEAR: 2020 + # COMPANY: MY COMPANY + regexp: + # define here regexp type values, for example + # AUTHOR: .*@mycompany\.com + template: + # put here copyright header template for source code files, for example: + # {{ AUTHOR }} {{ COMPANY }} {{ YEAR }} + # 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. + template-path: + # also as alternative of directive 'template' you may put the path to file with the template source goimports: # put imports beginning with prefix after 3rd-party packages; # it's a comma-separated list of prefixes diff --git a/go.mod b/go.mod index 403ca39d0ef3..3511ba32fc08 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Djarvur/go-err113 v0.0.0-20200511133814-5174e21577d5 github.com/OpenPeeDeeP/depguard v1.0.1 github.com/bombsimon/wsl/v3 v3.1.0 + github.com/denis-tingajkin/go-header v0.3.1 github.com/fatih/color v1.9.0 github.com/go-critic/go-critic v0.4.3 github.com/go-lintpack/lintpack v0.5.2 diff --git a/go.sum b/go.sum index 279fc61d2b88..ac108b2efdad 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 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/denis-tingajkin/go-header v0.3.1 h1:ymEpSiFjeItCy1FOP+x0M2KdCELdEAHUsNa8F+hHc6w= +github.com/denis-tingajkin/go-header v0.3.1/go.mod h1:sq/2IxMhaZX+RRcgHfCRx/m0M5na0fBt4/CRe7Lrji0= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= diff --git a/pkg/config/config.go b/pkg/config/config.go index 4c9c447c2e0a..3afd40406b08 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -239,6 +239,7 @@ type LintersSettings struct { Dogsled DogsledSettings Gocognit GocognitSettings Godot GodotSettings + Goheader GoHeaderSettings Testpackage TestpackageSettings Nestif NestifSettings NoLintLint NoLintLintSettings @@ -247,6 +248,12 @@ type LintersSettings struct { Custom map[string]CustomLinterSettings } +type GoHeaderSettings struct { + Values map[string]map[string]string `mapstructure:"values"` + Template string `mapstructure:"template"` + TemplatePath string `mapstructure:"template-path"` +} + type GovetSettings struct { CheckShadowing bool `mapstructure:"check-shadowing"` Settings map[string]map[string]interface{} diff --git a/pkg/golinters/goheader.go b/pkg/golinters/goheader.go new file mode 100644 index 000000000000..152069fa00b0 --- /dev/null +++ b/pkg/golinters/goheader.go @@ -0,0 +1,78 @@ +package golinters + +import ( + "go/token" + "sync" + + goheader "github.com/denis-tingajkin/go-header" + "golang.org/x/tools/go/analysis" + + "github.com/golangci/golangci-lint/pkg/golinters/goanalysis" + "github.com/golangci/golangci-lint/pkg/lint/linter" + "github.com/golangci/golangci-lint/pkg/result" +) + +const goHeaderName = "goheader" + +func NewGoHeader() *goanalysis.Linter { + var mu sync.Mutex + var issues []goanalysis.Issue + + analyzer := &analysis.Analyzer{ + Name: goHeaderName, + Doc: goanalysis.TheOnlyanalyzerDoc, + } + return goanalysis.NewLinter( + goHeaderName, + "Checks is file header matches to pattern", + []*analysis.Analyzer{analyzer}, + nil, + ).WithContextSetter(func(lintCtx *linter.Context) { + cfg := lintCtx.Cfg.LintersSettings.Goheader + c := &goheader.Configuration{ + Values: cfg.Values, + Template: cfg.Template, + TemplatePath: cfg.TemplatePath, + } + analyzer.Run = func(pass *analysis.Pass) (interface{}, error) { + if c.TemplatePath == "" && c.Template == "" { + // User did not pass template, so then do not run go-header linter + return nil, nil + } + template, err := c.GetTemplate() + if err != nil { + return nil, err + } + values, err := c.GetValues() + if err != nil { + return nil, err + } + a := goheader.New(goheader.WithTemplate(template), goheader.WithValues(values)) + var res []goanalysis.Issue + for _, file := range pass.Files { + i := a.Analyze(file) + issue := result.Issue{ + Pos: token.Position{ + Line: i.Location().Line + 1, + Column: i.Location().Position, + Filename: pass.Fset.Position(file.Pos()).Filename, + }, + Text: i.Message(), + FromLinter: goHeaderName, + } + res = append(res, goanalysis.NewIssue(&issue, pass)) + } + if len(res) == 0 { + return nil, nil + } + + mu.Lock() + issues = append(issues, res...) + mu.Unlock() + + return nil, nil + } + }).WithIssuesReporter(func(*linter.Context) []goanalysis.Issue { + return issues + }).WithLoadMode(goanalysis.LoadModeSyntax) +} diff --git a/pkg/lint/lintersdb/manager.go b/pkg/lint/lintersdb/manager.go index b310ad7a3c06..d988fb067c9f 100644 --- a/pkg/lint/lintersdb/manager.go +++ b/pkg/lint/lintersdb/manager.go @@ -202,6 +202,10 @@ func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config { WithPresets(linter.PresetFormatting). WithAutoFix(). WithURL("https://godoc.org/golang.org/x/tools/cmd/goimports"), + linter.NewConfig(golinters.NewGoHeader()). + WithPresets(linter.PresetStyle). + WithLoadForGoAnalysis(). + WithURL("https://github.com/denis-tingajkin/go-header"), linter.NewConfig(golinters.NewMaligned()). WithLoadForGoAnalysis(). WithPresets(linter.PresetPerformance). diff --git a/test/linters_test.go b/test/linters_test.go index bc8fbc5b7590..ebe58ae8e5fd 100644 --- a/test/linters_test.go +++ b/test/linters_test.go @@ -173,6 +173,12 @@ func buildConfigFromShortRepr(t *testing.T, repr string, config map[string]inter lastObj[keyParts[len(keyParts)-1]] = kv[1] } +func skipMultilineComment(scanner *bufio.Scanner) { + for line := scanner.Text(); !strings.Contains(line, "*/") && scanner.Scan(); { + line = scanner.Text() + } +} + func extractRunContextFromComments(t *testing.T, sourcePath string) *runContext { f, err := os.Open(sourcePath) assert.NoError(t, err) @@ -183,10 +189,13 @@ func extractRunContextFromComments(t *testing.T, sourcePath string) *runContext scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() + if strings.HasPrefix(line, "/*") { + skipMultilineComment(scanner) + continue + } if !strings.HasPrefix(line, "//") { return rc } - line = strings.TrimPrefix(line, "//") if strings.HasPrefix(line, "args: ") { assert.Nil(t, rc.args) diff --git a/test/testdata/configs/go-header.yml b/test/testdata/configs/go-header.yml new file mode 100644 index 000000000000..c20ed76c196f --- /dev/null +++ b/test/testdata/configs/go-header.yml @@ -0,0 +1,6 @@ +linters-settings: + goheader: + template: MY {{title}} + values: + const: + title: TITLE. diff --git a/test/testdata/go-header.go b/test/testdata/go-header.go new file mode 100644 index 000000000000..6714a867de57 --- /dev/null +++ b/test/testdata/go-header.go @@ -0,0 +1,4 @@ +/*MY TITLE!*/ // ERROR "Expected:TITLE., Actual: TITLE!" +//args: -Egoheader +//config_path: testdata/configs/go-header.yml +package testdata