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