You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

file.go 7.3KB

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