diff options
Diffstat (limited to 'vendor/golang.org/x/lint/lint.go')
-rw-r--r-- | vendor/golang.org/x/lint/lint.go | 1614 |
1 files changed, 1614 insertions, 0 deletions
diff --git a/vendor/golang.org/x/lint/lint.go b/vendor/golang.org/x/lint/lint.go new file mode 100644 index 0000000..532a75a --- /dev/null +++ b/vendor/golang.org/x/lint/lint.go | |||
@@ -0,0 +1,1614 @@ | |||
1 | // Copyright (c) 2013 The Go Authors. All rights reserved. | ||
2 | // | ||
3 | // Use of this source code is governed by a BSD-style | ||
4 | // license that can be found in the LICENSE file or at | ||
5 | // https://developers.google.com/open-source/licenses/bsd. | ||
6 | |||
7 | // Package lint contains a linter for Go source code. | ||
8 | package lint // import "golang.org/x/lint" | ||
9 | |||
10 | import ( | ||
11 | "bufio" | ||
12 | "bytes" | ||
13 | "fmt" | ||
14 | "go/ast" | ||
15 | "go/parser" | ||
16 | "go/printer" | ||
17 | "go/token" | ||
18 | "go/types" | ||
19 | "regexp" | ||
20 | "sort" | ||
21 | "strconv" | ||
22 | "strings" | ||
23 | "unicode" | ||
24 | "unicode/utf8" | ||
25 | |||
26 | "golang.org/x/tools/go/ast/astutil" | ||
27 | "golang.org/x/tools/go/gcexportdata" | ||
28 | ) | ||
29 | |||
30 | const styleGuideBase = "https://golang.org/wiki/CodeReviewComments" | ||
31 | |||
32 | // A Linter lints Go source code. | ||
33 | type Linter struct { | ||
34 | } | ||
35 | |||
36 | // Problem represents a problem in some source code. | ||
37 | type Problem struct { | ||
38 | Position token.Position // position in source file | ||
39 | Text string // the prose that describes the problem | ||
40 | Link string // (optional) the link to the style guide for the problem | ||
41 | Confidence float64 // a value in (0,1] estimating the confidence in this problem's correctness | ||
42 | LineText string // the source line | ||
43 | Category string // a short name for the general category of the problem | ||
44 | |||
45 | // If the problem has a suggested fix (the minority case), | ||
46 | // ReplacementLine is a full replacement for the relevant line of the source file. | ||
47 | ReplacementLine string | ||
48 | } | ||
49 | |||
50 | func (p *Problem) String() string { | ||
51 | if p.Link != "" { | ||
52 | return p.Text + "\n\n" + p.Link | ||
53 | } | ||
54 | return p.Text | ||
55 | } | ||
56 | |||
57 | type byPosition []Problem | ||
58 | |||
59 | func (p byPosition) Len() int { return len(p) } | ||
60 | func (p byPosition) Swap(i, j int) { p[i], p[j] = p[j], p[i] } | ||
61 | |||
62 | func (p byPosition) Less(i, j int) bool { | ||
63 | pi, pj := p[i].Position, p[j].Position | ||
64 | |||
65 | if pi.Filename != pj.Filename { | ||
66 | return pi.Filename < pj.Filename | ||
67 | } | ||
68 | if pi.Line != pj.Line { | ||
69 | return pi.Line < pj.Line | ||
70 | } | ||
71 | if pi.Column != pj.Column { | ||
72 | return pi.Column < pj.Column | ||
73 | } | ||
74 | |||
75 | return p[i].Text < p[j].Text | ||
76 | } | ||
77 | |||
78 | // Lint lints src. | ||
79 | func (l *Linter) Lint(filename string, src []byte) ([]Problem, error) { | ||
80 | return l.LintFiles(map[string][]byte{filename: src}) | ||
81 | } | ||
82 | |||
83 | // LintFiles lints a set of files of a single package. | ||
84 | // The argument is a map of filename to source. | ||
85 | func (l *Linter) LintFiles(files map[string][]byte) ([]Problem, error) { | ||
86 | pkg := &pkg{ | ||
87 | fset: token.NewFileSet(), | ||
88 | files: make(map[string]*file), | ||
89 | } | ||
90 | var pkgName string | ||
91 | for filename, src := range files { | ||
92 | if isGenerated(src) { | ||
93 | continue // See issue #239 | ||
94 | } | ||
95 | f, err := parser.ParseFile(pkg.fset, filename, src, parser.ParseComments) | ||
96 | if err != nil { | ||
97 | return nil, err | ||
98 | } | ||
99 | if pkgName == "" { | ||
100 | pkgName = f.Name.Name | ||
101 | } else if f.Name.Name != pkgName { | ||
102 | return nil, fmt.Errorf("%s is in package %s, not %s", filename, f.Name.Name, pkgName) | ||
103 | } | ||
104 | pkg.files[filename] = &file{ | ||
105 | pkg: pkg, | ||
106 | f: f, | ||
107 | fset: pkg.fset, | ||
108 | src: src, | ||
109 | filename: filename, | ||
110 | } | ||
111 | } | ||
112 | if len(pkg.files) == 0 { | ||
113 | return nil, nil | ||
114 | } | ||
115 | return pkg.lint(), nil | ||
116 | } | ||
117 | |||
118 | var ( | ||
119 | genHdr = []byte("// Code generated ") | ||
120 | genFtr = []byte(" DO NOT EDIT.") | ||
121 | ) | ||
122 | |||
123 | // isGenerated reports whether the source file is generated code | ||
124 | // according the rules from https://golang.org/s/generatedcode. | ||
125 | func isGenerated(src []byte) bool { | ||
126 | sc := bufio.NewScanner(bytes.NewReader(src)) | ||
127 | for sc.Scan() { | ||
128 | b := sc.Bytes() | ||
129 | if bytes.HasPrefix(b, genHdr) && bytes.HasSuffix(b, genFtr) && len(b) >= len(genHdr)+len(genFtr) { | ||
130 | return true | ||
131 | } | ||
132 | } | ||
133 | return false | ||
134 | } | ||
135 | |||
136 | // pkg represents a package being linted. | ||
137 | type pkg struct { | ||
138 | fset *token.FileSet | ||
139 | files map[string]*file | ||
140 | |||
141 | typesPkg *types.Package | ||
142 | typesInfo *types.Info | ||
143 | |||
144 | // sortable is the set of types in the package that implement sort.Interface. | ||
145 | sortable map[string]bool | ||
146 | // main is whether this is a "main" package. | ||
147 | main bool | ||
148 | |||
149 | problems []Problem | ||
150 | } | ||
151 | |||
152 | func (p *pkg) lint() []Problem { | ||
153 | if err := p.typeCheck(); err != nil { | ||
154 | /* TODO(dsymonds): Consider reporting these errors when golint operates on entire packages. | ||
155 | if e, ok := err.(types.Error); ok { | ||
156 | pos := p.fset.Position(e.Pos) | ||
157 | conf := 1.0 | ||
158 | if strings.Contains(e.Msg, "can't find import: ") { | ||
159 | // Golint is probably being run in a context that doesn't support | ||
160 | // typechecking (e.g. package files aren't found), so don't warn about it. | ||
161 | conf = 0 | ||
162 | } | ||
163 | if conf > 0 { | ||
164 | p.errorfAt(pos, conf, category("typechecking"), e.Msg) | ||
165 | } | ||
166 | |||
167 | // TODO(dsymonds): Abort if !e.Soft? | ||
168 | } | ||
169 | */ | ||
170 | } | ||
171 | |||
172 | p.scanSortable() | ||
173 | p.main = p.isMain() | ||
174 | |||
175 | for _, f := range p.files { | ||
176 | f.lint() | ||
177 | } | ||
178 | |||
179 | sort.Sort(byPosition(p.problems)) | ||
180 | |||
181 | return p.problems | ||
182 | } | ||
183 | |||
184 | // file represents a file being linted. | ||
185 | type file struct { | ||
186 | pkg *pkg | ||
187 | f *ast.File | ||
188 | fset *token.FileSet | ||
189 | src []byte | ||
190 | filename string | ||
191 | } | ||
192 | |||
193 | func (f *file) isTest() bool { return strings.HasSuffix(f.filename, "_test.go") } | ||
194 | |||
195 | func (f *file) lint() { | ||
196 | f.lintPackageComment() | ||
197 | f.lintImports() | ||
198 | f.lintBlankImports() | ||
199 | f.lintExported() | ||
200 | f.lintNames() | ||
201 | f.lintElses() | ||
202 | f.lintRanges() | ||
203 | f.lintErrorf() | ||
204 | f.lintErrors() | ||
205 | f.lintErrorStrings() | ||
206 | f.lintReceiverNames() | ||
207 | f.lintIncDec() | ||
208 | f.lintErrorReturn() | ||
209 | f.lintUnexportedReturn() | ||
210 | f.lintTimeNames() | ||
211 | f.lintContextKeyTypes() | ||
212 | f.lintContextArgs() | ||
213 | } | ||
214 | |||
215 | type link string | ||
216 | type category string | ||
217 | |||
218 | // The variadic arguments may start with link and category types, | ||
219 | // and must end with a format string and any arguments. | ||
220 | // It returns the new Problem. | ||
221 | func (f *file) errorf(n ast.Node, confidence float64, args ...interface{}) *Problem { | ||
222 | pos := f.fset.Position(n.Pos()) | ||
223 | if pos.Filename == "" { | ||
224 | pos.Filename = f.filename | ||
225 | } | ||
226 | return f.pkg.errorfAt(pos, confidence, args...) | ||
227 | } | ||
228 | |||
229 | func (p *pkg) errorfAt(pos token.Position, confidence float64, args ...interface{}) *Problem { | ||
230 | problem := Problem{ | ||
231 | Position: pos, | ||
232 | Confidence: confidence, | ||
233 | } | ||
234 | if pos.Filename != "" { | ||
235 | // The file might not exist in our mapping if a //line directive was encountered. | ||
236 | if f, ok := p.files[pos.Filename]; ok { | ||
237 | problem.LineText = srcLine(f.src, pos) | ||
238 | } | ||
239 | } | ||
240 | |||
241 | argLoop: | ||
242 | for len(args) > 1 { // always leave at least the format string in args | ||
243 | switch v := args[0].(type) { | ||
244 | case link: | ||
245 | problem.Link = string(v) | ||
246 | case category: | ||
247 | problem.Category = string(v) | ||
248 | default: | ||
249 | break argLoop | ||
250 | } | ||
251 | args = args[1:] | ||
252 | } | ||
253 | |||
254 | problem.Text = fmt.Sprintf(args[0].(string), args[1:]...) | ||
255 | |||
256 | p.problems = append(p.problems, problem) | ||
257 | return &p.problems[len(p.problems)-1] | ||
258 | } | ||
259 | |||
260 | var newImporter = func(fset *token.FileSet) types.ImporterFrom { | ||
261 | return gcexportdata.NewImporter(fset, make(map[string]*types.Package)) | ||
262 | } | ||
263 | |||
264 | func (p *pkg) typeCheck() error { | ||
265 | config := &types.Config{ | ||
266 | // By setting a no-op error reporter, the type checker does as much work as possible. | ||
267 | Error: func(error) {}, | ||
268 | Importer: newImporter(p.fset), | ||
269 | } | ||
270 | info := &types.Info{ | ||
271 | Types: make(map[ast.Expr]types.TypeAndValue), | ||
272 | Defs: make(map[*ast.Ident]types.Object), | ||
273 | Uses: make(map[*ast.Ident]types.Object), | ||
274 | Scopes: make(map[ast.Node]*types.Scope), | ||
275 | } | ||
276 | var anyFile *file | ||
277 | var astFiles []*ast.File | ||
278 | for _, f := range p.files { | ||
279 | anyFile = f | ||
280 | astFiles = append(astFiles, f.f) | ||
281 | } | ||
282 | pkg, err := config.Check(anyFile.f.Name.Name, p.fset, astFiles, info) | ||
283 | // Remember the typechecking info, even if config.Check failed, | ||
284 | // since we will get partial information. | ||
285 | p.typesPkg = pkg | ||
286 | p.typesInfo = info | ||
287 | return err | ||
288 | } | ||
289 | |||
290 | func (p *pkg) typeOf(expr ast.Expr) types.Type { | ||
291 | if p.typesInfo == nil { | ||
292 | return nil | ||
293 | } | ||
294 | return p.typesInfo.TypeOf(expr) | ||
295 | } | ||
296 | |||
297 | func (p *pkg) isNamedType(typ types.Type, importPath, name string) bool { | ||
298 | n, ok := typ.(*types.Named) | ||
299 | if !ok { | ||
300 | return false | ||
301 | } | ||
302 | tn := n.Obj() | ||
303 | return tn != nil && tn.Pkg() != nil && tn.Pkg().Path() == importPath && tn.Name() == name | ||
304 | } | ||
305 | |||
306 | // scopeOf returns the tightest scope encompassing id. | ||
307 | func (p *pkg) scopeOf(id *ast.Ident) *types.Scope { | ||
308 | var scope *types.Scope | ||
309 | if obj := p.typesInfo.ObjectOf(id); obj != nil { | ||
310 | scope = obj.Parent() | ||
311 | } | ||
312 | if scope == p.typesPkg.Scope() { | ||
313 | // We were given a top-level identifier. | ||
314 | // Use the file-level scope instead of the package-level scope. | ||
315 | pos := id.Pos() | ||
316 | for _, f := range p.files { | ||
317 | if f.f.Pos() <= pos && pos < f.f.End() { | ||
318 | scope = p.typesInfo.Scopes[f.f] | ||
319 | break | ||
320 | } | ||
321 | } | ||
322 | } | ||
323 | return scope | ||
324 | } | ||
325 | |||
326 | func (p *pkg) scanSortable() { | ||
327 | p.sortable = make(map[string]bool) | ||
328 | |||
329 | // bitfield for which methods exist on each type. | ||
330 | const ( | ||
331 | Len = 1 << iota | ||
332 | Less | ||
333 | Swap | ||
334 | ) | ||
335 | nmap := map[string]int{"Len": Len, "Less": Less, "Swap": Swap} | ||
336 | has := make(map[string]int) | ||
337 | for _, f := range p.files { | ||
338 | f.walk(func(n ast.Node) bool { | ||
339 | fn, ok := n.(*ast.FuncDecl) | ||
340 | if !ok || fn.Recv == nil || len(fn.Recv.List) == 0 { | ||
341 | return true | ||
342 | } | ||
343 | // TODO(dsymonds): We could check the signature to be more precise. | ||
344 | recv := receiverType(fn) | ||
345 | if i, ok := nmap[fn.Name.Name]; ok { | ||
346 | has[recv] |= i | ||
347 | } | ||
348 | return false | ||
349 | }) | ||
350 | } | ||
351 | for typ, ms := range has { | ||
352 | if ms == Len|Less|Swap { | ||
353 | p.sortable[typ] = true | ||
354 | } | ||
355 | } | ||
356 | } | ||
357 | |||
358 | func (p *pkg) isMain() bool { | ||
359 | for _, f := range p.files { | ||
360 | if f.isMain() { | ||
361 | return true | ||
362 | } | ||
363 | } | ||
364 | return false | ||
365 | } | ||
366 | |||
367 | func (f *file) isMain() bool { | ||
368 | if f.f.Name.Name == "main" { | ||
369 | return true | ||
370 | } | ||
371 | return false | ||
372 | } | ||
373 | |||
374 | // lintPackageComment checks package comments. It complains if | ||
375 | // there is no package comment, or if it is not of the right form. | ||
376 | // This has a notable false positive in that a package comment | ||
377 | // could rightfully appear in a different file of the same package, | ||
378 | // but that's not easy to fix since this linter is file-oriented. | ||
379 | func (f *file) lintPackageComment() { | ||
380 | if f.isTest() { | ||
381 | return | ||
382 | } | ||
383 | |||
384 | const ref = styleGuideBase + "#package-comments" | ||
385 | prefix := "Package " + f.f.Name.Name + " " | ||
386 | |||
387 | // Look for a detached package comment. | ||
388 | // First, scan for the last comment that occurs before the "package" keyword. | ||
389 | var lastCG *ast.CommentGroup | ||
390 | for _, cg := range f.f.Comments { | ||
391 | if cg.Pos() > f.f.Package { | ||
392 | // Gone past "package" keyword. | ||
393 | break | ||
394 | } | ||
395 | lastCG = cg | ||
396 | } | ||
397 | if lastCG != nil && strings.HasPrefix(lastCG.Text(), prefix) { | ||
398 | endPos := f.fset.Position(lastCG.End()) | ||
399 | pkgPos := f.fset.Position(f.f.Package) | ||
400 | if endPos.Line+1 < pkgPos.Line { | ||
401 | // There isn't a great place to anchor this error; | ||
402 | // the start of the blank lines between the doc and the package statement | ||
403 | // is at least pointing at the location of the problem. | ||
404 | pos := token.Position{ | ||
405 | Filename: endPos.Filename, | ||
406 | // Offset not set; it is non-trivial, and doesn't appear to be needed. | ||
407 | Line: endPos.Line + 1, | ||
408 | Column: 1, | ||
409 | } | ||
410 | f.pkg.errorfAt(pos, 0.9, link(ref), category("comments"), "package comment is detached; there should be no blank lines between it and the package statement") | ||
411 | return | ||
412 | } | ||
413 | } | ||
414 | |||
415 | if f.f.Doc == nil { | ||
416 | f.errorf(f.f, 0.2, link(ref), category("comments"), "should have a package comment, unless it's in another file for this package") | ||
417 | return | ||
418 | } | ||
419 | s := f.f.Doc.Text() | ||
420 | if ts := strings.TrimLeft(s, " \t"); ts != s { | ||
421 | f.errorf(f.f.Doc, 1, link(ref), category("comments"), "package comment should not have leading space") | ||
422 | s = ts | ||
423 | } | ||
424 | // Only non-main packages need to keep to this form. | ||
425 | if !f.pkg.main && !strings.HasPrefix(s, prefix) { | ||
426 | f.errorf(f.f.Doc, 1, link(ref), category("comments"), `package comment should be of the form "%s..."`, prefix) | ||
427 | } | ||
428 | } | ||
429 | |||
430 | // lintBlankImports complains if a non-main package has blank imports that are | ||
431 | // not documented. | ||
432 | func (f *file) lintBlankImports() { | ||
433 | // In package main and in tests, we don't complain about blank imports. | ||
434 | if f.pkg.main || f.isTest() { | ||
435 | return | ||
436 | } | ||
437 | |||
438 | // The first element of each contiguous group of blank imports should have | ||
439 | // an explanatory comment of some kind. | ||
440 | for i, imp := range f.f.Imports { | ||
441 | pos := f.fset.Position(imp.Pos()) | ||
442 | |||
443 | if !isBlank(imp.Name) { | ||
444 | continue // Ignore non-blank imports. | ||
445 | } | ||
446 | if i > 0 { | ||
447 | prev := f.f.Imports[i-1] | ||
448 | prevPos := f.fset.Position(prev.Pos()) | ||
449 | if isBlank(prev.Name) && prevPos.Line+1 == pos.Line { | ||
450 | continue // A subsequent blank in a group. | ||
451 | } | ||
452 | } | ||
453 | |||
454 | // This is the first blank import of a group. | ||
455 | if imp.Doc == nil && imp.Comment == nil { | ||
456 | ref := "" | ||
457 | f.errorf(imp, 1, link(ref), category("imports"), "a blank import should be only in a main or test package, or have a comment justifying it") | ||
458 | } | ||
459 | } | ||
460 | } | ||
461 | |||
462 | // lintImports examines import blocks. | ||
463 | func (f *file) lintImports() { | ||
464 | for i, is := range f.f.Imports { | ||
465 | _ = i | ||
466 | if is.Name != nil && is.Name.Name == "." && !f.isTest() { | ||
467 | f.errorf(is, 1, link(styleGuideBase+"#import-dot"), category("imports"), "should not use dot imports") | ||
468 | } | ||
469 | |||
470 | } | ||
471 | } | ||
472 | |||
473 | const docCommentsLink = styleGuideBase + "#doc-comments" | ||
474 | |||
475 | // lintExported examines the exported names. | ||
476 | // It complains if any required doc comments are missing, | ||
477 | // or if they are not of the right form. The exact rules are in | ||
478 | // lintFuncDoc, lintTypeDoc and lintValueSpecDoc; this function | ||
479 | // also tracks the GenDecl structure being traversed to permit | ||
480 | // doc comments for constants to be on top of the const block. | ||
481 | // It also complains if the names stutter when combined with | ||
482 | // the package name. | ||
483 | func (f *file) lintExported() { | ||
484 | if f.isTest() { | ||
485 | return | ||
486 | } | ||
487 | |||
488 | var lastGen *ast.GenDecl // last GenDecl entered. | ||
489 | |||
490 | // Set of GenDecls that have already had missing comments flagged. | ||
491 | genDeclMissingComments := make(map[*ast.GenDecl]bool) | ||
492 | |||
493 | f.walk(func(node ast.Node) bool { | ||
494 | switch v := node.(type) { | ||
495 | case *ast.GenDecl: | ||
496 | if v.Tok == token.IMPORT { | ||
497 | return false | ||
498 | } | ||
499 | // token.CONST, token.TYPE or token.VAR | ||
500 | lastGen = v | ||
501 | return true | ||
502 | case *ast.FuncDecl: | ||
503 | f.lintFuncDoc(v) | ||
504 | if v.Recv == nil { | ||
505 | // Only check for stutter on functions, not methods. | ||
506 | // Method names are not used package-qualified. | ||
507 | f.checkStutter(v.Name, "func") | ||
508 | } | ||
509 | // Don't proceed inside funcs. | ||
510 | return false | ||
511 | case *ast.TypeSpec: | ||
512 | // inside a GenDecl, which usually has the doc | ||
513 | doc := v.Doc | ||
514 | if doc == nil { | ||
515 | doc = lastGen.Doc | ||
516 | } | ||
517 | f.lintTypeDoc(v, doc) | ||
518 | f.checkStutter(v.Name, "type") | ||
519 | // Don't proceed inside types. | ||
520 | return false | ||
521 | case *ast.ValueSpec: | ||
522 | f.lintValueSpecDoc(v, lastGen, genDeclMissingComments) | ||
523 | return false | ||
524 | } | ||
525 | return true | ||
526 | }) | ||
527 | } | ||
528 | |||
529 | var ( | ||
530 | allCapsRE = regexp.MustCompile(`^[A-Z0-9_]+$`) | ||
531 | anyCapsRE = regexp.MustCompile(`[A-Z]`) | ||
532 | ) | ||
533 | |||
534 | // knownNameExceptions is a set of names that are known to be exempt from naming checks. | ||
535 | // This is usually because they are constrained by having to match names in the | ||
536 | // standard library. | ||
537 | var knownNameExceptions = map[string]bool{ | ||
538 | "LastInsertId": true, // must match database/sql | ||
539 | "kWh": true, | ||
540 | } | ||
541 | |||
542 | func isInTopLevel(f *ast.File, ident *ast.Ident) bool { | ||
543 | path, _ := astutil.PathEnclosingInterval(f, ident.Pos(), ident.End()) | ||
544 | for _, f := range path { | ||
545 | switch f.(type) { | ||
546 | case *ast.File, *ast.GenDecl, *ast.ValueSpec, *ast.Ident: | ||
547 | continue | ||
548 | } | ||
549 | return false | ||
550 | } | ||
551 | return true | ||
552 | } | ||
553 | |||
554 | // lintNames examines all names in the file. | ||
555 | // It complains if any use underscores or incorrect known initialisms. | ||
556 | func (f *file) lintNames() { | ||
557 | // Package names need slightly different handling than other names. | ||
558 | if strings.Contains(f.f.Name.Name, "_") && !strings.HasSuffix(f.f.Name.Name, "_test") { | ||
559 | f.errorf(f.f, 1, link("http://golang.org/doc/effective_go.html#package-names"), category("naming"), "don't use an underscore in package name") | ||
560 | } | ||
561 | if anyCapsRE.MatchString(f.f.Name.Name) { | ||
562 | f.errorf(f.f, 1, link("http://golang.org/doc/effective_go.html#package-names"), category("mixed-caps"), "don't use MixedCaps in package name; %s should be %s", f.f.Name.Name, strings.ToLower(f.f.Name.Name)) | ||
563 | } | ||
564 | |||
565 | check := func(id *ast.Ident, thing string) { | ||
566 | if id.Name == "_" { | ||
567 | return | ||
568 | } | ||
569 | if knownNameExceptions[id.Name] { | ||
570 | return | ||
571 | } | ||
572 | |||
573 | // Handle two common styles from other languages that don't belong in Go. | ||
574 | if len(id.Name) >= 5 && allCapsRE.MatchString(id.Name) && strings.Contains(id.Name, "_") { | ||
575 | capCount := 0 | ||
576 | for _, c := range id.Name { | ||
577 | if 'A' <= c && c <= 'Z' { | ||
578 | capCount++ | ||
579 | } | ||
580 | } | ||
581 | if capCount >= 2 { | ||
582 | f.errorf(id, 0.8, link(styleGuideBase+"#mixed-caps"), category("naming"), "don't use ALL_CAPS in Go names; use CamelCase") | ||
583 | return | ||
584 | } | ||
585 | } | ||
586 | if thing == "const" || (thing == "var" && isInTopLevel(f.f, id)) { | ||
587 | if len(id.Name) > 2 && id.Name[0] == 'k' && id.Name[1] >= 'A' && id.Name[1] <= 'Z' { | ||
588 | should := string(id.Name[1]+'a'-'A') + id.Name[2:] | ||
589 | f.errorf(id, 0.8, link(styleGuideBase+"#mixed-caps"), category("naming"), "don't use leading k in Go names; %s %s should be %s", thing, id.Name, should) | ||
590 | } | ||
591 | } | ||
592 | |||
593 | should := lintName(id.Name) | ||
594 | if id.Name == should { | ||
595 | return | ||
596 | } | ||
597 | |||
598 | if len(id.Name) > 2 && strings.Contains(id.Name[1:], "_") { | ||
599 | f.errorf(id, 0.9, link("http://golang.org/doc/effective_go.html#mixed-caps"), category("naming"), "don't use underscores in Go names; %s %s should be %s", thing, id.Name, should) | ||
600 | return | ||
601 | } | ||
602 | f.errorf(id, 0.8, link(styleGuideBase+"#initialisms"), category("naming"), "%s %s should be %s", thing, id.Name, should) | ||
603 | } | ||
604 | checkList := func(fl *ast.FieldList, thing string) { | ||
605 | if fl == nil { | ||
606 | return | ||
607 | } | ||
608 | for _, f := range fl.List { | ||
609 | for _, id := range f.Names { | ||
610 | check(id, thing) | ||
611 | } | ||
612 | } | ||
613 | } | ||
614 | f.walk(func(node ast.Node) bool { | ||
615 | switch v := node.(type) { | ||
616 | case *ast.AssignStmt: | ||
617 | if v.Tok == token.ASSIGN { | ||
618 | return true | ||
619 | } | ||
620 | for _, exp := range v.Lhs { | ||
621 | if id, ok := exp.(*ast.Ident); ok { | ||
622 | check(id, "var") | ||
623 | } | ||
624 | } | ||
625 | case *ast.FuncDecl: | ||
626 | if f.isTest() && (strings.HasPrefix(v.Name.Name, "Example") || strings.HasPrefix(v.Name.Name, "Test") || strings.HasPrefix(v.Name.Name, "Benchmark")) { | ||
627 | return true | ||
628 | } | ||
629 | |||
630 | thing := "func" | ||
631 | if v.Recv != nil { | ||
632 | thing = "method" | ||
633 | } | ||
634 | |||
635 | // Exclude naming warnings for functions that are exported to C but | ||
636 | // not exported in the Go API. | ||
637 | // See https://github.com/golang/lint/issues/144. | ||
638 | if ast.IsExported(v.Name.Name) || !isCgoExported(v) { | ||
639 | check(v.Name, thing) | ||
640 | } | ||
641 | |||
642 | checkList(v.Type.Params, thing+" parameter") | ||
643 | checkList(v.Type.Results, thing+" result") | ||
644 | case *ast.GenDecl: | ||
645 | if v.Tok == token.IMPORT { | ||
646 | return true | ||
647 | } | ||
648 | var thing string | ||
649 | switch v.Tok { | ||
650 | case token.CONST: | ||
651 | thing = "const" | ||
652 | case token.TYPE: | ||
653 | thing = "type" | ||
654 | case token.VAR: | ||
655 | thing = "var" | ||
656 | } | ||
657 | for _, spec := range v.Specs { | ||
658 | switch s := spec.(type) { | ||
659 | case *ast.TypeSpec: | ||
660 | check(s.Name, thing) | ||
661 | case *ast.ValueSpec: | ||
662 | for _, id := range s.Names { | ||
663 | check(id, thing) | ||
664 | } | ||
665 | } | ||
666 | } | ||
667 | case *ast.InterfaceType: | ||
668 | // Do not check interface method names. | ||
669 | // They are often constrainted by the method names of concrete types. | ||
670 | for _, x := range v.Methods.List { | ||
671 | ft, ok := x.Type.(*ast.FuncType) | ||
672 | if !ok { // might be an embedded interface name | ||
673 | continue | ||
674 | } | ||
675 | checkList(ft.Params, "interface method parameter") | ||
676 | checkList(ft.Results, "interface method result") | ||
677 | } | ||
678 | case *ast.RangeStmt: | ||
679 | if v.Tok == token.ASSIGN { | ||
680 | return true | ||
681 | } | ||
682 | if id, ok := v.Key.(*ast.Ident); ok { | ||
683 | check(id, "range var") | ||
684 | } | ||
685 | if id, ok := v.Value.(*ast.Ident); ok { | ||
686 | check(id, "range var") | ||
687 | } | ||
688 | case *ast.StructType: | ||
689 | for _, f := range v.Fields.List { | ||
690 | for _, id := range f.Names { | ||
691 | check(id, "struct field") | ||
692 | } | ||
693 | } | ||
694 | } | ||
695 | return true | ||
696 | }) | ||
697 | } | ||
698 | |||
699 | // lintName returns a different name if it should be different. | ||
700 | func lintName(name string) (should string) { | ||
701 | // Fast path for simple cases: "_" and all lowercase. | ||
702 | if name == "_" { | ||
703 | return name | ||
704 | } | ||
705 | allLower := true | ||
706 | for _, r := range name { | ||
707 | if !unicode.IsLower(r) { | ||
708 | allLower = false | ||
709 | break | ||
710 | } | ||
711 | } | ||
712 | if allLower { | ||
713 | return name | ||
714 | } | ||
715 | |||
716 | // Split camelCase at any lower->upper transition, and split on underscores. | ||
717 | // Check each word for common initialisms. | ||
718 | runes := []rune(name) | ||
719 | w, i := 0, 0 // index of start of word, scan | ||
720 | for i+1 <= len(runes) { | ||
721 | eow := false // whether we hit the end of a word | ||
722 | if i+1 == len(runes) { | ||
723 | eow = true | ||
724 | } else if runes[i+1] == '_' { | ||
725 | // underscore; shift the remainder forward over any run of underscores | ||
726 | eow = true | ||
727 | n := 1 | ||
728 | for i+n+1 < len(runes) && runes[i+n+1] == '_' { | ||
729 | n++ | ||
730 | } | ||
731 | |||
732 | // Leave at most one underscore if the underscore is between two digits | ||
733 | if i+n+1 < len(runes) && unicode.IsDigit(runes[i]) && unicode.IsDigit(runes[i+n+1]) { | ||
734 | n-- | ||
735 | } | ||
736 | |||
737 | copy(runes[i+1:], runes[i+n+1:]) | ||
738 | runes = runes[:len(runes)-n] | ||
739 | } else if unicode.IsLower(runes[i]) && !unicode.IsLower(runes[i+1]) { | ||
740 | // lower->non-lower | ||
741 | eow = true | ||
742 | } | ||
743 | i++ | ||
744 | if !eow { | ||
745 | continue | ||
746 | } | ||
747 | |||
748 | // [w,i) is a word. | ||
749 | word := string(runes[w:i]) | ||
750 | if u := strings.ToUpper(word); commonInitialisms[u] { | ||
751 | // Keep consistent case, which is lowercase only at the start. | ||
752 | if w == 0 && unicode.IsLower(runes[w]) { | ||
753 | u = strings.ToLower(u) | ||
754 | } | ||
755 | // All the common initialisms are ASCII, | ||
756 | // so we can replace the bytes exactly. | ||
757 | copy(runes[w:], []rune(u)) | ||
758 | } else if w > 0 && strings.ToLower(word) == word { | ||
759 | // already all lowercase, and not the first word, so uppercase the first character. | ||
760 | runes[w] = unicode.ToUpper(runes[w]) | ||
761 | } | ||
762 | w = i | ||
763 | } | ||
764 | return string(runes) | ||
765 | } | ||
766 | |||
767 | // commonInitialisms is a set of common initialisms. | ||
768 | // Only add entries that are highly unlikely to be non-initialisms. | ||
769 | // For instance, "ID" is fine (Freudian code is rare), but "AND" is not. | ||
770 | var commonInitialisms = map[string]bool{ | ||
771 | "ACL": true, | ||
772 | "API": true, | ||
773 | "ASCII": true, | ||
774 | "CPU": true, | ||
775 | "CSS": true, | ||
776 | "DNS": true, | ||
777 | "EOF": true, | ||
778 | "GUID": true, | ||
779 | "HTML": true, | ||
780 | "HTTP": true, | ||
781 | "HTTPS": true, | ||
782 | "ID": true, | ||
783 | "IP": true, | ||
784 | "JSON": true, | ||
785 | "LHS": true, | ||
786 | "QPS": true, | ||
787 | "RAM": true, | ||
788 | "RHS": true, | ||
789 | "RPC": true, | ||
790 | "SLA": true, | ||
791 | "SMTP": true, | ||
792 | "SQL": true, | ||
793 | "SSH": true, | ||
794 | "TCP": true, | ||
795 | "TLS": true, | ||
796 | "TTL": true, | ||
797 | "UDP": true, | ||
798 | "UI": true, | ||
799 | "UID": true, | ||
800 | "UUID": true, | ||
801 | "URI": true, | ||
802 | "URL": true, | ||
803 | "UTF8": true, | ||
804 | "VM": true, | ||
805 | "XML": true, | ||
806 | "XMPP": true, | ||
807 | "XSRF": true, | ||
808 | "XSS": true, | ||
809 | } | ||
810 | |||
811 | // lintTypeDoc examines the doc comment on a type. | ||
812 | // It complains if they are missing from an exported type, | ||
813 | // or if they are not of the standard form. | ||
814 | func (f *file) lintTypeDoc(t *ast.TypeSpec, doc *ast.CommentGroup) { | ||
815 | if !ast.IsExported(t.Name.Name) { | ||
816 | return | ||
817 | } | ||
818 | if doc == nil { | ||
819 | f.errorf(t, 1, link(docCommentsLink), category("comments"), "exported type %v should have comment or be unexported", t.Name) | ||
820 | return | ||
821 | } | ||
822 | |||
823 | s := doc.Text() | ||
824 | articles := [...]string{"A", "An", "The"} | ||
825 | for _, a := range articles { | ||
826 | if strings.HasPrefix(s, a+" ") { | ||
827 | s = s[len(a)+1:] | ||
828 | break | ||
829 | } | ||
830 | } | ||
831 | if !strings.HasPrefix(s, t.Name.Name+" ") { | ||
832 | f.errorf(doc, 1, link(docCommentsLink), category("comments"), `comment on exported type %v should be of the form "%v ..." (with optional leading article)`, t.Name, t.Name) | ||
833 | } | ||
834 | } | ||
835 | |||
836 | var commonMethods = map[string]bool{ | ||
837 | "Error": true, | ||
838 | "Read": true, | ||
839 | "ServeHTTP": true, | ||
840 | "String": true, | ||
841 | "Write": true, | ||
842 | } | ||
843 | |||
844 | // lintFuncDoc examines doc comments on functions and methods. | ||
845 | // It complains if they are missing, or not of the right form. | ||
846 | // It has specific exclusions for well-known methods (see commonMethods above). | ||
847 | func (f *file) lintFuncDoc(fn *ast.FuncDecl) { | ||
848 | if !ast.IsExported(fn.Name.Name) { | ||
849 | // func is unexported | ||
850 | return | ||
851 | } | ||
852 | kind := "function" | ||
853 | name := fn.Name.Name | ||
854 | if fn.Recv != nil && len(fn.Recv.List) > 0 { | ||
855 | // method | ||
856 | kind = "method" | ||
857 | recv := receiverType(fn) | ||
858 | if !ast.IsExported(recv) { | ||
859 | // receiver is unexported | ||
860 | return | ||
861 | } | ||
862 | if commonMethods[name] { | ||
863 | return | ||
864 | } | ||
865 | switch name { | ||
866 | case "Len", "Less", "Swap": | ||
867 | if f.pkg.sortable[recv] { | ||
868 | return | ||
869 | } | ||
870 | } | ||
871 | name = recv + "." + name | ||
872 | } | ||
873 | if fn.Doc == nil { | ||
874 | f.errorf(fn, 1, link(docCommentsLink), category("comments"), "exported %s %s should have comment or be unexported", kind, name) | ||
875 | return | ||
876 | } | ||
877 | s := fn.Doc.Text() | ||
878 | prefix := fn.Name.Name + " " | ||
879 | if !strings.HasPrefix(s, prefix) { | ||
880 | f.errorf(fn.Doc, 1, link(docCommentsLink), category("comments"), `comment on exported %s %s should be of the form "%s..."`, kind, name, prefix) | ||
881 | } | ||
882 | } | ||
883 | |||
884 | // lintValueSpecDoc examines package-global variables and constants. | ||
885 | // It complains if they are not individually declared, | ||
886 | // or if they are not suitably documented in the right form (unless they are in a block that is commented). | ||
887 | func (f *file) lintValueSpecDoc(vs *ast.ValueSpec, gd *ast.GenDecl, genDeclMissingComments map[*ast.GenDecl]bool) { | ||
888 | kind := "var" | ||
889 | if gd.Tok == token.CONST { | ||
890 | kind = "const" | ||
891 | } | ||
892 | |||
893 | if len(vs.Names) > 1 { | ||
894 | // Check that none are exported except for the first. | ||
895 | for _, n := range vs.Names[1:] { | ||
896 | if ast.IsExported(n.Name) { | ||
897 | f.errorf(vs, 1, category("comments"), "exported %s %s should have its own declaration", kind, n.Name) | ||
898 | return | ||
899 | } | ||
900 | } | ||
901 | } | ||
902 | |||
903 | // Only one name. | ||
904 | name := vs.Names[0].Name | ||
905 | if !ast.IsExported(name) { | ||
906 | return | ||
907 | } | ||
908 | |||
909 | if vs.Doc == nil && gd.Doc == nil { | ||
910 | if genDeclMissingComments[gd] { | ||
911 | return | ||
912 | } | ||
913 | block := "" | ||
914 | if kind == "const" && gd.Lparen.IsValid() { | ||
915 | block = " (or a comment on this block)" | ||
916 | } | ||
917 | f.errorf(vs, 1, link(docCommentsLink), category("comments"), "exported %s %s should have comment%s or be unexported", kind, name, block) | ||
918 | genDeclMissingComments[gd] = true | ||
919 | return | ||
920 | } | ||
921 | // If this GenDecl has parens and a comment, we don't check its comment form. | ||
922 | if gd.Lparen.IsValid() && gd.Doc != nil { | ||
923 | return | ||
924 | } | ||
925 | // The relevant text to check will be on either vs.Doc or gd.Doc. | ||
926 | // Use vs.Doc preferentially. | ||
927 | doc := vs.Doc | ||
928 | if doc == nil { | ||
929 | doc = gd.Doc | ||
930 | } | ||
931 | prefix := name + " " | ||
932 | if !strings.HasPrefix(doc.Text(), prefix) { | ||
933 | f.errorf(doc, 1, link(docCommentsLink), category("comments"), `comment on exported %s %s should be of the form "%s..."`, kind, name, prefix) | ||
934 | } | ||
935 | } | ||
936 | |||
937 | func (f *file) checkStutter(id *ast.Ident, thing string) { | ||
938 | pkg, name := f.f.Name.Name, id.Name | ||
939 | if !ast.IsExported(name) { | ||
940 | // unexported name | ||
941 | return | ||
942 | } | ||
943 | // A name stutters if the package name is a strict prefix | ||
944 | // and the next character of the name starts a new word. | ||
945 | if len(name) <= len(pkg) { | ||
946 | // name is too short to stutter. | ||
947 | // This permits the name to be the same as the package name. | ||
948 | return | ||
949 | } | ||
950 | if !strings.EqualFold(pkg, name[:len(pkg)]) { | ||
951 | return | ||
952 | } | ||
953 | // We can assume the name is well-formed UTF-8. | ||
954 | // If the next rune after the package name is uppercase or an underscore | ||
955 | // the it's starting a new word and thus this name stutters. | ||
956 | rem := name[len(pkg):] | ||
957 | if next, _ := utf8.DecodeRuneInString(rem); next == '_' || unicode.IsUpper(next) { | ||
958 | f.errorf(id, 0.8, link(styleGuideBase+"#package-names"), category("naming"), "%s name will be used as %s.%s by other packages, and that stutters; consider calling this %s", thing, pkg, name, rem) | ||
959 | } | ||
960 | } | ||
961 | |||
962 | // zeroLiteral is a set of ast.BasicLit values that are zero values. | ||
963 | // It is not exhaustive. | ||
964 | var zeroLiteral = map[string]bool{ | ||
965 | "false": true, // bool | ||
966 | // runes | ||
967 | `'\x00'`: true, | ||
968 | `'\000'`: true, | ||
969 | // strings | ||
970 | `""`: true, | ||
971 | "``": true, | ||
972 | // numerics | ||
973 | "0": true, | ||
974 | "0.": true, | ||
975 | "0.0": true, | ||
976 | "0i": true, | ||
977 | } | ||
978 | |||
979 | // lintElses examines else blocks. It complains about any else block whose if block ends in a return. | ||
980 | func (f *file) lintElses() { | ||
981 | // We don't want to flag if { } else if { } else { } constructions. | ||
982 | // They will appear as an IfStmt whose Else field is also an IfStmt. | ||
983 | // Record such a node so we ignore it when we visit it. | ||
984 | ignore := make(map[*ast.IfStmt]bool) | ||
985 | |||
986 | f.walk(func(node ast.Node) bool { | ||
987 | ifStmt, ok := node.(*ast.IfStmt) | ||
988 | if !ok || ifStmt.Else == nil { | ||
989 | return true | ||
990 | } | ||
991 | if elseif, ok := ifStmt.Else.(*ast.IfStmt); ok { | ||
992 | ignore[elseif] = true | ||
993 | return true | ||
994 | } | ||
995 | if ignore[ifStmt] { | ||
996 | return true | ||
997 | } | ||
998 | if _, ok := ifStmt.Else.(*ast.BlockStmt); !ok { | ||
999 | // only care about elses without conditions | ||
1000 | return true | ||
1001 | } | ||
1002 | if len(ifStmt.Body.List) == 0 { | ||
1003 | return true | ||
1004 | } | ||
1005 | shortDecl := false // does the if statement have a ":=" initialization statement? | ||
1006 | if ifStmt.Init != nil { | ||
1007 | if as, ok := ifStmt.Init.(*ast.AssignStmt); ok && as.Tok == token.DEFINE { | ||
1008 | shortDecl = true | ||
1009 | } | ||
1010 | } | ||
1011 | lastStmt := ifStmt.Body.List[len(ifStmt.Body.List)-1] | ||
1012 | if _, ok := lastStmt.(*ast.ReturnStmt); ok { | ||
1013 | extra := "" | ||
1014 | if shortDecl { | ||
1015 | extra = " (move short variable declaration to its own line if necessary)" | ||
1016 | } | ||
1017 | f.errorf(ifStmt.Else, 1, link(styleGuideBase+"#indent-error-flow"), category("indent"), "if block ends with a return statement, so drop this else and outdent its block"+extra) | ||
1018 | } | ||
1019 | return true | ||
1020 | }) | ||
1021 | } | ||
1022 | |||
1023 | // lintRanges examines range clauses. It complains about redundant constructions. | ||
1024 | func (f *file) lintRanges() { | ||
1025 | f.walk(func(node ast.Node) bool { | ||
1026 | rs, ok := node.(*ast.RangeStmt) | ||
1027 | if !ok { | ||
1028 | return true | ||
1029 | } | ||
1030 | |||
1031 | if isIdent(rs.Key, "_") && (rs.Value == nil || isIdent(rs.Value, "_")) { | ||
1032 | p := f.errorf(rs.Key, 1, category("range-loop"), "should omit values from range; this loop is equivalent to `for range ...`") | ||
1033 | |||
1034 | newRS := *rs // shallow copy | ||
1035 | newRS.Value = nil | ||
1036 | newRS.Key = nil | ||
1037 | p.ReplacementLine = f.firstLineOf(&newRS, rs) | ||
1038 | |||
1039 | return true | ||
1040 | } | ||
1041 | |||
1042 | if isIdent(rs.Value, "_") { | ||
1043 | p := f.errorf(rs.Value, 1, category("range-loop"), "should omit 2nd value from range; this loop is equivalent to `for %s %s range ...`", f.render(rs.Key), rs.Tok) | ||
1044 | |||
1045 | newRS := *rs // shallow copy | ||
1046 | newRS.Value = nil | ||
1047 | p.ReplacementLine = f.firstLineOf(&newRS, rs) | ||
1048 | } | ||
1049 | |||
1050 | return true | ||
1051 | }) | ||
1052 | } | ||
1053 | |||
1054 | // lintErrorf examines errors.New and testing.Error calls. It complains if its only argument is an fmt.Sprintf invocation. | ||
1055 | func (f *file) lintErrorf() { | ||
1056 | f.walk(func(node ast.Node) bool { | ||
1057 | ce, ok := node.(*ast.CallExpr) | ||
1058 | if !ok || len(ce.Args) != 1 { | ||
1059 | return true | ||
1060 | } | ||
1061 | isErrorsNew := isPkgDot(ce.Fun, "errors", "New") | ||
1062 | var isTestingError bool | ||
1063 | se, ok := ce.Fun.(*ast.SelectorExpr) | ||
1064 | if ok && se.Sel.Name == "Error" { | ||
1065 | if typ := f.pkg.typeOf(se.X); typ != nil { | ||
1066 | isTestingError = typ.String() == "*testing.T" | ||
1067 | } | ||
1068 | } | ||
1069 | if !isErrorsNew && !isTestingError { | ||
1070 | return true | ||
1071 | } | ||
1072 | if !f.imports("errors") { | ||
1073 | return true | ||
1074 | } | ||
1075 | arg := ce.Args[0] | ||
1076 | ce, ok = arg.(*ast.CallExpr) | ||
1077 | if !ok || !isPkgDot(ce.Fun, "fmt", "Sprintf") { | ||
1078 | return true | ||
1079 | } | ||
1080 | errorfPrefix := "fmt" | ||
1081 | if isTestingError { | ||
1082 | errorfPrefix = f.render(se.X) | ||
1083 | } | ||
1084 | p := f.errorf(node, 1, category("errors"), "should replace %s(fmt.Sprintf(...)) with %s.Errorf(...)", f.render(se), errorfPrefix) | ||
1085 | |||
1086 | m := f.srcLineWithMatch(ce, `^(.*)`+f.render(se)+`\(fmt\.Sprintf\((.*)\)\)(.*)$`) | ||
1087 | if m != nil { | ||
1088 | p.ReplacementLine = m[1] + errorfPrefix + ".Errorf(" + m[2] + ")" + m[3] | ||
1089 | } | ||
1090 | |||
1091 | return true | ||
1092 | }) | ||
1093 | } | ||
1094 | |||
1095 | // lintErrors examines global error vars. It complains if they aren't named in the standard way. | ||
1096 | func (f *file) lintErrors() { | ||
1097 | for _, decl := range f.f.Decls { | ||
1098 | gd, ok := decl.(*ast.GenDecl) | ||
1099 | if !ok || gd.Tok != token.VAR { | ||
1100 | continue | ||
1101 | } | ||
1102 | for _, spec := range gd.Specs { | ||
1103 | spec := spec.(*ast.ValueSpec) | ||
1104 | if len(spec.Names) != 1 || len(spec.Values) != 1 { | ||
1105 | continue | ||
1106 | } | ||
1107 | ce, ok := spec.Values[0].(*ast.CallExpr) | ||
1108 | if !ok { | ||
1109 | continue | ||
1110 | } | ||
1111 | if !isPkgDot(ce.Fun, "errors", "New") && !isPkgDot(ce.Fun, "fmt", "Errorf") { | ||
1112 | continue | ||
1113 | } | ||
1114 | |||
1115 | id := spec.Names[0] | ||
1116 | prefix := "err" | ||
1117 | if id.IsExported() { | ||
1118 | prefix = "Err" | ||
1119 | } | ||
1120 | if !strings.HasPrefix(id.Name, prefix) { | ||
1121 | f.errorf(id, 0.9, category("naming"), "error var %s should have name of the form %sFoo", id.Name, prefix) | ||
1122 | } | ||
1123 | } | ||
1124 | } | ||
1125 | } | ||
1126 | |||
1127 | func lintErrorString(s string) (isClean bool, conf float64) { | ||
1128 | const basicConfidence = 0.8 | ||
1129 | const capConfidence = basicConfidence - 0.2 | ||
1130 | first, firstN := utf8.DecodeRuneInString(s) | ||
1131 | last, _ := utf8.DecodeLastRuneInString(s) | ||
1132 | if last == '.' || last == ':' || last == '!' || last == '\n' { | ||
1133 | return false, basicConfidence | ||
1134 | } | ||
1135 | if unicode.IsUpper(first) { | ||
1136 | // People use proper nouns and exported Go identifiers in error strings, | ||
1137 | // so decrease the confidence of warnings for capitalization. | ||
1138 | if len(s) <= firstN { | ||
1139 | return false, capConfidence | ||
1140 | } | ||
1141 | // Flag strings starting with something that doesn't look like an initialism. | ||
1142 | if second, _ := utf8.DecodeRuneInString(s[firstN:]); !unicode.IsUpper(second) { | ||
1143 | return false, capConfidence | ||
1144 | } | ||
1145 | } | ||
1146 | return true, 0 | ||
1147 | } | ||
1148 | |||
1149 | // lintErrorStrings examines error strings. | ||
1150 | // It complains if they are capitalized or end in punctuation or a newline. | ||
1151 | func (f *file) lintErrorStrings() { | ||
1152 | f.walk(func(node ast.Node) bool { | ||
1153 | ce, ok := node.(*ast.CallExpr) | ||
1154 | if !ok { | ||
1155 | return true | ||
1156 | } | ||
1157 | if !isPkgDot(ce.Fun, "errors", "New") && !isPkgDot(ce.Fun, "fmt", "Errorf") { | ||
1158 | return true | ||
1159 | } | ||
1160 | if len(ce.Args) < 1 { | ||
1161 | return true | ||
1162 | } | ||
1163 | str, ok := ce.Args[0].(*ast.BasicLit) | ||
1164 | if !ok || str.Kind != token.STRING { | ||
1165 | return true | ||
1166 | } | ||
1167 | s, _ := strconv.Unquote(str.Value) // can assume well-formed Go | ||
1168 | if s == "" { | ||
1169 | return true | ||
1170 | } | ||
1171 | clean, conf := lintErrorString(s) | ||
1172 | if clean { | ||
1173 | return true | ||
1174 | } | ||
1175 | |||
1176 | f.errorf(str, conf, link(styleGuideBase+"#error-strings"), category("errors"), | ||
1177 | "error strings should not be capitalized or end with punctuation or a newline") | ||
1178 | return true | ||
1179 | }) | ||
1180 | } | ||
1181 | |||
1182 | // lintReceiverNames examines receiver names. It complains about inconsistent | ||
1183 | // names used for the same type and names such as "this". | ||
1184 | func (f *file) lintReceiverNames() { | ||
1185 | typeReceiver := map[string]string{} | ||
1186 | f.walk(func(n ast.Node) bool { | ||
1187 | fn, ok := n.(*ast.FuncDecl) | ||
1188 | if !ok || fn.Recv == nil || len(fn.Recv.List) == 0 { | ||
1189 | return true | ||
1190 | } | ||
1191 | names := fn.Recv.List[0].Names | ||
1192 | if len(names) < 1 { | ||
1193 | return true | ||
1194 | } | ||
1195 | name := names[0].Name | ||
1196 | const ref = styleGuideBase + "#receiver-names" | ||
1197 | if name == "_" { | ||
1198 | f.errorf(n, 1, link(ref), category("naming"), `receiver name should not be an underscore, omit the name if it is unused`) | ||
1199 | return true | ||
1200 | } | ||
1201 | if name == "this" || name == "self" { | ||
1202 | f.errorf(n, 1, link(ref), category("naming"), `receiver name should be a reflection of its identity; don't use generic names such as "this" or "self"`) | ||
1203 | return true | ||
1204 | } | ||
1205 | recv := receiverType(fn) | ||
1206 | if prev, ok := typeReceiver[recv]; ok && prev != name { | ||
1207 | f.errorf(n, 1, link(ref), category("naming"), "receiver name %s should be consistent with previous receiver name %s for %s", name, prev, recv) | ||
1208 | return true | ||
1209 | } | ||
1210 | typeReceiver[recv] = name | ||
1211 | return true | ||
1212 | }) | ||
1213 | } | ||
1214 | |||
1215 | // lintIncDec examines statements that increment or decrement a variable. | ||
1216 | // It complains if they don't use x++ or x--. | ||
1217 | func (f *file) lintIncDec() { | ||
1218 | f.walk(func(n ast.Node) bool { | ||
1219 | as, ok := n.(*ast.AssignStmt) | ||
1220 | if !ok { | ||
1221 | return true | ||
1222 | } | ||
1223 | if len(as.Lhs) != 1 { | ||
1224 | return true | ||
1225 | } | ||
1226 | if !isOne(as.Rhs[0]) { | ||
1227 | return true | ||
1228 | } | ||
1229 | var suffix string | ||
1230 | switch as.Tok { | ||
1231 | case token.ADD_ASSIGN: | ||
1232 | suffix = "++" | ||
1233 | case token.SUB_ASSIGN: | ||
1234 | suffix = "--" | ||
1235 | default: | ||
1236 | return true | ||
1237 | } | ||
1238 | f.errorf(as, 0.8, category("unary-op"), "should replace %s with %s%s", f.render(as), f.render(as.Lhs[0]), suffix) | ||
1239 | return true | ||
1240 | }) | ||
1241 | } | ||
1242 | |||
1243 | // lintErrorReturn examines function declarations that return an error. | ||
1244 | // It complains if the error isn't the last parameter. | ||
1245 | func (f *file) lintErrorReturn() { | ||
1246 | f.walk(func(n ast.Node) bool { | ||
1247 | fn, ok := n.(*ast.FuncDecl) | ||
1248 | if !ok || fn.Type.Results == nil { | ||
1249 | return true | ||
1250 | } | ||
1251 | ret := fn.Type.Results.List | ||
1252 | if len(ret) <= 1 { | ||
1253 | return true | ||
1254 | } | ||
1255 | if isIdent(ret[len(ret)-1].Type, "error") { | ||
1256 | return true | ||
1257 | } | ||
1258 | // An error return parameter should be the last parameter. | ||
1259 | // Flag any error parameters found before the last. | ||
1260 | for _, r := range ret[:len(ret)-1] { | ||
1261 | if isIdent(r.Type, "error") { | ||
1262 | f.errorf(fn, 0.9, category("arg-order"), "error should be the last type when returning multiple items") | ||
1263 | break // only flag one | ||
1264 | } | ||
1265 | } | ||
1266 | return true | ||
1267 | }) | ||
1268 | } | ||
1269 | |||
1270 | // lintUnexportedReturn examines exported function declarations. | ||
1271 | // It complains if any return an unexported type. | ||
1272 | func (f *file) lintUnexportedReturn() { | ||
1273 | f.walk(func(n ast.Node) bool { | ||
1274 | fn, ok := n.(*ast.FuncDecl) | ||
1275 | if !ok { | ||
1276 | return true | ||
1277 | } | ||
1278 | if fn.Type.Results == nil { | ||
1279 | return false | ||
1280 | } | ||
1281 | if !fn.Name.IsExported() { | ||
1282 | return false | ||
1283 | } | ||
1284 | thing := "func" | ||
1285 | if fn.Recv != nil && len(fn.Recv.List) > 0 { | ||
1286 | thing = "method" | ||
1287 | if !ast.IsExported(receiverType(fn)) { | ||
1288 | // Don't report exported methods of unexported types, | ||
1289 | // such as private implementations of sort.Interface. | ||
1290 | return false | ||
1291 | } | ||
1292 | } | ||
1293 | for _, ret := range fn.Type.Results.List { | ||
1294 | typ := f.pkg.typeOf(ret.Type) | ||
1295 | if exportedType(typ) { | ||
1296 | continue | ||
1297 | } | ||
1298 | f.errorf(ret.Type, 0.8, category("unexported-type-in-api"), | ||
1299 | "exported %s %s returns unexported type %s, which can be annoying to use", | ||
1300 | thing, fn.Name.Name, typ) | ||
1301 | break // only flag one | ||
1302 | } | ||
1303 | return false | ||
1304 | }) | ||
1305 | } | ||
1306 | |||
1307 | // exportedType reports whether typ is an exported type. | ||
1308 | // It is imprecise, and will err on the side of returning true, | ||
1309 | // such as for composite types. | ||
1310 | func exportedType(typ types.Type) bool { | ||
1311 | switch T := typ.(type) { | ||
1312 | case *types.Named: | ||
1313 | // Builtin types have no package. | ||
1314 | return T.Obj().Pkg() == nil || T.Obj().Exported() | ||
1315 | case *types.Map: | ||
1316 | return exportedType(T.Key()) && exportedType(T.Elem()) | ||
1317 | case interface { | ||
1318 | Elem() types.Type | ||
1319 | }: // array, slice, pointer, chan | ||
1320 | return exportedType(T.Elem()) | ||
1321 | } | ||
1322 | // Be conservative about other types, such as struct, interface, etc. | ||
1323 | return true | ||
1324 | } | ||
1325 | |||
1326 | // timeSuffixes is a list of name suffixes that imply a time unit. | ||
1327 | // This is not an exhaustive list. | ||
1328 | var timeSuffixes = []string{ | ||
1329 | "Sec", "Secs", "Seconds", | ||
1330 | "Msec", "Msecs", | ||
1331 | "Milli", "Millis", "Milliseconds", | ||
1332 | "Usec", "Usecs", "Microseconds", | ||
1333 | "MS", "Ms", | ||
1334 | } | ||
1335 | |||
1336 | func (f *file) lintTimeNames() { | ||
1337 | f.walk(func(node ast.Node) bool { | ||
1338 | v, ok := node.(*ast.ValueSpec) | ||
1339 | if !ok { | ||
1340 | return true | ||
1341 | } | ||
1342 | for _, name := range v.Names { | ||
1343 | origTyp := f.pkg.typeOf(name) | ||
1344 | // Look for time.Duration or *time.Duration; | ||
1345 | // the latter is common when using flag.Duration. | ||
1346 | typ := origTyp | ||
1347 | if pt, ok := typ.(*types.Pointer); ok { | ||
1348 | typ = pt.Elem() | ||
1349 | } | ||
1350 | if !f.pkg.isNamedType(typ, "time", "Duration") { | ||
1351 | continue | ||
1352 | } | ||
1353 | suffix := "" | ||
1354 | for _, suf := range timeSuffixes { | ||
1355 | if strings.HasSuffix(name.Name, suf) { | ||
1356 | suffix = suf | ||
1357 | break | ||
1358 | } | ||
1359 | } | ||
1360 | if suffix == "" { | ||
1361 | continue | ||
1362 | } | ||
1363 | f.errorf(v, 0.9, category("time"), "var %s is of type %v; don't use unit-specific suffix %q", name.Name, origTyp, suffix) | ||
1364 | } | ||
1365 | return true | ||
1366 | }) | ||
1367 | } | ||
1368 | |||
1369 | // lintContextKeyTypes checks for call expressions to context.WithValue with | ||
1370 | // basic types used for the key argument. | ||
1371 | // See: https://golang.org/issue/17293 | ||
1372 | func (f *file) lintContextKeyTypes() { | ||
1373 | f.walk(func(node ast.Node) bool { | ||
1374 | switch node := node.(type) { | ||
1375 | case *ast.CallExpr: | ||
1376 | f.checkContextKeyType(node) | ||
1377 | } | ||
1378 | |||
1379 | return true | ||
1380 | }) | ||
1381 | } | ||
1382 | |||
1383 | // checkContextKeyType reports an error if the call expression calls | ||
1384 | // context.WithValue with a key argument of basic type. | ||
1385 | func (f *file) checkContextKeyType(x *ast.CallExpr) { | ||
1386 | sel, ok := x.Fun.(*ast.SelectorExpr) | ||
1387 | if !ok { | ||
1388 | return | ||
1389 | } | ||
1390 | pkg, ok := sel.X.(*ast.Ident) | ||
1391 | if !ok || pkg.Name != "context" { | ||
1392 | return | ||
1393 | } | ||
1394 | if sel.Sel.Name != "WithValue" { | ||
1395 | return | ||
1396 | } | ||
1397 | |||
1398 | // key is second argument to context.WithValue | ||
1399 | if len(x.Args) != 3 { | ||
1400 | return | ||
1401 | } | ||
1402 | key := f.pkg.typesInfo.Types[x.Args[1]] | ||
1403 | |||
1404 | if ktyp, ok := key.Type.(*types.Basic); ok && ktyp.Kind() != types.Invalid { | ||
1405 | f.errorf(x, 1.0, category("context"), fmt.Sprintf("should not use basic type %s as key in context.WithValue", key.Type)) | ||
1406 | } | ||
1407 | } | ||
1408 | |||
1409 | // lintContextArgs examines function declarations that contain an | ||
1410 | // argument with a type of context.Context | ||
1411 | // It complains if that argument isn't the first parameter. | ||
1412 | func (f *file) lintContextArgs() { | ||
1413 | f.walk(func(n ast.Node) bool { | ||
1414 | fn, ok := n.(*ast.FuncDecl) | ||
1415 | if !ok || len(fn.Type.Params.List) <= 1 { | ||
1416 | return true | ||
1417 | } | ||
1418 | // A context.Context should be the first parameter of a function. | ||
1419 | // Flag any that show up after the first. | ||
1420 | for _, arg := range fn.Type.Params.List[1:] { | ||
1421 | if isPkgDot(arg.Type, "context", "Context") { | ||
1422 | f.errorf(fn, 0.9, link("https://golang.org/pkg/context/"), category("arg-order"), "context.Context should be the first parameter of a function") | ||
1423 | break // only flag one | ||
1424 | } | ||
1425 | } | ||
1426 | return true | ||
1427 | }) | ||
1428 | } | ||
1429 | |||
1430 | // containsComments returns whether the interval [start, end) contains any | ||
1431 | // comments without "// MATCH " prefix. | ||
1432 | func (f *file) containsComments(start, end token.Pos) bool { | ||
1433 | for _, cgroup := range f.f.Comments { | ||
1434 | comments := cgroup.List | ||
1435 | if comments[0].Slash >= end { | ||
1436 | // All comments starting with this group are after end pos. | ||
1437 | return false | ||
1438 | } | ||
1439 | if comments[len(comments)-1].Slash < start { | ||
1440 | // Comments group ends before start pos. | ||
1441 | continue | ||
1442 | } | ||
1443 | for _, c := range comments { | ||
1444 | if start <= c.Slash && c.Slash < end && !strings.HasPrefix(c.Text, "// MATCH ") { | ||
1445 | return true | ||
1446 | } | ||
1447 | } | ||
1448 | } | ||
1449 | return false | ||
1450 | } | ||
1451 | |||
1452 | // receiverType returns the named type of the method receiver, sans "*", | ||
1453 | // or "invalid-type" if fn.Recv is ill formed. | ||
1454 | func receiverType(fn *ast.FuncDecl) string { | ||
1455 | switch e := fn.Recv.List[0].Type.(type) { | ||
1456 | case *ast.Ident: | ||
1457 | return e.Name | ||
1458 | case *ast.StarExpr: | ||
1459 | if id, ok := e.X.(*ast.Ident); ok { | ||
1460 | return id.Name | ||
1461 | } | ||
1462 | } | ||
1463 | // The parser accepts much more than just the legal forms. | ||
1464 | return "invalid-type" | ||
1465 | } | ||
1466 | |||
1467 | func (f *file) walk(fn func(ast.Node) bool) { | ||
1468 | ast.Walk(walker(fn), f.f) | ||
1469 | } | ||
1470 | |||
1471 | func (f *file) render(x interface{}) string { | ||
1472 | var buf bytes.Buffer | ||
1473 | if err := printer.Fprint(&buf, f.fset, x); err != nil { | ||
1474 | panic(err) | ||
1475 | } | ||
1476 | return buf.String() | ||
1477 | } | ||
1478 | |||
1479 | func (f *file) debugRender(x interface{}) string { | ||
1480 | var buf bytes.Buffer | ||
1481 | if err := ast.Fprint(&buf, f.fset, x, nil); err != nil { | ||
1482 | panic(err) | ||
1483 | } | ||
1484 | return buf.String() | ||
1485 | } | ||
1486 | |||
1487 | // walker adapts a function to satisfy the ast.Visitor interface. | ||
1488 | // The function return whether the walk should proceed into the node's children. | ||
1489 | type walker func(ast.Node) bool | ||
1490 | |||
1491 | func (w walker) Visit(node ast.Node) ast.Visitor { | ||
1492 | if w(node) { | ||
1493 | return w | ||
1494 | } | ||
1495 | return nil | ||
1496 | } | ||
1497 | |||
1498 | func isIdent(expr ast.Expr, ident string) bool { | ||
1499 | id, ok := expr.(*ast.Ident) | ||
1500 | return ok && id.Name == ident | ||
1501 | } | ||
1502 | |||
1503 | // isBlank returns whether id is the blank identifier "_". | ||
1504 | // If id == nil, the answer is false. | ||
1505 | func isBlank(id *ast.Ident) bool { return id != nil && id.Name == "_" } | ||
1506 | |||
1507 | func isPkgDot(expr ast.Expr, pkg, name string) bool { | ||
1508 | sel, ok := expr.(*ast.SelectorExpr) | ||
1509 | return ok && isIdent(sel.X, pkg) && isIdent(sel.Sel, name) | ||
1510 | } | ||
1511 | |||
1512 | func isOne(expr ast.Expr) bool { | ||
1513 | lit, ok := expr.(*ast.BasicLit) | ||
1514 | return ok && lit.Kind == token.INT && lit.Value == "1" | ||
1515 | } | ||
1516 | |||
1517 | func isCgoExported(f *ast.FuncDecl) bool { | ||
1518 | if f.Recv != nil || f.Doc == nil { | ||
1519 | return false | ||
1520 | } | ||
1521 | |||
1522 | cgoExport := regexp.MustCompile(fmt.Sprintf("(?m)^//export %s$", regexp.QuoteMeta(f.Name.Name))) | ||
1523 | for _, c := range f.Doc.List { | ||
1524 | if cgoExport.MatchString(c.Text) { | ||
1525 | return true | ||
1526 | } | ||
1527 | } | ||
1528 | return false | ||
1529 | } | ||
1530 | |||
1531 | var basicTypeKinds = map[types.BasicKind]string{ | ||
1532 | types.UntypedBool: "bool", | ||
1533 | types.UntypedInt: "int", | ||
1534 | types.UntypedRune: "rune", | ||
1535 | types.UntypedFloat: "float64", | ||
1536 | types.UntypedComplex: "complex128", | ||
1537 | types.UntypedString: "string", | ||
1538 | } | ||
1539 | |||
1540 | // isUntypedConst reports whether expr is an untyped constant, | ||
1541 | // and indicates what its default type is. | ||
1542 | // scope may be nil. | ||
1543 | func (f *file) isUntypedConst(expr ast.Expr) (defType string, ok bool) { | ||
1544 | // Re-evaluate expr outside of its context to see if it's untyped. | ||
1545 | // (An expr evaluated within, for example, an assignment context will get the type of the LHS.) | ||
1546 | exprStr := f.render(expr) | ||
1547 | tv, err := types.Eval(f.fset, f.pkg.typesPkg, expr.Pos(), exprStr) | ||
1548 | if err != nil { | ||
1549 | return "", false | ||
1550 | } | ||
1551 | if b, ok := tv.Type.(*types.Basic); ok { | ||
1552 | if dt, ok := basicTypeKinds[b.Kind()]; ok { | ||
1553 | return dt, true | ||
1554 | } | ||
1555 | } | ||
1556 | |||
1557 | return "", false | ||
1558 | } | ||
1559 | |||
1560 | // firstLineOf renders the given node and returns its first line. | ||
1561 | // It will also match the indentation of another node. | ||
1562 | func (f *file) firstLineOf(node, match ast.Node) string { | ||
1563 | line := f.render(node) | ||
1564 | if i := strings.Index(line, "\n"); i >= 0 { | ||
1565 | line = line[:i] | ||
1566 | } | ||
1567 | return f.indentOf(match) + line | ||
1568 | } | ||
1569 | |||
1570 | func (f *file) indentOf(node ast.Node) string { | ||
1571 | line := srcLine(f.src, f.fset.Position(node.Pos())) | ||
1572 | for i, r := range line { | ||
1573 | switch r { | ||
1574 | case ' ', '\t': | ||
1575 | default: | ||
1576 | return line[:i] | ||
1577 | } | ||
1578 | } | ||
1579 | return line // unusual or empty line | ||
1580 | } | ||
1581 | |||
1582 | func (f *file) srcLineWithMatch(node ast.Node, pattern string) (m []string) { | ||
1583 | line := srcLine(f.src, f.fset.Position(node.Pos())) | ||
1584 | line = strings.TrimSuffix(line, "\n") | ||
1585 | rx := regexp.MustCompile(pattern) | ||
1586 | return rx.FindStringSubmatch(line) | ||
1587 | } | ||
1588 | |||
1589 | // imports returns true if the current file imports the specified package path. | ||
1590 | func (f *file) imports(importPath string) bool { | ||
1591 | all := astutil.Imports(f.fset, f.f) | ||
1592 | for _, p := range all { | ||
1593 | for _, i := range p { | ||
1594 | uq, err := strconv.Unquote(i.Path.Value) | ||
1595 | if err == nil && importPath == uq { | ||
1596 | return true | ||
1597 | } | ||
1598 | } | ||
1599 | } | ||
1600 | return false | ||
1601 | } | ||
1602 | |||
1603 | // srcLine returns the complete line at p, including the terminating newline. | ||
1604 | func srcLine(src []byte, p token.Position) string { | ||
1605 | // Run to end of line in both directions if not at line start/end. | ||
1606 | lo, hi := p.Offset, p.Offset+1 | ||
1607 | for lo > 0 && src[lo-1] != '\n' { | ||
1608 | lo-- | ||
1609 | } | ||
1610 | for hi < len(src) && src[hi-1] != '\n' { | ||
1611 | hi++ | ||
1612 | } | ||
1613 | return string(src[lo:hi]) | ||
1614 | } | ||