123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- package lint
-
- import (
- "bytes"
- "go/ast"
- "go/parser"
- "go/printer"
- "go/token"
- "go/types"
- "math"
- "regexp"
- "strings"
- )
-
- // File abstraction used for representing files.
- type File struct {
- Name string
- Pkg *Package
- content []byte
- AST *ast.File
- }
-
- // IsTest returns if the file contains tests.
- func (f *File) IsTest() bool { return strings.HasSuffix(f.Name, "_test.go") }
-
- // Content returns the file's content.
- func (f *File) Content() []byte {
- return f.content
- }
-
- // NewFile creates a new file
- func NewFile(name string, content []byte, pkg *Package) (*File, error) {
- f, err := parser.ParseFile(pkg.fset, name, content, parser.ParseComments)
- if err != nil {
- return nil, err
- }
- return &File{
- Name: name,
- content: content,
- Pkg: pkg,
- AST: f,
- }, nil
- }
-
- // ToPosition returns line and column for given position.
- func (f *File) ToPosition(pos token.Pos) token.Position {
- return f.Pkg.fset.Position(pos)
- }
-
- // Render renters a node.
- func (f *File) Render(x interface{}) string {
- var buf bytes.Buffer
- if err := printer.Fprint(&buf, f.Pkg.fset, x); err != nil {
- panic(err)
- }
- return buf.String()
- }
-
- // CommentMap builds a comment map for the file.
- func (f *File) CommentMap() ast.CommentMap {
- return ast.NewCommentMap(f.Pkg.fset, f.AST, f.AST.Comments)
- }
-
- var basicTypeKinds = map[types.BasicKind]string{
- types.UntypedBool: "bool",
- types.UntypedInt: "int",
- types.UntypedRune: "rune",
- types.UntypedFloat: "float64",
- types.UntypedComplex: "complex128",
- types.UntypedString: "string",
- }
-
- // IsUntypedConst reports whether expr is an untyped constant,
- // and indicates what its default type is.
- // scope may be nil.
- func (f *File) IsUntypedConst(expr ast.Expr) (defType string, ok bool) {
- // Re-evaluate expr outside of its context to see if it's untyped.
- // (An expr evaluated within, for example, an assignment context will get the type of the LHS.)
- exprStr := f.Render(expr)
- tv, err := types.Eval(f.Pkg.fset, f.Pkg.TypesPkg, expr.Pos(), exprStr)
- if err != nil {
- return "", false
- }
- if b, ok := tv.Type.(*types.Basic); ok {
- if dt, ok := basicTypeKinds[b.Kind()]; ok {
- return dt, true
- }
- }
-
- return "", false
- }
-
- func (f *File) isMain() bool {
- if f.AST.Name.Name == "main" {
- return true
- }
- return false
- }
-
- const directiveSpecifyDisableReason = "specify-disable-reason"
-
- func (f *File) lint(rules []Rule, config Config, failures chan Failure) {
- rulesConfig := config.Rules
- _, mustSpecifyDisableReason := config.Directives[directiveSpecifyDisableReason]
- disabledIntervals := f.disabledIntervals(rules, mustSpecifyDisableReason, failures)
- for _, currentRule := range rules {
- ruleConfig := rulesConfig[currentRule.Name()]
- currentFailures := currentRule.Apply(f, ruleConfig.Arguments)
- for idx, failure := range currentFailures {
- if failure.RuleName == "" {
- failure.RuleName = currentRule.Name()
- }
- if failure.Node != nil {
- failure.Position = ToFailurePosition(failure.Node.Pos(), failure.Node.End(), f)
- }
- currentFailures[idx] = failure
- }
- currentFailures = f.filterFailures(currentFailures, disabledIntervals)
- for _, failure := range currentFailures {
- if failure.Confidence >= config.Confidence {
- failures <- failure
- }
- }
- }
- }
-
- type enableDisableConfig struct {
- enabled bool
- position int
- }
-
- const directiveRE = `^//[\s]*revive:(enable|disable)(?:-(line|next-line))?(?::([^\s]+))?[\s]*(?: (.+))?$`
- const directivePos = 1
- const modifierPos = 2
- const rulesPos = 3
- const reasonPos = 4
-
- var re = regexp.MustCompile(directiveRE)
-
- func (f *File) disabledIntervals(rules []Rule, mustSpecifyDisableReason bool, failures chan Failure) disabledIntervalsMap {
- enabledDisabledRulesMap := make(map[string][]enableDisableConfig)
-
- getEnabledDisabledIntervals := func() disabledIntervalsMap {
- result := make(disabledIntervalsMap)
-
- for ruleName, disabledArr := range enabledDisabledRulesMap {
- ruleResult := []DisabledInterval{}
- for i := 0; i < len(disabledArr); i++ {
- interval := DisabledInterval{
- RuleName: ruleName,
- From: token.Position{
- Filename: f.Name,
- Line: disabledArr[i].position,
- },
- To: token.Position{
- Filename: f.Name,
- Line: math.MaxInt32,
- },
- }
- if i%2 == 0 {
- ruleResult = append(ruleResult, interval)
- } else {
- ruleResult[len(ruleResult)-1].To.Line = disabledArr[i].position
- }
- }
- result[ruleName] = ruleResult
- }
-
- return result
- }
-
- handleConfig := func(isEnabled bool, line int, name string) {
- existing, ok := enabledDisabledRulesMap[name]
- if !ok {
- existing = []enableDisableConfig{}
- enabledDisabledRulesMap[name] = existing
- }
- if (len(existing) > 1 && existing[len(existing)-1].enabled == isEnabled) ||
- (len(existing) == 0 && isEnabled) {
- return
- }
- existing = append(existing, enableDisableConfig{
- enabled: isEnabled,
- position: line,
- })
- enabledDisabledRulesMap[name] = existing
- }
-
- handleRules := func(filename, modifier string, isEnabled bool, line int, ruleNames []string) []DisabledInterval {
- var result []DisabledInterval
- for _, name := range ruleNames {
- if modifier == "line" {
- handleConfig(isEnabled, line, name)
- handleConfig(!isEnabled, line, name)
- } else if modifier == "next-line" {
- handleConfig(isEnabled, line+1, name)
- handleConfig(!isEnabled, line+1, name)
- } else {
- handleConfig(isEnabled, line, name)
- }
- }
- return result
- }
-
- handleComment := func(filename string, c *ast.CommentGroup, line int) {
- comments := c.List
- for _, c := range comments {
- match := re.FindStringSubmatch(c.Text)
- if len(match) == 0 {
- return
- }
-
- ruleNames := []string{}
- tempNames := strings.Split(match[rulesPos], ",")
- for _, name := range tempNames {
- name = strings.Trim(name, "\n")
- if len(name) > 0 {
- ruleNames = append(ruleNames, name)
- }
- }
-
- mustCheckDisablingReason := mustSpecifyDisableReason && match[directivePos] == "disable"
- if mustCheckDisablingReason && strings.Trim(match[reasonPos], " ") == "" {
- failures <- Failure{
- Confidence: 1,
- RuleName: directiveSpecifyDisableReason,
- Failure: "reason of lint disabling not found",
- Position: ToFailurePosition(c.Pos(), c.End(), f),
- Node: c,
- }
- continue // skip this linter disabling directive
- }
-
- // TODO: optimize
- if len(ruleNames) == 0 {
- for _, rule := range rules {
- ruleNames = append(ruleNames, rule.Name())
- }
- }
-
- handleRules(filename, match[modifierPos], match[directivePos] == "enable", line, ruleNames)
- }
- }
-
- comments := f.AST.Comments
- for _, c := range comments {
- handleComment(f.Name, c, f.ToPosition(c.End()).Line)
- }
-
- return getEnabledDisabledIntervals()
- }
-
- func (f *File) filterFailures(failures []Failure, disabledIntervals disabledIntervalsMap) []Failure {
- result := []Failure{}
- for _, failure := range failures {
- fStart := failure.Position.Start.Line
- fEnd := failure.Position.End.Line
- intervals, ok := disabledIntervals[failure.RuleName]
- if !ok {
- result = append(result, failure)
- } else {
- include := true
- for _, interval := range intervals {
- intStart := interval.From.Line
- intEnd := interval.To.Line
- if (fStart >= intStart && fStart <= intEnd) ||
- (fEnd >= intStart && fEnd <= intEnd) {
- include = false
- break
- }
- }
- if include {
- result = append(result, failure)
- }
- }
- }
- return result
- }
|