github.com/songshiyun/revive@v1.1.5-0.20220323112655-f8433a19b3c5/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 renders 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 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 return f.AST.Name.Name == "main" 95 } 96 97 const directiveSpecifyDisableReason = "specify-disable-reason" 98 99 func (f *File) lint(rules []Rule, config Config, failures chan Failure) { 100 rulesConfig := config.Rules 101 _, mustSpecifyDisableReason := config.Directives[directiveSpecifyDisableReason] 102 disabledIntervals := f.disabledIntervals(rules, mustSpecifyDisableReason, failures) 103 for _, currentRule := range rules { 104 ruleConfig := rulesConfig[currentRule.Name()] 105 currentFailures := currentRule.Apply(f, ruleConfig.Arguments) 106 for idx, failure := range currentFailures { 107 if failure.RuleName == "" { 108 failure.RuleName = currentRule.Name() 109 } 110 if failure.Node != nil { 111 failure.Position = ToFailurePosition(failure.Node.Pos(), failure.Node.End(), f) 112 } 113 currentFailures[idx] = failure 114 } 115 currentFailures = f.filterFailures(currentFailures, disabledIntervals) 116 for _, failure := range currentFailures { 117 if failure.Confidence >= config.Confidence { 118 failures <- failure 119 } 120 } 121 } 122 } 123 124 type enableDisableConfig struct { 125 enabled bool 126 position int 127 } 128 129 const ( 130 directiveRE = `^//[\s]*revive:(enable|disable)(?:-(line|next-line))?(?::([^\s]+))?[\s]*(?: (.+))?$` 131 directivePos = 1 132 modifierPos = 2 133 rulesPos = 3 134 reasonPos = 4 135 ) 136 137 var re = regexp.MustCompile(directiveRE) 138 139 func (f *File) disabledIntervals(rules []Rule, mustSpecifyDisableReason bool, failures chan Failure) disabledIntervalsMap { 140 enabledDisabledRulesMap := make(map[string][]enableDisableConfig) 141 142 getEnabledDisabledIntervals := func() disabledIntervalsMap { 143 result := make(disabledIntervalsMap) 144 145 for ruleName, disabledArr := range enabledDisabledRulesMap { 146 ruleResult := []DisabledInterval{} 147 for i := 0; i < len(disabledArr); i++ { 148 interval := DisabledInterval{ 149 RuleName: ruleName, 150 From: token.Position{ 151 Filename: f.Name, 152 Line: disabledArr[i].position, 153 }, 154 To: token.Position{ 155 Filename: f.Name, 156 Line: math.MaxInt32, 157 }, 158 } 159 if i%2 == 0 { 160 ruleResult = append(ruleResult, interval) 161 } else { 162 ruleResult[len(ruleResult)-1].To.Line = disabledArr[i].position 163 } 164 } 165 result[ruleName] = ruleResult 166 } 167 168 return result 169 } 170 171 handleConfig := func(isEnabled bool, line int, name string) { 172 existing, ok := enabledDisabledRulesMap[name] 173 if !ok { 174 existing = []enableDisableConfig{} 175 enabledDisabledRulesMap[name] = existing 176 } 177 if (len(existing) > 1 && existing[len(existing)-1].enabled == isEnabled) || 178 (len(existing) == 0 && isEnabled) { 179 return 180 } 181 existing = append(existing, enableDisableConfig{ 182 enabled: isEnabled, 183 position: line, 184 }) 185 enabledDisabledRulesMap[name] = existing 186 } 187 188 handleRules := func(filename, modifier string, isEnabled bool, line int, ruleNames []string) []DisabledInterval { 189 var result []DisabledInterval 190 for _, name := range ruleNames { 191 if modifier == "line" { 192 handleConfig(isEnabled, line, name) 193 handleConfig(!isEnabled, line, name) 194 } else if modifier == "next-line" { 195 handleConfig(isEnabled, line+1, name) 196 handleConfig(!isEnabled, line+1, name) 197 } else { 198 handleConfig(isEnabled, line, name) 199 } 200 } 201 return result 202 } 203 204 handleComment := func(filename string, c *ast.CommentGroup, line int) { 205 comments := c.List 206 for _, c := range comments { 207 match := re.FindStringSubmatch(c.Text) 208 if len(match) == 0 { 209 continue 210 } 211 ruleNames := []string{} 212 tempNames := strings.Split(match[rulesPos], ",") 213 214 for _, name := range tempNames { 215 name = strings.Trim(name, "\n") 216 if len(name) > 0 { 217 ruleNames = append(ruleNames, name) 218 } 219 } 220 221 mustCheckDisablingReason := mustSpecifyDisableReason && match[directivePos] == "disable" 222 if mustCheckDisablingReason && strings.Trim(match[reasonPos], " ") == "" { 223 failures <- Failure{ 224 Confidence: 1, 225 RuleName: directiveSpecifyDisableReason, 226 Failure: "reason of lint disabling not found", 227 Position: ToFailurePosition(c.Pos(), c.End(), f), 228 Node: c, 229 } 230 continue // skip this linter disabling directive 231 } 232 233 // TODO: optimize 234 if len(ruleNames) == 0 { 235 for _, rule := range rules { 236 ruleNames = append(ruleNames, rule.Name()) 237 } 238 } 239 240 handleRules(filename, match[modifierPos], match[directivePos] == "enable", line, ruleNames) 241 } 242 } 243 244 comments := f.AST.Comments 245 for _, c := range comments { 246 handleComment(f.Name, c, f.ToPosition(c.End()).Line) 247 } 248 249 return getEnabledDisabledIntervals() 250 } 251 252 func (f *File) filterFailures(failures []Failure, disabledIntervals disabledIntervalsMap) []Failure { 253 result := []Failure{} 254 for _, failure := range failures { 255 fStart := failure.Position.Start.Line 256 fEnd := failure.Position.End.Line 257 intervals, ok := disabledIntervals[failure.RuleName] 258 if !ok { 259 result = append(result, failure) 260 } else { 261 include := true 262 for _, interval := range intervals { 263 intStart := interval.From.Line 264 intEnd := interval.To.Line 265 if (fStart >= intStart && fStart <= intEnd) || 266 (fEnd >= intStart && fEnd <= intEnd) { 267 include = false 268 break 269 } 270 } 271 if include { 272 result = append(result, failure) 273 } 274 } 275 } 276 return result 277 }