1package lint 2 3import ( 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. 16type 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. 24func (f *File) IsTest() bool { return strings.HasSuffix(f.Name, "_test.go") } 25 26// Content returns the file's content. 27func (f *File) Content() []byte { 28 return f.content 29} 30 31// NewFile creates a new file 32func 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. 46func (f *File) ToPosition(pos token.Pos) token.Position { 47 return f.Pkg.fset.Position(pos) 48} 49 50// Render renters a node. 51func (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. 60func (f *File) CommentMap() ast.CommentMap { 61 return ast.NewCommentMap(f.Pkg.fset, f.AST, f.AST.Comments) 62} 63 64var 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. 76func (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 93func (f *File) isMain() bool { 94 if f.AST.Name.Name == "main" { 95 return true 96 } 97 return false 98} 99 100const directiveSpecifyDisableReason = "specify-disable-reason" 101 102func (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 127type enableDisableConfig struct { 128 enabled bool 129 position int 130} 131 132const directiveRE = `^//[\s]*revive:(enable|disable)(?:-(line|next-line))?(?::([^\s]+))?[\s]*(?: (.+))?$` 133const directivePos = 1 134const modifierPos = 2 135const rulesPos = 3 136const reasonPos = 4 137 138var re = regexp.MustCompile(directiveRE) 139 140func (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 253func (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} 279