diff --git a/.golangci.example.yml b/.golangci.example.yml index a6be95a0..50e3d6ac 100644 --- a/.golangci.example.yml +++ b/.golangci.example.yml @@ -126,6 +126,9 @@ linters-settings: gocyclo: # minimal code complexity to report, 30 by default (but we recommend 10-20) min-complexity: 10 + godot: + # check all top-level comments, not only declarations + check-all: false godox: # report any comments starting with keywords, this is useful for TODO or FIXME comments that # might be left in the code accidentally and should be resolved before merging diff --git a/README.md b/README.md index 9a86b13f..0d959130 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,7 @@ gocognit: Computes and checks the cognitive complexity of functions [fast: true, goconst: Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false] gocritic: The most opinionated Go source code linter [fast: true, auto-fix: false] gocyclo: Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false] +godot: Check if comments end in a period [fast: true, auto-fix: false] godox: Tool for detection of FIXME, TODO and other comment keywords [fast: true, auto-fix: false] gofmt: Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true] goimports: Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true] @@ -488,6 +489,7 @@ golangci-lint help linters - [wsl](https://github.com/bombsimon/wsl) - Whitespace Linter - Forces you to use empty lines! - [goprintffuncname](https://github.com/jirfag/go-printf-func-name) - Checks that printf-like functions are named with `f` at the end - [gomnd](https://github.com/tommy-muehle/go-mnd) - An analyzer to detect magic numbers. +- [godot](https://github.com/tetafro/godot) - Check if comments end in a period ## Configuration @@ -736,6 +738,9 @@ linters-settings: gocyclo: # minimal code complexity to report, 30 by default (but we recommend 10-20) min-complexity: 10 + godot: + # check all top-level comments, not only declarations + check-all: false godox: # report any comments starting with keywords, this is useful for TODO or FIXME comments that # might be left in the code accidentally and should be resolved before merging @@ -1261,6 +1266,7 @@ Thanks to developers and authors of used linters: - [bombsimon](https://github.com/bombsimon) - [jirfag](https://github.com/jirfag) - [tommy-muehle](https://github.com/tommy-muehle) +- [tetafro](https://github.com/tetafro) ## Changelog diff --git a/go.mod b/go.mod index 5d8545ac..c58df072 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.6.1 github.com/stretchr/testify v1.5.1 + github.com/tetafro/godot v0.2.5 github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa github.com/ultraware/funlen v0.0.2 diff --git a/go.sum b/go.sum index c95fabc9..fa3ecc58 100644 --- a/go.sum +++ b/go.sum @@ -262,6 +262,8 @@ github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tetafro/godot v0.2.5 h1:7+EYJM/Z4gYZhBFdRrVm6JTj5ZLw/QI1j4RfEOXJviE= +github.com/tetafro/godot v0.2.5/go.mod h1:pT6/T8+h6//L/LwQcFc4C0xpfy1euZwzS1sHdrFCms0= github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e h1:RumXZ56IrCj4CL+g1b9OL/oH0QnsF976bC8xQFYUD5Q= github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= diff --git a/pkg/config/config.go b/pkg/config/config.go index a4c7e453..b0df8ecf 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -196,6 +196,7 @@ type LintersSettings struct { Godox GodoxSettings Dogsled DogsledSettings Gocognit GocognitSettings + Godot GodotSettings Custom map[string]CustomLinterSettings } @@ -273,6 +274,10 @@ type WSLSettings struct { ForceCaseTrailingWhitespaceLimit int `mapstructure:"force-case-trailing-whitespace"` } +type GodotSettings struct { + CheckAll bool `mapstructure:"check-all"` +} + //nolint:gomnd var defaultLintersSettings = LintersSettings{ Lll: LllSettings{ diff --git a/pkg/golinters/godot.go b/pkg/golinters/godot.go new file mode 100644 index 00000000..842ec97d --- /dev/null +++ b/pkg/golinters/godot.go @@ -0,0 +1,64 @@ +package golinters + +import ( + "sync" + + "github.com/tetafro/godot" + "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 godotName = "godot" + +func NewGodot() *goanalysis.Linter { + var mu sync.Mutex + var resIssues []goanalysis.Issue + + analyzer := &analysis.Analyzer{ + Name: godotName, + Doc: goanalysis.TheOnlyanalyzerDoc, + } + return goanalysis.NewLinter( + godotName, + "Check if comments end in a period", + []*analysis.Analyzer{analyzer}, + nil, + ).WithContextSetter(func(lintCtx *linter.Context) { + cfg := lintCtx.Cfg.LintersSettings.Godot + settings := godot.Settings{CheckAll: cfg.CheckAll} + + analyzer.Run = func(pass *analysis.Pass) (interface{}, error) { + var issues []godot.Message + for _, file := range pass.Files { + issues = append(issues, godot.Run(file, pass.Fset, settings)...) + } + + if len(issues) == 0 { + return nil, nil + } + + res := make([]goanalysis.Issue, len(issues)) + for k, i := range issues { + issue := result.Issue{ + Pos: i.Pos, + Text: i.Message, + FromLinter: godotName, + Replacement: &result.Replacement{}, + } + + res[k] = goanalysis.NewIssue(&issue, pass) + } + + mu.Lock() + resIssues = append(resIssues, res...) + mu.Unlock() + + return nil, nil + } + }).WithIssuesReporter(func(*linter.Context) []goanalysis.Issue { + return resIssues + }).WithLoadMode(goanalysis.LoadModeSyntax) +} diff --git a/pkg/lint/lintersdb/manager.go b/pkg/lint/lintersdb/manager.go index cf71c1f4..85f2cec7 100644 --- a/pkg/lint/lintersdb/manager.go +++ b/pkg/lint/lintersdb/manager.go @@ -247,6 +247,9 @@ func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config { linter.NewConfig(golinters.NewGoMND(m.cfg)). WithPresets(linter.PresetStyle). WithURL("https://github.com/tommy-muehle/go-mnd"), + linter.NewConfig(golinters.NewGodot()). + WithPresets(linter.PresetStyle). + WithURL("https://github.com/tetafro/godot"), } isLocalRun := os.Getenv("GOLANGCI_COM_RUN") == "" diff --git a/test/testdata/godot.go b/test/testdata/godot.go new file mode 100644 index 00000000..2412a048 --- /dev/null +++ b/test/testdata/godot.go @@ -0,0 +1,7 @@ +//args: -Egodot +package testdata + +// Godot checks top-level comments // ERROR "Top level comment should end in a period" +func Godot() { + // nothing to do here +} diff --git a/vendor/github.com/tetafro/godot/.gitignore b/vendor/github.com/tetafro/godot/.gitignore new file mode 100644 index 00000000..339f1705 --- /dev/null +++ b/vendor/github.com/tetafro/godot/.gitignore @@ -0,0 +1 @@ +/godot diff --git a/vendor/github.com/tetafro/godot/LICENSE b/vendor/github.com/tetafro/godot/LICENSE new file mode 100644 index 00000000..120c6d50 --- /dev/null +++ b/vendor/github.com/tetafro/godot/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Denis Krivak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/tetafro/godot/README.md b/vendor/github.com/tetafro/godot/README.md new file mode 100644 index 00000000..612f9edb --- /dev/null +++ b/vendor/github.com/tetafro/godot/README.md @@ -0,0 +1,40 @@ +# godot + +[![License](http://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://raw.githubusercontent.com/tetafro/godot/master/LICENSE) +[![Github CI](https://img.shields.io/github/workflow/status/tetafro/godot/Test)](https://github.com/tetafro/godot/actions?query=workflow%3ATest) +[![Go Report](https://goreportcard.com/badge/github.com/tetafro/godot)](https://goreportcard.com/report/github.com/tetafro/godot) +[![Codecov](https://codecov.io/gh/tetafro/godot/branch/master/graph/badge.svg)](https://codecov.io/gh/tetafro/godot) + +Linter that checks if all top-level comments contain a period at the +end of the last sentence if needed. + +[CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments#comment-sentences) quote: + +> Comments should begin with the name of the thing being described +> and end in a period + +## Install and run + +```sh +go get -u github.com/tetafro/godot/cmd/godot +godot ./myproject +``` + +## Examples + +Code + +```go +package math + +// Sum sums two integers +func Sum(a, b int) int { + return a + b // result +} +``` + +Output + +```sh +Top level comment should end in a period: math/math.go:3:1 +``` diff --git a/vendor/github.com/tetafro/godot/go.mod b/vendor/github.com/tetafro/godot/go.mod new file mode 100644 index 00000000..ae9467ba --- /dev/null +++ b/vendor/github.com/tetafro/godot/go.mod @@ -0,0 +1,3 @@ +module github.com/tetafro/godot + +go 1.13 diff --git a/vendor/github.com/tetafro/godot/godot.go b/vendor/github.com/tetafro/godot/godot.go new file mode 100644 index 00000000..f011c978 --- /dev/null +++ b/vendor/github.com/tetafro/godot/godot.go @@ -0,0 +1,130 @@ +// Package godot checks if all top-level comments contain a period at the +// end of the last sentence if needed. +package godot + +import ( + "go/ast" + "go/token" + "regexp" + "strings" +) + +// Message contains a message of linting error. +type Message struct { + Pos token.Position + Message string +} + +// Settings contains linter settings. +type Settings struct { + // Check all top-level comments, not only declarations + CheckAll bool +} + +var ( + // List of valid last characters. + lastChars = []string{".", "?", "!"} + + // Special tags in comments like "nolint" or "build". + tags = regexp.MustCompile("^[a-z]+:") + + // URL at the end of the line. + endURL = regexp.MustCompile(`[a-z]+://[^\s]+$`) +) + +// Run runs this linter on the provided code. +func Run(file *ast.File, fset *token.FileSet, settings Settings) []Message { + msgs := []Message{} + + // Check all top-level comments + if settings.CheckAll { + for _, group := range file.Comments { + if ok, msg := check(fset, group); !ok { + msgs = append(msgs, msg) + } + } + return msgs + } + + // Check only declaration comments + for _, decl := range file.Decls { + switch d := decl.(type) { + case *ast.GenDecl: + case *ast.FuncDecl: + if ok, msg := check(fset, d.Doc); !ok { + msgs = append(msgs, msg) + } + } + } + return msgs +} + +func check(fset *token.FileSet, group *ast.CommentGroup) (ok bool, msg Message) { + if group == nil || len(group.List) == 0 { + return true, Message{} + } + + // Check only top-level comments + if fset.Position(group.Pos()).Column > 1 { + return true, Message{} + } + + // Get last element from comment group - it can be either + // last (or single) line for "//"-comment, or multiline string + // for "/*"-comment + last := group.List[len(group.List)-1] + + line, ok := checkComment(last.Text) + if ok { + return true, Message{} + } + pos := fset.Position(last.Slash) + pos.Line += line + return false, Message{ + Pos: pos, + Message: "Top level comment should end in a period", + } +} + +func checkComment(comment string) (line int, ok bool) { + // Check last line of "//"-comment + if strings.HasPrefix(comment, "//") { + comment = strings.TrimPrefix(comment, "//") + return 0, checkLastChar(comment) + } + + // Check multiline "/*"-comment block + lines := strings.Split(comment, "\n") + var i int + for i = len(lines) - 1; i >= 0; i-- { + if s := strings.TrimSpace(lines[i]); s == "*/" || s == "" { + continue + } + break + } + comment = strings.TrimPrefix(lines[i], "/*") + comment = strings.TrimSuffix(comment, "*/") + return i, checkLastChar(comment) +} + +func checkLastChar(s string) bool { + // Don't check comments starting with space indentation - they may + // contain code examples, which shouldn't end with period + if strings.HasPrefix(s, " ") || strings.HasPrefix(s, "\t") { + return true + } + s = strings.TrimSpace(s) + if tags.MatchString(s) || endURL.MatchString(s) || strings.HasPrefix(s, "+build") { + return true + } + // Don't check empty lines + if s == "" { + return true + } + for _, ch := range lastChars { + if string(s[len(s)-1]) == ch { + return true + } + } + return false +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 35f2b807..fb64f94b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -174,6 +174,8 @@ github.com/stretchr/testify/mock github.com/stretchr/testify/require # github.com/subosito/gotenv v1.2.0 github.com/subosito/gotenv +# github.com/tetafro/godot v0.2.5 +github.com/tetafro/godot # github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e github.com/timakin/bodyclose/passes/bodyclose # github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa