Skip to content

Commit

Permalink
Add exclude pattern.
Browse files Browse the repository at this point in the history
  • Loading branch information
Denis Krivak committed Jan 9, 2021
1 parent 90874a1 commit c85e9f3
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 59 deletions.
112 changes: 67 additions & 45 deletions getters.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"go/ast"
"go/token"
"io/ioutil"
"regexp"
"strings"
)

Expand All @@ -13,57 +14,74 @@ import (
// code it should not necessarily have a period at the end.
const specialReplacer = "<godotSpecialReplacer>"

// getComments extracts comments from a file.
func getComments(file *ast.File, fset *token.FileSet, scope Scope) ([]comment, error) {
if len(file.Comments) == 0 {
return nil, nil
type parsedFile struct {
fset *token.FileSet
file *ast.File
lines []string
}

func newParsedFile(file *ast.File, fset *token.FileSet) (*parsedFile, error) {
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.
lines, err := readFile(file, fset)
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 := file.Comments[len(file.Comments)-1]
if p := fset.Position(lastComment.End()); len(lines) < p.Line {
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 {
if len(pf.file.Comments) == 0 {
return nil
}

var comments []comment
decl := getDeclarationComments(file, fset, lines)
decl := pf.getDeclarationComments(exclude)
switch scope {
case AllScope:
// All comments
comments = getAllComments(file, fset, lines)
comments = pf.getAllComments(exclude)
case TopLevelScope:
// All top level comments and comments from the inside
// of top level blocks
comments = append(
getBlockComments(file, fset, lines),
getTopLevelComments(file, fset, lines)...,
pf.getBlockComments(exclude),
pf.getTopLevelComments(exclude)...,
)
default:
// Top level declaration comments and comments from the inside
// of top level blocks
comments = append(getBlockComments(file, fset, lines), decl...)
comments = append(pf.getBlockComments(exclude), decl...)
}

// Set `decl` flag
setDecl(comments, decl)

return comments, nil
return comments
}

// getBlockComments gets comments from the inside of top level blocks:
// var (...), const (...).
func getBlockComments(file *ast.File, fset *token.FileSet, lines []string) []comment {
func (pf *parsedFile) getBlockComments(exclude *regexp.Regexp) []comment {
var comments []comment
for _, decl := range file.Decls {
for _, decl := range pf.file.Decls {
d, ok := decl.(*ast.GenDecl)
if !ok {
continue
Expand All @@ -72,7 +90,7 @@ func getBlockComments(file *ast.File, fset *token.FileSet, lines []string) []com
if d.Lparen == 0 {
continue
}
for _, c := range file.Comments {
for _, c := range pf.file.Comments {
if c == nil || len(c.List) == 0 {
continue
}
Expand All @@ -84,46 +102,46 @@ func getBlockComments(file *ast.File, fset *token.FileSet, lines []string) []com
// (the block itself is top level, so comments inside this block
// would be on column 2)
// nolint: gomnd
if fset.Position(c.Pos()).Column != 2 {
if pf.fset.Position(c.Pos()).Column != 2 {
continue
}
firstLine := fset.Position(c.Pos()).Line
lastLine := fset.Position(c.End()).Line
firstLine := pf.fset.Position(c.Pos()).Line
lastLine := pf.fset.Position(c.End()).Line
comments = append(comments, comment{
lines: lines[firstLine-1 : lastLine],
text: getText(c),
start: fset.Position(c.List[0].Slash),
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 getTopLevelComments(file *ast.File, fset *token.FileSet, lines []string) []comment {
func (pf *parsedFile) getTopLevelComments(exclude *regexp.Regexp) []comment {
var comments []comment // nolint: prealloc
for _, c := range file.Comments {
for _, c := range pf.file.Comments {
if c == nil || len(c.List) == 0 {
continue
}
if fset.Position(c.Pos()).Column != 1 {
if pf.fset.Position(c.Pos()).Column != 1 {
continue
}
firstLine := fset.Position(c.Pos()).Line
lastLine := fset.Position(c.End()).Line
firstLine := pf.fset.Position(c.Pos()).Line
lastLine := pf.fset.Position(c.End()).Line
comments = append(comments, comment{
lines: lines[firstLine-1 : lastLine],
text: getText(c),
start: fset.Position(c.List[0].Slash),
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 getDeclarationComments(file *ast.File, fset *token.FileSet, lines []string) []comment {
func (pf *parsedFile) getDeclarationComments(exclude *regexp.Regexp) []comment {
var comments []comment // nolint: prealloc
for _, decl := range file.Decls {
for _, decl := range pf.file.Decls {
var cg *ast.CommentGroup
switch d := decl.(type) {
case *ast.GenDecl:
Expand All @@ -136,30 +154,30 @@ func getDeclarationComments(file *ast.File, fset *token.FileSet, lines []string)
continue
}

firstLine := fset.Position(cg.Pos()).Line
lastLine := fset.Position(cg.End()).Line
firstLine := pf.fset.Position(cg.Pos()).Line
lastLine := pf.fset.Position(cg.End()).Line
comments = append(comments, comment{
lines: lines[firstLine-1 : lastLine],
text: getText(cg),
start: fset.Position(cg.List[0].Slash),
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 getAllComments(file *ast.File, fset *token.FileSet, lines []string) []comment {
func (pf *parsedFile) getAllComments(exclude *regexp.Regexp) []comment {
var comments []comment //nolint: prealloc
for _, c := range file.Comments {
for _, c := range pf.file.Comments {
if c == nil || len(c.List) == 0 {
continue
}
firstLine := fset.Position(c.Pos()).Line
lastLine := fset.Position(c.End()).Line
firstLine := pf.fset.Position(c.Pos()).Line
lastLine := pf.fset.Position(c.End()).Line
comments = append(comments, comment{
lines: lines[firstLine-1 : lastLine],
start: fset.Position(c.List[0].Slash),
text: getText(c),
lines: pf.lines[firstLine-1 : lastLine],
start: pf.fset.Position(c.List[0].Slash),
text: getText(c, exclude),
})
}
return comments
Expand All @@ -170,7 +188,7 @@ func getAllComments(file *ast.File, fset *token.FileSet, lines []string) []comme
// 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) (s string) {
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) {
Expand All @@ -193,6 +211,10 @@ func getText(comment *ast.CommentGroup) (s string) {
if !isBlock {
line = strings.TrimPrefix(line, "//")
}
if exclude != nil && exclude.MatchString(line) {
s += specialReplacer + "\n"
continue
}
s += line + "\n"
}
}
Expand Down
48 changes: 43 additions & 5 deletions getters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"go/parser"
"go/token"
"path/filepath"
"regexp"
"strings"
"testing"
)
Expand All @@ -17,6 +18,11 @@ func TestGetComments(t *testing.T) {
t.Fatalf("Failed to parse input file: %v", err)
}

pf, err := newParsedFile(file, fset)
if err != nil {
t.Fatalf("Failed to parse input file: %v", err)
}

testCases := []struct {
name string
scope Scope
Expand All @@ -42,10 +48,7 @@ func TestGetComments(t *testing.T) {
for _, tt := range testCases {
tt := tt
t.Run(tt.name, func(t *testing.T) {
comments, err := getComments(file, fset, tt.scope)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
comments := pf.getComments(tt.scope, nil)
var expected int
for _, c := range comments {
if linesContain(c.lines, "[NONE]") {
Expand Down Expand Up @@ -73,6 +76,7 @@ func TestGetText(t *testing.T) {
name string
comment *ast.CommentGroup
text string
exclude *regexp.Regexp
}{
{
name: "regular text",
Expand Down Expand Up @@ -180,12 +184,46 @@ func TestGetText(t *testing.T) {
comment: &ast.CommentGroup{List: []*ast.Comment{}},
text: "",
},
{
name: "single excluded line",
comment: &ast.CommentGroup{List: []*ast.Comment{
{Text: "// Hello, world."},
}},
text: "<godotSpecialReplacer>",
exclude: regexp.MustCompile("Hello"),
},
{
name: "excluded line in the middle",
comment: &ast.CommentGroup{List: []*ast.Comment{
{Text: "/*\n" +
"Read more:\n" +
"@intenal.link\n" +
"Thanks." +
"*/"},
}},
text: "\n" +
"Read more:\n" +
"<godotSpecialReplacer>\n" +
"Thanks." +
"",
exclude: regexp.MustCompile("^@.+"),
},
{
name: "excluded line at the end",
comment: &ast.CommentGroup{List: []*ast.Comment{
{Text: "/* Read more:\n" +
"@intenal.link */"},
}},
text: " Read more:\n" +
"<godotSpecialReplacer>",
exclude: regexp.MustCompile("^@.+"),
},
}

for _, tt := range testCases {
tt := tt
t.Run(tt.name, func(t *testing.T) {
if text := getText(tt.comment); text != tt.text {
if text := getText(tt.comment, tt.exclude); text != tt.text {
t.Fatalf("Wrong text\n expected: '%s'\n got: '%s'", tt.text, text)
}
})
Expand Down
14 changes: 12 additions & 2 deletions godot.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"go/token"
"io/ioutil"
"os"
"regexp"
"sort"
"strings"
)
Expand Down Expand Up @@ -42,11 +43,20 @@ type comment struct {

// Run runs this linter on the provided code.
func Run(file *ast.File, fset *token.FileSet, settings Settings) ([]Issue, error) {
comments, err := getComments(file, fset, settings.Scope)
pf, err := newParsedFile(file, fset)
if err != nil {
return nil, fmt.Errorf("get comments: %v", err)
return nil, fmt.Errorf("parse input file: %v", err)
}

var exclude *regexp.Regexp
if settings.Exclude != "" {
exclude, err = regexp.Compile(settings.Exclude)
if err != nil {
return nil, fmt.Errorf("invalid regexp: %v", err)
}
}

comments := pf.getComments(settings.Scope, exclude)
issues := checkComments(comments, settings)
sortIssues(issues)

Expand Down

0 comments on commit c85e9f3

Please sign in to comment.