From fcc6d71be2c3ce7d9305c04fc2e87af554571bac Mon Sep 17 00:00:00 2001 From: Dmitry Vyukov Date: Mon, 22 Feb 2021 20:37:25 +0100 Subject: go.mod: update golangci-lint to v1.37 --- vendor/github.com/tetafro/godot/.gitignore | 2 + vendor/github.com/tetafro/godot/Makefile | 4 + vendor/github.com/tetafro/godot/README.md | 32 +++- vendor/github.com/tetafro/godot/checks.go | 260 +++++++++++++++++++++++++++ vendor/github.com/tetafro/godot/config.yaml | 14 ++ vendor/github.com/tetafro/godot/getters.go | 260 +++++++++++++++++++++++++++ vendor/github.com/tetafro/godot/go.mod | 4 +- vendor/github.com/tetafro/godot/go.sum | 4 + vendor/github.com/tetafro/godot/godot.go | 264 ++++------------------------ vendor/github.com/tetafro/godot/settings.go | 29 +++ 10 files changed, 641 insertions(+), 232 deletions(-) create mode 100644 vendor/github.com/tetafro/godot/checks.go create mode 100644 vendor/github.com/tetafro/godot/config.yaml create mode 100644 vendor/github.com/tetafro/godot/getters.go create mode 100644 vendor/github.com/tetafro/godot/go.sum create mode 100644 vendor/github.com/tetafro/godot/settings.go (limited to 'vendor/github.com/tetafro') diff --git a/vendor/github.com/tetafro/godot/.gitignore b/vendor/github.com/tetafro/godot/.gitignore index 2b87e3915..db77fd15d 100644 --- a/vendor/github.com/tetafro/godot/.gitignore +++ b/vendor/github.com/tetafro/godot/.gitignore @@ -1,2 +1,4 @@ /dist/ +/vendor/ /godot +/profile.out diff --git a/vendor/github.com/tetafro/godot/Makefile b/vendor/github.com/tetafro/godot/Makefile index 98a691d78..f167051e8 100644 --- a/vendor/github.com/tetafro/godot/Makefile +++ b/vendor/github.com/tetafro/godot/Makefile @@ -1,3 +1,7 @@ +.PHONY: dep +dep: + go mod tidy && go mod verify + .PHONY: test test: go test ./... diff --git a/vendor/github.com/tetafro/godot/README.md b/vendor/github.com/tetafro/godot/README.md index 864767e3c..902678f54 100644 --- a/vendor/github.com/tetafro/godot/README.md +++ b/vendor/github.com/tetafro/godot/README.md @@ -26,6 +26,28 @@ go get -u github.com/tetafro/godot/cmd/godot or download binary from [releases page](https://github.com/tetafro/godot/releases). +## Config + +You can specify options using config file. If no config provided the following +defaults are used: + +```yaml +# Which comments to check: +# declarations - for top level declaration comments (default); +# toplevel - for top level comments; +# all - for all comments. +scope: toplevel + +# List pf regexps for excluding particular comment lines from check. +exclude: + +# Check periods at the end of sentences. +period: true + +# Check that first letter of each sentence is capital. +capital: false +``` + ## Run ```sh @@ -39,7 +61,9 @@ godot -f ./myproject # fix issues and print the result godot -w ./myproject # fix issues and replace the original file ``` -## Examples +See all flags with `godot -h`. + +## Example Code @@ -55,9 +79,5 @@ func Sum(a, b int) int { Output ```sh -Top level comment should end in a period: math/math.go:3:1 +Comment should end in a period: math/math.go:3:1 ``` - -See more examples in test files: -- [for default mode](testdata/default/in/main.go) -- [for using --all flag](testdata/checkall/in/main.go) diff --git a/vendor/github.com/tetafro/godot/checks.go b/vendor/github.com/tetafro/godot/checks.go new file mode 100644 index 000000000..0e66add1d --- /dev/null +++ b/vendor/github.com/tetafro/godot/checks.go @@ -0,0 +1,260 @@ +package godot + +import ( + "go/token" + "regexp" + "strings" + "unicode" +) + +// Error messages. +const ( + noPeriodMessage = "Comment should end in a period" + noCapitalMessage = "Sentence should start with a capital letter" +) + +var ( + // List of valid sentence ending. + // A sentence can be inside parenthesis, and therefore ends with parenthesis. + lastChars = []string{".", "?", "!", ".)", "?)", "!)", specialReplacer} + + // Special tags in comments like "// nolint:", or "// +k8s:". + tags = regexp.MustCompile(`^\+?[a-z0-9]+:`) + + // Special hashtags in comments like "// #nosec". + hashtags = regexp.MustCompile(`^#[a-z]+($|\s)`) + + // URL at the end of the line. + endURL = regexp.MustCompile(`[a-z]+://[^\s]+$`) +) + +// checkComments checks every comment accordings to the rules from +// `settings` argument. +func checkComments(comments []comment, settings Settings) []Issue { + var issues []Issue // nolint: prealloc + for _, c := range comments { + if settings.Period { + if iss := checkCommentForPeriod(c); iss != nil { + issues = append(issues, *iss) + } + } + if settings.Capital { + if iss := checkCommentForCapital(c); len(iss) > 0 { + issues = append(issues, iss...) + } + } + } + return issues +} + +// checkCommentForPeriod checks that the last sentense of the comment ends +// in a period. +func checkCommentForPeriod(c comment) *Issue { + pos, ok := checkPeriod(c.text) + if ok { + return nil + } + + // Shift position by the length of comment's special symbols: /* or // + isBlock := strings.HasPrefix(c.lines[0], "/*") + if (isBlock && pos.line == 1) || !isBlock { + pos.column += 2 + } + + iss := Issue{ + Pos: token.Position{ + Filename: c.start.Filename, + Offset: c.start.Offset, + Line: pos.line + c.start.Line - 1, + Column: pos.column + c.start.Column - 1, + }, + Message: noPeriodMessage, + } + + // Make a replacement. Use `pos.line` to get an original line from + // attached lines. Use `iss.Pos.Column` because it's a position in + // the original line. + original := []rune(c.lines[pos.line-1]) + iss.Replacement = string(original[:iss.Pos.Column-1]) + "." + + string(original[iss.Pos.Column-1:]) + + // Save replacement to raw lines to be able to combine it with + // further replacements + c.lines[pos.line-1] = iss.Replacement + + return &iss +} + +// checkCommentForCapital checks that the each sentense of the comment starts with +// a capital letter. +// nolint: unparam +func checkCommentForCapital(c comment) []Issue { + pp := checkCapital(c.text, c.decl) + if len(pp) == 0 { + return nil + } + + issues := make([]Issue, len(pp)) + for i, pos := range pp { + // Shift position by the length of comment's special symbols: /* or // + isBlock := strings.HasPrefix(c.lines[0], "/*") + if (isBlock && pos.line == 1) || !isBlock { + pos.column += 2 + } + + iss := Issue{ + Pos: token.Position{ + Filename: c.start.Filename, + Offset: c.start.Offset, + Line: pos.line + c.start.Line - 1, + Column: pos.column + c.start.Column - 1, + }, + Message: noCapitalMessage, + } + + // Make a replacement. Use `pos.line` to get an original line from + // attached lines. Use `iss.Pos.Column` because it's a position in + // the original line. + rep := []rune(c.lines[pos.line-1]) + rep[iss.Pos.Column-1] = unicode.ToTitle(rep[iss.Pos.Column-1]) + iss.Replacement = string(rep) + + // Save replacement to raw lines to be able to combine it with + // further replacements + c.lines[pos.line-1] = iss.Replacement + + issues[i] = iss + } + + return issues +} + +// checkPeriod checks that the last sentense of the text ends in a period. +// NOTE: Returned position is a position inside given text, not in the +// original file. +func checkPeriod(comment string) (pos position, ok bool) { + // Check last non-empty line + var found bool + var line string + lines := strings.Split(comment, "\n") + for i := len(lines) - 1; i >= 0; i-- { + line = strings.TrimRightFunc(lines[i], unicode.IsSpace) + if line == "" { + continue + } + found = true + pos.line = i + 1 + break + } + // All lines are empty + if !found { + return position{}, true + } + // Correct line + if hasSuffix(line, lastChars) { + return position{}, true + } + + pos.column = len([]rune(line)) + 1 + return pos, false +} + +// checkCapital checks that the each sentense of the text starts with +// a capital letter. +// NOTE: First letter is not checked in declaration comments, because they +// can describe unexported functions, which start from small letter. +func checkCapital(comment string, skipFirst bool) (pp []position) { + // List of states during the scan: `empty` - nothing special, + // `endChar` - found one of sentence ending chars (.!?), + // `endOfSentence` - found `endChar`, and then space or newline. + const empty, endChar, endOfSentence = 1, 2, 3 + + pos := position{line: 1} + state := endOfSentence + if skipFirst { + state = empty + } + for _, r := range comment { + s := string(r) + + pos.column++ + if s == "\n" { + pos.line++ + pos.column = 0 + if state == endChar { + state = endOfSentence + } + continue + } + if s == "." || s == "!" || s == "?" { + state = endChar + continue + } + if s == ")" && state == endChar { + continue + } + if s == " " { + if state == endChar { + state = endOfSentence + } + continue + } + if state == endOfSentence && unicode.IsLower(r) { + pp = append(pp, position{line: pos.line, column: pos.column}) + } + state = empty + } + return pp +} + +// isSpecialBlock checks that given block of comment lines is special and +// shouldn't be checked as a regular sentence. +func isSpecialBlock(comment string) bool { + // Skip cgo code blocks + // TODO: Find a better way to detect cgo code + if strings.HasPrefix(comment, "/*") && (strings.Contains(comment, "#include") || + strings.Contains(comment, "#define")) { + return true + } + return false +} + +// isSpecialBlock checks that given comment line is special and +// shouldn't be checked as a regular sentence. +func isSpecialLine(comment string) bool { + // Skip cgo export tags: https://golang.org/cmd/cgo/#hdr-C_references_to_Go + if strings.HasPrefix(comment, "//export ") { + return true + } + + comment = strings.TrimPrefix(comment, "//") + comment = strings.TrimPrefix(comment, "/*") + + // Don't check comments starting with space indentation - they may + // contain code examples, which shouldn't end with period + if strings.HasPrefix(comment, " ") || + strings.HasPrefix(comment, " \t") || + strings.HasPrefix(comment, "\t") { + return true + } + + // Skip tags and URLs + comment = strings.TrimSpace(comment) + if tags.MatchString(comment) || + hashtags.MatchString(comment) || + endURL.MatchString(comment) || + strings.HasPrefix(comment, "+build") { + return true + } + + return false +} + +func hasSuffix(s string, suffixes []string) bool { + for _, suffix := range suffixes { + if strings.HasSuffix(s, suffix) { + return true + } + } + return false +} diff --git a/vendor/github.com/tetafro/godot/config.yaml b/vendor/github.com/tetafro/godot/config.yaml new file mode 100644 index 000000000..c459ca323 --- /dev/null +++ b/vendor/github.com/tetafro/godot/config.yaml @@ -0,0 +1,14 @@ +# Which comments to check: +# declarations - for top level declaration comments (default); +# toplevel - for top level comments; +# all - for all comments. +scope: toplevel + +# List pf regexps for excluding particular comment lines from check. +exclude: + +# Check periods at the end of sentences. +period: true + +# Check that first letter of each sentence is capital. +capital: false diff --git a/vendor/github.com/tetafro/godot/getters.go b/vendor/github.com/tetafro/godot/getters.go new file mode 100644 index 000000000..02b8acec7 --- /dev/null +++ b/vendor/github.com/tetafro/godot/getters.go @@ -0,0 +1,260 @@ +package godot + +import ( + "errors" + "fmt" + "go/ast" + "go/token" + "io/ioutil" + "regexp" + "strings" +) + +var errEmptyInput = errors.New("empty input") + +// specialReplacer is a replacer for some types of special lines in comments, +// which shouldn't be checked. For example, if comment ends with a block of +// code it should not necessarily have a period at the end. +const specialReplacer = "" + +type parsedFile struct { + fset *token.FileSet + file *ast.File + lines []string +} + +func newParsedFile(file *ast.File, fset *token.FileSet) (*parsedFile, error) { + if file == nil || fset == nil || len(file.Comments) == 0 { + return nil, errEmptyInput + } + + pf := parsedFile{ + fset: fset, + file: file, + } + + var err error + + // Read original file. This is necessary for making a replacements for + // inline comments. I couldn't find a better way to get original line + // with code and comment without reading the file. Function `Format` + // from "go/format" won't help here if the original file is not gofmt-ed. + pf.lines, err = readFile(file, fset) + if err != nil { + return nil, fmt.Errorf("read file: %v", err) + } + + // Check consistency to avoid checking slice indexes in each function + lastComment := pf.file.Comments[len(pf.file.Comments)-1] + if p := pf.fset.Position(lastComment.End()); len(pf.lines) < p.Line { + return nil, fmt.Errorf("inconsistence between file and AST: %s", p.Filename) + } + + return &pf, nil +} + +// getComments extracts comments from a file. +func (pf *parsedFile) getComments(scope Scope, exclude []*regexp.Regexp) []comment { + var comments []comment + decl := pf.getDeclarationComments(exclude) + switch scope { + case AllScope: + // All comments + comments = pf.getAllComments(exclude) + case TopLevelScope: + // All top level comments and comments from the inside + // of top level blocks + comments = append( + pf.getBlockComments(exclude), + pf.getTopLevelComments(exclude)..., + ) + default: + // Top level declaration comments and comments from the inside + // of top level blocks + comments = append(pf.getBlockComments(exclude), decl...) + } + + // Set `decl` flag + setDecl(comments, decl) + + return comments +} + +// getBlockComments gets comments from the inside of top level blocks: +// var (...), const (...). +func (pf *parsedFile) getBlockComments(exclude []*regexp.Regexp) []comment { + var comments []comment + for _, decl := range pf.file.Decls { + d, ok := decl.(*ast.GenDecl) + if !ok { + continue + } + // No parenthesis == no block + if d.Lparen == 0 { + continue + } + for _, c := range pf.file.Comments { + if c == nil || len(c.List) == 0 { + continue + } + // Skip comments outside this block + if d.Lparen > c.Pos() || c.Pos() > d.Rparen { + continue + } + // Skip comments that are not top-level for this block + // (the block itself is top level, so comments inside this block + // would be on column 2) + // nolint: gomnd + if pf.fset.Position(c.Pos()).Column != 2 { + continue + } + firstLine := pf.fset.Position(c.Pos()).Line + lastLine := pf.fset.Position(c.End()).Line + comments = append(comments, comment{ + lines: pf.lines[firstLine-1 : lastLine], + text: getText(c, exclude), + start: pf.fset.Position(c.List[0].Slash), + }) + } + } + return comments +} + +// getTopLevelComments gets all top level comments. +func (pf *parsedFile) getTopLevelComments(exclude []*regexp.Regexp) []comment { + var comments []comment // nolint: prealloc + for _, c := range pf.file.Comments { + if c == nil || len(c.List) == 0 { + continue + } + if pf.fset.Position(c.Pos()).Column != 1 { + continue + } + firstLine := pf.fset.Position(c.Pos()).Line + lastLine := pf.fset.Position(c.End()).Line + comments = append(comments, comment{ + lines: pf.lines[firstLine-1 : lastLine], + text: getText(c, exclude), + start: pf.fset.Position(c.List[0].Slash), + }) + } + return comments +} + +// getDeclarationComments gets top level declaration comments. +func (pf *parsedFile) getDeclarationComments(exclude []*regexp.Regexp) []comment { + var comments []comment // nolint: prealloc + for _, decl := range pf.file.Decls { + var cg *ast.CommentGroup + switch d := decl.(type) { + case *ast.GenDecl: + cg = d.Doc + case *ast.FuncDecl: + cg = d.Doc + } + + if cg == nil || len(cg.List) == 0 { + continue + } + + firstLine := pf.fset.Position(cg.Pos()).Line + lastLine := pf.fset.Position(cg.End()).Line + comments = append(comments, comment{ + lines: pf.lines[firstLine-1 : lastLine], + text: getText(cg, exclude), + start: pf.fset.Position(cg.List[0].Slash), + }) + } + return comments +} + +// getAllComments gets every single comment from the file. +func (pf *parsedFile) getAllComments(exclude []*regexp.Regexp) []comment { + var comments []comment //nolint: prealloc + for _, c := range pf.file.Comments { + if c == nil || len(c.List) == 0 { + continue + } + firstLine := pf.fset.Position(c.Pos()).Line + lastLine := pf.fset.Position(c.End()).Line + comments = append(comments, comment{ + lines: pf.lines[firstLine-1 : lastLine], + start: pf.fset.Position(c.List[0].Slash), + text: getText(c, exclude), + }) + } + return comments +} + +// getText extracts text from comment. If comment is a special block +// (e.g., CGO code), a block of empty lines is returned. If comment contains +// special lines (e.g., tags or indented code examples), they are replaced +// with `specialReplacer` to skip checks for it. +// The result can be multiline. +func getText(comment *ast.CommentGroup, exclude []*regexp.Regexp) (s string) { + if len(comment.List) == 1 && + strings.HasPrefix(comment.List[0].Text, "/*") && + isSpecialBlock(comment.List[0].Text) { + return "" + } + + for _, c := range comment.List { + text := c.Text + isBlock := false + if strings.HasPrefix(c.Text, "/*") { + isBlock = true + text = strings.TrimPrefix(text, "/*") + text = strings.TrimSuffix(text, "*/") + } + for _, line := range strings.Split(text, "\n") { + if isSpecialLine(line) { + s += specialReplacer + "\n" + continue + } + if !isBlock { + line = strings.TrimPrefix(line, "//") + } + if matchAny(line, exclude) { + s += specialReplacer + "\n" + continue + } + s += line + "\n" + } + } + if len(s) == 0 { + return "" + } + return s[:len(s)-1] // trim last "\n" +} + +// readFile reads file and returns it's lines as strings. +func readFile(file *ast.File, fset *token.FileSet) ([]string, error) { + fname := fset.File(file.Package) + f, err := ioutil.ReadFile(fname.Name()) + if err != nil { + return nil, err + } + return strings.Split(string(f), "\n"), nil +} + +// setDecl sets `decl` flag to comments which are declaration comments. +func setDecl(comments, decl []comment) { + for _, d := range decl { + for i, c := range comments { + if d.start == c.start { + comments[i].decl = true + break + } + } + } +} + +// matchAny checks if string matches any of given regexps. +func matchAny(s string, rr []*regexp.Regexp) bool { + for _, re := range rr { + if re.MatchString(s) { + return true + } + } + return false +} diff --git a/vendor/github.com/tetafro/godot/go.mod b/vendor/github.com/tetafro/godot/go.mod index d86531c42..d0fa675b6 100644 --- a/vendor/github.com/tetafro/godot/go.mod +++ b/vendor/github.com/tetafro/godot/go.mod @@ -1,3 +1,5 @@ module github.com/tetafro/godot -go 1.14 +go 1.15 + +require gopkg.in/yaml.v2 v2.4.0 diff --git a/vendor/github.com/tetafro/godot/go.sum b/vendor/github.com/tetafro/godot/go.sum new file mode 100644 index 000000000..dd0bc19f1 --- /dev/null +++ b/vendor/github.com/tetafro/godot/go.sum @@ -0,0 +1,4 @@ +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.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/vendor/github.com/tetafro/godot/godot.go b/vendor/github.com/tetafro/godot/godot.go index 81211d725..526a5f7d1 100644 --- a/vendor/github.com/tetafro/godot/godot.go +++ b/vendor/github.com/tetafro/godot/godot.go @@ -1,5 +1,5 @@ -// Package godot checks if all top-level comments contain a period at the -// end of the last sentence if needed. +// Package godot checks if comments contain a period at the end of the last +// sentence if needed. package godot import ( @@ -13,67 +13,60 @@ import ( "strings" ) -const ( - // noPeriodMessage is an error message to return. - noPeriodMessage = "Top level comment should end in a period" - // topLevelColumn is just the most left column of the file. - topLevelColumn = 1 - // topLevelGroupColumn is the most left column inside a group declaration - // on the top level. - topLevelGroupColumn = 2 -) +// NOTE: Line and column indexes are 1-based. -// Settings contains linter settings. -type Settings struct { - // Check all top-level comments, not only declarations - CheckAll bool -} +// NOTE: Errors `invalid line number inside comment...` should never happen. +// Their goal is to prevent panic, if there's a bug with array indexes. -// Issue contains a description of linting error and a possible replacement. +// Issue contains a description of linting error and a recommended replacement. type Issue struct { Pos token.Position Message string Replacement string } -// position is an position inside a comment (might be multiline comment). +// position is a position inside a comment (might be multiline comment). type position struct { line int column int } -var ( - // List of valid last characters. - lastChars = []string{".", "?", "!"} - - // Special tags in comments like "// nolint:", or "// +k8s:". - tags = regexp.MustCompile(`^\+?[a-z0-9]+:`) - - // Special hashtags in comments like "#nosec". - hashtags = regexp.MustCompile("^#[a-z]+ ") - - // URL at the end of the line. - endURL = regexp.MustCompile(`[a-z]+://[^\s]+$`) -) +// comment is an internal representation of AST comment entity with additional +// data attached. The latter is used for creating a full replacement for +// the line with issues. +type comment struct { + lines []string // unmodified lines from file + text string // concatenated `lines` with special parts excluded + start token.Position // position of the first symbol in comment + decl bool // whether comment is a special one (should not be checked) +} // Run runs this linter on the provided code. -func Run(file *ast.File, fset *token.FileSet, settings Settings) []Issue { - issues := checkBlocks(file, fset) +func Run(file *ast.File, fset *token.FileSet, settings Settings) ([]Issue, error) { + pf, err := newParsedFile(file, fset) + if err == errEmptyInput { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("parse input file: %v", err) + } - // Check all top-level comments - if settings.CheckAll { - issues = append(issues, checkTopLevel(file, fset)...) - sortIssues(issues) - return issues + exclude := make([]*regexp.Regexp, len(settings.Exclude)) + for i := 0; i < len(settings.Exclude); i++ { + exclude[i], err = regexp.Compile(settings.Exclude[i]) + if err != nil { + return nil, fmt.Errorf("invalid regexp: %v", err) + } } - // Check only declaration comments - issues = append(issues, checkDeclarations(file, fset)...) + comments := pf.getComments(settings.Scope, exclude) + issues := checkComments(comments, settings) sortIssues(issues) - return issues + + return issues, nil } -// Fix fixes all issues and return new version of file content. +// Fix fixes all issues and returns new version of file content. func Fix(path string, file *ast.File, fset *token.FileSet, settings Settings) ([]byte, error) { // Read file content, err := ioutil.ReadFile(path) // nolint: gosec @@ -84,7 +77,10 @@ func Fix(path string, file *ast.File, fset *token.FileSet, settings Settings) ([ return nil, nil } - issues := Run(file, fset, settings) + issues, err := Run(file, fset, settings) + if err != nil { + return nil, fmt.Errorf("run linter: %v", err) + } // slice -> map m := map[int]Issue{} @@ -137,185 +133,3 @@ func sortIssues(iss []Issue) { return iss[i].Pos.Column < iss[j].Pos.Column }) } - -// checkTopLevel checks all top-level comments. -func checkTopLevel(file *ast.File, fset *token.FileSet) (issues []Issue) { - for _, group := range file.Comments { - if iss, ok := check(fset, group, topLevelColumn); !ok { - issues = append(issues, iss) - } - } - return issues -} - -// checkDeclarations checks top level declaration comments. -func checkDeclarations(file *ast.File, fset *token.FileSet) (issues []Issue) { - for _, decl := range file.Decls { - switch d := decl.(type) { - case *ast.GenDecl: - if iss, ok := check(fset, d.Doc, topLevelColumn); !ok { - issues = append(issues, iss) - } - case *ast.FuncDecl: - if iss, ok := check(fset, d.Doc, topLevelColumn); !ok { - issues = append(issues, iss) - } - } - } - return issues -} - -// checkBlocks checks comments inside top level blocks (var (...), const (...), etc). -func checkBlocks(file *ast.File, fset *token.FileSet) (issues []Issue) { - for _, decl := range file.Decls { - d, ok := decl.(*ast.GenDecl) - if !ok { - continue - } - // No parenthesis == no block - if d.Lparen == 0 { - continue - } - for _, group := range file.Comments { - // Skip comments outside this block - if d.Lparen > group.Pos() || group.Pos() > d.Rparen { - continue - } - // Skip comments that are not top-level for this block - if fset.Position(group.Pos()).Column != topLevelGroupColumn { - continue - } - if iss, ok := check(fset, group, topLevelGroupColumn); !ok { - issues = append(issues, iss) - } - } - } - return issues -} - -func check(fset *token.FileSet, group *ast.CommentGroup, level int) (iss Issue, ok bool) { - if group == nil || len(group.List) == 0 { - return Issue{}, true - } - - // Check only top-level comments - if fset.Position(group.Pos()).Column > level { - return Issue{}, true - } - - // 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] - - p, ok := checkComment(last.Text) - if ok { - return Issue{}, true - } - - pos := fset.Position(last.Slash) - pos.Line += p.line - pos.Column = p.column + level - 1 - - indent := strings.Repeat("\t", level-1) - - iss = Issue{ - Pos: pos, - Message: noPeriodMessage, - Replacement: indent + makeReplacement(last.Text, p), - } - return iss, false -} - -func checkComment(comment string) (pos position, ok bool) { - // Check last line of "//"-comment - if strings.HasPrefix(comment, "//") { - pos.column = len([]rune(comment)) // runes for non-latin chars - comment = strings.TrimPrefix(comment, "//") - if checkLastChar(comment) { - return position{}, true - } - return pos, false - } - - // Skip cgo code blocks - // TODO: Find a better way to detect cgo code - if strings.Contains(comment, "#include") || strings.Contains(comment, "#define") { - return position{}, true - } - - // Check last non-empty line in 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 - } - pos.line = i - comment = lines[i] - comment = strings.TrimSuffix(comment, "*/") - comment = strings.TrimRight(comment, " ") - // Get position of the last non-space char in comment line, use runes - // in case of non-latin chars - pos.column = len([]rune(comment)) - comment = strings.TrimPrefix(comment, "/*") - - if checkLastChar(comment) { - return position{}, true - } - return pos, false -} - -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") || strings.HasPrefix(s, "\t") { - return true - } - // Skip cgo export tags: https://golang.org/cmd/cgo/#hdr-C_references_to_Go - if strings.HasPrefix(s, "export") { - return true - } - s = strings.TrimSpace(s) - if tags.MatchString(s) || - hashtags.MatchString(s) || - endURL.MatchString(s) || - strings.HasPrefix(s, "+build") { - return true - } - // Don't check empty lines - if s == "" { - return true - } - // Trim parenthesis for cases when the whole sentence is inside parenthesis - s = strings.TrimRight(s, ")") - for _, ch := range lastChars { - if string(s[len(s)-1]) == ch { - return true - } - } - return false -} - -// makeReplacement basically just inserts a period into comment on -// the given position. -func makeReplacement(s string, pos position) string { - lines := strings.Split(s, "\n") - if len(lines) < pos.line { - // This should never happen - return s - } - line := []rune(lines[pos.line]) - if len(line) < pos.column { - // This should never happen - return s - } - // Insert a period - newline := append( - line[:pos.column], - append([]rune{'.'}, line[pos.column:]...)..., - ) - return string(newline) -} diff --git a/vendor/github.com/tetafro/godot/settings.go b/vendor/github.com/tetafro/godot/settings.go new file mode 100644 index 000000000..b71bf5d58 --- /dev/null +++ b/vendor/github.com/tetafro/godot/settings.go @@ -0,0 +1,29 @@ +package godot + +// Settings contains linter settings. +type Settings struct { + // Which comments to check (top level declarations, top level, all). + Scope Scope + + // Regexp for excluding particular comment lines from check. + Exclude []string + + // Check periods at the end of sentences. + Period bool + + // Check that first letter of each sentence is capital. + Capital bool +} + +// Scope sets which comments should be checked. +type Scope string + +// List of available check scopes. +const ( + // DeclScope is for top level declaration comments. + DeclScope Scope = "declarations" + // TopLevelScope is for all top level comments. + TopLevelScope Scope = "toplevel" + // AllScope is for all comments. + AllScope Scope = "all" +) -- cgit mrf-deployment