mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-04 15:46:30 +00:00
c28f7cb29f
Initial part of #435
283 lines
7.5 KiB
Go
283 lines
7.5 KiB
Go
package godot
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/token"
|
|
"io/ioutil"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
errEmptyInput = errors.New("empty input")
|
|
errUnsuitableInput = errors.New("unsuitable 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 = "<godotSpecialReplacer>"
|
|
|
|
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)
|
|
}
|
|
|
|
// Dirty hack. For some cases Go generates temporary files during
|
|
// compilation process if there is a cgo block in the source file. Some of
|
|
// these temporary files are just copies of original source files but with
|
|
// new generated comments at the top. Because of them the content differs
|
|
// from AST. For some reason it differs only in golangci-lint. I failed to
|
|
// find out the exact description of the process, so let's just skip files
|
|
// generated by cgo.
|
|
if isCgoGenerated(pf.lines) {
|
|
return nil, errUnsuitableInput
|
|
}
|
|
|
|
// 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("inconsistency 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
|
|
}
|
|
|
|
func isCgoGenerated(lines []string) bool {
|
|
for i := range lines {
|
|
if strings.Contains(lines[i], "Code generated by cmd/cgo") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|