workgroups/vendor/github.com/tetafro/godot/getters.go
Marvin Preuss 1d4ae27878
All checks were successful
continuous-integration/drone/push Build is passing
ci: drone yaml with reusable anchors
2021-09-24 17:34:17 +02:00

284 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
}