1
16
17 package warn
18
19 import (
20 "fmt"
21 "regexp"
22 "strings"
23
24 "github.com/bazelbuild/buildtools/build"
25 )
26
27
28
29 const FunctionLengthDocstringThreshold = 5
30
31
32
33 func getDocstring(stmts []build.Expr) (*build.Expr, bool) {
34 for i, stmt := range stmts {
35 if stmt == nil {
36 continue
37 }
38 switch stmt.(type) {
39 case *build.CommentBlock:
40 continue
41 case *build.StringExpr:
42 return &stmts[i], true
43 default:
44 return &stmts[i], false
45 }
46 }
47 return nil, false
48 }
49
50 func moduleDocstringWarning(f *build.File) []*LinterFinding {
51 if f.Type != build.TypeDefault && f.Type != build.TypeBzl {
52 return nil
53 }
54 if stmt, ok := getDocstring(f.Stmt); stmt != nil && !ok {
55 start, _ := (*stmt).Span()
56 end := build.Position{
57 Line: start.Line,
58 LineRune: start.LineRune + 1,
59 Byte: start.Byte + 1,
60 }
61 finding := makeLinterFinding(*stmt, `The file has no module docstring.
62 A module docstring is a string literal (not a comment) which should be the first statement of a file (it may follow comment lines).`)
63 finding.End = end
64 return []*LinterFinding{finding}
65 }
66 return nil
67 }
68
69 func stmtsCount(stmts []build.Expr) int {
70 result := 0
71 for _, stmt := range stmts {
72 result++
73 switch stmt := stmt.(type) {
74 case *build.IfStmt:
75 result += stmtsCount(stmt.True)
76 result += stmtsCount(stmt.False)
77 case *build.ForStmt:
78 result += stmtsCount(stmt.Body)
79 }
80 }
81 return result
82 }
83
84
85 type docstringInfo struct {
86 hasHeader bool
87 args map[string]build.Position
88 returns bool
89 deprecated bool
90 argumentsPos build.Position
91 }
92
93
94 func countLeadingSpaces(s string) int {
95 spaces := 0
96 for _, c := range s {
97 if c == ' ' {
98 spaces++
99 } else {
100 break
101 }
102 }
103 return spaces
104 }
105
106 var argRegex = regexp.MustCompile(`^ *(\*?\*?\w*)( *\([\w\ ,<>\[\]]+\))?:`)
107
108
109
110 func parseFunctionDocstring(doc *build.StringExpr) docstringInfo {
111 start, _ := doc.Span()
112 indent := start.LineRune - 1
113 prefix := strings.Repeat(" ", indent)
114 lines := strings.Split(doc.Value, "\n")
115
116
117 for i, line := range lines {
118 lines[i] = strings.TrimRight(line, "\r")
119 }
120
121 info := docstringInfo{}
122 info.args = make(map[string]build.Position)
123
124 isArgumentsDescription := false
125 argIndentation := 1000000
126
127 for i := range lines {
128 lines[i] = strings.TrimRight(lines[i], " ")
129 }
130
131
132 for i, line := range lines {
133 if line == "" {
134 continue
135 }
136 if i == len(lines)-1 || lines[i+1] == "" {
137 info.hasHeader = true
138 }
139 break
140 }
141
142
143 for i, line := range lines {
144 switch line {
145 case prefix + "Arguments:":
146 info.argumentsPos = build.Position{
147 Line: start.Line + i,
148 LineRune: indent,
149 }
150 isArgumentsDescription = true
151 continue
152 case prefix + "Args:":
153 isArgumentsDescription = true
154 continue
155 case prefix + "Returns:":
156 isArgumentsDescription = false
157 info.returns = true
158 continue
159 case prefix + "Deprecated:":
160 isArgumentsDescription = false
161 info.deprecated = true
162 continue
163 }
164
165 if isArgumentsDescription {
166 newIndentation := countLeadingSpaces(line)
167
168 if line != "" && newIndentation <= indent {
169
170 isArgumentsDescription = false
171 continue
172 } else if newIndentation > argIndentation {
173
174 continue
175 } else {
176
177 result := argRegex.FindStringSubmatch(line)
178 if len(result) > 1 {
179 argIndentation = newIndentation
180 info.args[result[1]] = build.Position{
181 Line: start.Line + i,
182 LineRune: indent + argIndentation,
183 }
184 }
185 }
186 }
187 }
188 return info
189 }
190
191 func hasReturnValues(def *build.DefStmt) bool {
192 result := false
193 build.WalkStatements(def, func(expr build.Expr, stack []build.Expr) (err error) {
194 if _, ok := expr.(*build.DefStmt); ok && len(stack) > 0 {
195
196 return &build.StopTraversalError{}
197 }
198
199 ret, ok := expr.(*build.ReturnStmt)
200 if ok && ret.Result != nil {
201 result = true
202 }
203 return
204 })
205 return result
206 }
207
208
209
210 func isDocstringRequired(def *build.DefStmt) bool {
211 if start, _ := def.Span(); start.LineRune > 1 {
212
213 return false
214 }
215 return !strings.HasPrefix(def.Name, "_") && stmtsCount(def.Body) >= FunctionLengthDocstringThreshold
216 }
217
218 func functionDocstringWarning(f *build.File) []*LinterFinding {
219 var findings []*LinterFinding
220
221
222 for _, stmt := range f.Stmt {
223 def, ok := stmt.(*build.DefStmt)
224 if !ok {
225 continue
226 }
227
228 if !isDocstringRequired(def) {
229 continue
230 }
231
232 if _, ok = getDocstring(def.Body); ok {
233 continue
234 }
235
236 message := fmt.Sprintf(`The function %q has no docstring.
237 A docstring is a string literal (not a comment) which should be the first statement of a function body (it may follow comment lines).`, def.Name)
238 finding := makeLinterFinding(def, message)
239 finding.End = def.ColonPos
240 findings = append(findings, finding)
241 }
242 return findings
243 }
244
245 func functionDocstringHeaderWarning(f *build.File) []*LinterFinding {
246 var findings []*LinterFinding
247
248 build.WalkStatements(f, func(expr build.Expr, stack []build.Expr) (err error) {
249 def, ok := expr.(*build.DefStmt)
250 if !ok {
251 return
252 }
253
254 doc, ok := getDocstring(def.Body)
255 if !ok {
256 return
257 }
258
259 info := parseFunctionDocstring((*doc).(*build.StringExpr))
260
261 if !info.hasHeader {
262 message := fmt.Sprintf("The docstring for the function %q should start with a one-line summary.", def.Name)
263 findings = append(findings, makeLinterFinding(*doc, message))
264 }
265 return
266 })
267 return findings
268 }
269
270 func functionDocstringArgsWarning(f *build.File) []*LinterFinding {
271 var findings []*LinterFinding
272
273 build.WalkStatements(f, func(expr build.Expr, stack []build.Expr) (err error) {
274 def, ok := expr.(*build.DefStmt)
275 if !ok {
276 return
277 }
278
279 doc, ok := getDocstring(def.Body)
280 if !ok {
281 return
282 }
283
284 info := parseFunctionDocstring((*doc).(*build.StringExpr))
285
286 if info.argumentsPos.LineRune > 0 {
287 argumentsEnd := info.argumentsPos
288 argumentsEnd.LineRune += len("Arguments:")
289 argumentsEnd.Byte += len("Arguments:")
290 finding := makeLinterFinding(*doc, `Prefer "Args:" to "Arguments:" when documenting function arguments.`)
291 finding.Start = info.argumentsPos
292 finding.End = argumentsEnd
293 findings = append(findings, finding)
294 }
295
296 if !isDocstringRequired(def) && len(info.args) == 0 {
297 return
298 }
299
300
301
302
303 notDocumentedArguments := []string{}
304 paramNames := make(map[string]bool)
305 for _, param := range def.Params {
306 name, op := build.GetParamName(param)
307 if name == "" {
308 continue
309 }
310 name = op + name
311 paramNames[name] = true
312 if _, ok := info.args[name]; !ok {
313 notDocumentedArguments = append(notDocumentedArguments, name)
314 }
315 }
316
317
318 if len(notDocumentedArguments) > 0 {
319 message := fmt.Sprintf("Argument %q is not documented.", notDocumentedArguments[0])
320 plural := ""
321 if len(notDocumentedArguments) > 1 {
322 message = fmt.Sprintf(
323 `Arguments "%s" are not documented.`,
324 strings.Join(notDocumentedArguments, `", "`),
325 )
326 plural = "s"
327 }
328
329 if len(info.args) == 0 {
330
331
332 message += fmt.Sprintf(`
333
334 If the documentation for the argument%s exists but is not recognized by Buildifier
335 make sure it follows the line "Args:" which has the same indentation as the opening """,
336 and the argument description starts with "<argument_name>:" and indented with at least
337 one (preferably two) space more than "Args:", for example:
338
339 def %s(%s):
340 """Function description.
341
342 Args:
343 %s: argument description, can be
344 multiline with additional indentation.
345 """`, plural, def.Name, notDocumentedArguments[0], notDocumentedArguments[0])
346 }
347
348 findings = append(findings, makeLinterFinding(*doc, message))
349 }
350
351
352 for name, pos := range info.args {
353 if paramNames[name] {
354 continue
355 }
356 msg := fmt.Sprintf("Argument %q is documented but doesn't exist in the function signature.", name)
357
358 for _, asterisks := range []string{"*", "**"} {
359 if paramNames[asterisks+name] {
360 msg += fmt.Sprintf(` Do you mean "%s%s"?`, asterisks, name)
361 break
362 }
363 }
364 posEnd := pos
365 posEnd.LineRune += len(name)
366 finding := makeLinterFinding(*doc, msg)
367 finding.Start = pos
368 finding.End = posEnd
369 findings = append(findings, finding)
370 }
371 return
372 })
373 return findings
374 }
375
376 func functionDocstringReturnWarning(f *build.File) []*LinterFinding {
377 var findings []*LinterFinding
378
379 build.WalkStatements(f, func(expr build.Expr, stack []build.Expr) (err error) {
380 def, ok := expr.(*build.DefStmt)
381 if !ok {
382 return
383 }
384
385 doc, ok := getDocstring(def.Body)
386 if !ok {
387 return
388 }
389
390 info := parseFunctionDocstring((*doc).(*build.StringExpr))
391
392
393 if isDocstringRequired(def) && hasReturnValues(def) && !info.returns {
394 message := fmt.Sprintf("Return value of %q is not documented.", def.Name)
395 findings = append(findings, makeLinterFinding(*doc, message))
396 }
397 return
398 })
399 return findings
400 }
401
View as plain text