github.com/Johnny2210/revive@v1.0.8-0.20210625134200-febf37ccd0f5/lint/file.go (about)

     1  package lint
     2  
     3  import (
     4  	"bytes"
     5  	"go/ast"
     6  	"go/parser"
     7  	"go/printer"
     8  	"go/token"
     9  	"go/types"
    10  	"math"
    11  	"regexp"
    12  	"strings"
    13  )
    14  
    15  // File abstraction used for representing files.
    16  type File struct {
    17  	Name    string
    18  	Pkg     *Package
    19  	content []byte
    20  	AST     *ast.File
    21  }
    22  
    23  // IsTest returns if the file contains tests.
    24  func (f *File) IsTest() bool { return strings.HasSuffix(f.Name, "_test.go") }
    25  
    26  // Content returns the file's content.
    27  func (f *File) Content() []byte {
    28  	return f.content
    29  }
    30  
    31  // NewFile creates a new file
    32  func NewFile(name string, content []byte, pkg *Package) (*File, error) {
    33  	f, err := parser.ParseFile(pkg.fset, name, content, parser.ParseComments)
    34  	if err != nil {
    35  		return nil, err
    36  	}
    37  	return &File{
    38  		Name:    name,
    39  		content: content,
    40  		Pkg:     pkg,
    41  		AST:     f,
    42  	}, nil
    43  }
    44  
    45  // ToPosition returns line and column for given position.
    46  func (f *File) ToPosition(pos token.Pos) token.Position {
    47  	return f.Pkg.fset.Position(pos)
    48  }
    49  
    50  // Render renters a node.
    51  func (f *File) Render(x interface{}) string {
    52  	var buf bytes.Buffer
    53  	if err := printer.Fprint(&buf, f.Pkg.fset, x); err != nil {
    54  		panic(err)
    55  	}
    56  	return buf.String()
    57  }
    58  
    59  // CommentMap builds a comment map for the file.
    60  func (f *File) CommentMap() ast.CommentMap {
    61  	return ast.NewCommentMap(f.Pkg.fset, f.AST, f.AST.Comments)
    62  }
    63  
    64  var basicTypeKinds = map[types.BasicKind]string{
    65  	types.UntypedBool:    "bool",
    66  	types.UntypedInt:     "int",
    67  	types.UntypedRune:    "rune",
    68  	types.UntypedFloat:   "float64",
    69  	types.UntypedComplex: "complex128",
    70  	types.UntypedString:  "string",
    71  }
    72  
    73  // IsUntypedConst reports whether expr is an untyped constant,
    74  // and indicates what its default type is.
    75  // scope may be nil.
    76  func (f *File) IsUntypedConst(expr ast.Expr) (defType string, ok bool) {
    77  	// Re-evaluate expr outside of its context to see if it's untyped.
    78  	// (An expr evaluated within, for example, an assignment context will get the type of the LHS.)
    79  	exprStr := f.Render(expr)
    80  	tv, err := types.Eval(f.Pkg.fset, f.Pkg.TypesPkg, expr.Pos(), exprStr)
    81  	if err != nil {
    82  		return "", false
    83  	}
    84  	if b, ok := tv.Type.(*types.Basic); ok {
    85  		if dt, ok := basicTypeKinds[b.Kind()]; ok {
    86  			return dt, true
    87  		}
    88  	}
    89  
    90  	return "", false
    91  }
    92  
    93  func (f *File) isMain() bool {
    94  	if f.AST.Name.Name == "main" {
    95  		return true
    96  	}
    97  	return false
    98  }
    99  
   100  const directiveSpecifyDisableReason = "specify-disable-reason"
   101  
   102  func (f *File) lint(rules []Rule, config Config, failures chan Failure) {
   103  	rulesConfig := config.Rules
   104  	_, mustSpecifyDisableReason := config.Directives[directiveSpecifyDisableReason]
   105  	disabledIntervals := f.disabledIntervals(rules, mustSpecifyDisableReason, failures)
   106  	for _, currentRule := range rules {
   107  		ruleConfig := rulesConfig[currentRule.Name()]
   108  		currentFailures := currentRule.Apply(f, ruleConfig.Arguments)
   109  		for idx, failure := range currentFailures {
   110  			if failure.RuleName == "" {
   111  				failure.RuleName = currentRule.Name()
   112  			}
   113  			if failure.Node != nil {
   114  				failure.Position = ToFailurePosition(failure.Node.Pos(), failure.Node.End(), f)
   115  			}
   116  			currentFailures[idx] = failure
   117  		}
   118  		currentFailures = f.filterFailures(currentFailures, disabledIntervals)
   119  		for _, failure := range currentFailures {
   120  			if failure.Confidence >= config.Confidence {
   121  				failures <- failure
   122  			}
   123  		}
   124  	}
   125  }
   126  
   127  type enableDisableConfig struct {
   128  	enabled  bool
   129  	position int
   130  }
   131  
   132  const directiveRE = `^//[\s]*revive:(enable|disable)(?:-(line|next-line))?(?::([^\s]+))?[\s]*(?: (.+))?$`
   133  const directivePos = 1
   134  const modifierPos = 2
   135  const rulesPos = 3
   136  const reasonPos = 4
   137  
   138  var re = regexp.MustCompile(directiveRE)
   139  
   140  func (f *File) disabledIntervals(rules []Rule, mustSpecifyDisableReason bool, failures chan Failure) disabledIntervalsMap {
   141  	enabledDisabledRulesMap := make(map[string][]enableDisableConfig)
   142  
   143  	getEnabledDisabledIntervals := func() disabledIntervalsMap {
   144  		result := make(disabledIntervalsMap)
   145  
   146  		for ruleName, disabledArr := range enabledDisabledRulesMap {
   147  			ruleResult := []DisabledInterval{}
   148  			for i := 0; i < len(disabledArr); i++ {
   149  				interval := DisabledInterval{
   150  					RuleName: ruleName,
   151  					From: token.Position{
   152  						Filename: f.Name,
   153  						Line:     disabledArr[i].position,
   154  					},
   155  					To: token.Position{
   156  						Filename: f.Name,
   157  						Line:     math.MaxInt32,
   158  					},
   159  				}
   160  				if i%2 == 0 {
   161  					ruleResult = append(ruleResult, interval)
   162  				} else {
   163  					ruleResult[len(ruleResult)-1].To.Line = disabledArr[i].position
   164  				}
   165  			}
   166  			result[ruleName] = ruleResult
   167  		}
   168  
   169  		return result
   170  	}
   171  
   172  	handleConfig := func(isEnabled bool, line int, name string) {
   173  		existing, ok := enabledDisabledRulesMap[name]
   174  		if !ok {
   175  			existing = []enableDisableConfig{}
   176  			enabledDisabledRulesMap[name] = existing
   177  		}
   178  		if (len(existing) > 1 && existing[len(existing)-1].enabled == isEnabled) ||
   179  			(len(existing) == 0 && isEnabled) {
   180  			return
   181  		}
   182  		existing = append(existing, enableDisableConfig{
   183  			enabled:  isEnabled,
   184  			position: line,
   185  		})
   186  		enabledDisabledRulesMap[name] = existing
   187  	}
   188  
   189  	handleRules := func(filename, modifier string, isEnabled bool, line int, ruleNames []string) []DisabledInterval {
   190  		var result []DisabledInterval
   191  		for _, name := range ruleNames {
   192  			if modifier == "line" {
   193  				handleConfig(isEnabled, line, name)
   194  				handleConfig(!isEnabled, line, name)
   195  			} else if modifier == "next-line" {
   196  				handleConfig(isEnabled, line+1, name)
   197  				handleConfig(!isEnabled, line+1, name)
   198  			} else {
   199  				handleConfig(isEnabled, line, name)
   200  			}
   201  		}
   202  		return result
   203  	}
   204  
   205  	handleComment := func(filename string, c *ast.CommentGroup, line int) {
   206  		comments := c.List
   207  		for _, c := range comments {
   208  			match := re.FindStringSubmatch(c.Text)
   209  			if len(match) == 0 {
   210  				return
   211  			}
   212  
   213  			ruleNames := []string{}
   214  			tempNames := strings.Split(match[rulesPos], ",")
   215  			for _, name := range tempNames {
   216  				name = strings.Trim(name, "\n")
   217  				if len(name) > 0 {
   218  					ruleNames = append(ruleNames, name)
   219  				}
   220  			}
   221  
   222  			mustCheckDisablingReason := mustSpecifyDisableReason && match[directivePos] == "disable"
   223  			if mustCheckDisablingReason && strings.Trim(match[reasonPos], " ") == "" {
   224  				failures <- Failure{
   225  					Confidence: 1,
   226  					RuleName:   directiveSpecifyDisableReason,
   227  					Failure:    "reason of lint disabling not found",
   228  					Position:   ToFailurePosition(c.Pos(), c.End(), f),
   229  					Node:       c,
   230  				}
   231  				continue // skip this linter disabling directive
   232  			}
   233  
   234  			// TODO: optimize
   235  			if len(ruleNames) == 0 {
   236  				for _, rule := range rules {
   237  					ruleNames = append(ruleNames, rule.Name())
   238  				}
   239  			}
   240  
   241  			handleRules(filename, match[modifierPos], match[directivePos] == "enable", line, ruleNames)
   242  		}
   243  	}
   244  
   245  	comments := f.AST.Comments
   246  	for _, c := range comments {
   247  		handleComment(f.Name, c, f.ToPosition(c.End()).Line)
   248  	}
   249  
   250  	return getEnabledDisabledIntervals()
   251  }
   252  
   253  func (f *File) filterFailures(failures []Failure, disabledIntervals disabledIntervalsMap) []Failure {
   254  	result := []Failure{}
   255  	for _, failure := range failures {
   256  		fStart := failure.Position.Start.Line
   257  		fEnd := failure.Position.End.Line
   258  		intervals, ok := disabledIntervals[failure.RuleName]
   259  		if !ok {
   260  			result = append(result, failure)
   261  		} else {
   262  			include := true
   263  			for _, interval := range intervals {
   264  				intStart := interval.From.Line
   265  				intEnd := interval.To.Line
   266  				if (fStart >= intStart && fStart <= intEnd) ||
   267  					(fEnd >= intStart && fEnd <= intEnd) {
   268  					include = false
   269  					break
   270  				}
   271  			}
   272  			if include {
   273  				result = append(result, failure)
   274  			}
   275  		}
   276  	}
   277  	return result
   278  }