1package stylecheck // import "honnef.co/go/tools/stylecheck"
2
3import (
4	"fmt"
5	"go/ast"
6	"go/constant"
7	"go/token"
8	"go/types"
9	"sort"
10	"strconv"
11	"strings"
12	"unicode"
13	"unicode/utf8"
14
15	"honnef.co/go/tools/code"
16	"honnef.co/go/tools/config"
17	"honnef.co/go/tools/edit"
18	"honnef.co/go/tools/internal/passes/buildir"
19	"honnef.co/go/tools/ir"
20	. "honnef.co/go/tools/lint/lintdsl"
21	"honnef.co/go/tools/pattern"
22	"honnef.co/go/tools/report"
23
24	"golang.org/x/tools/go/analysis"
25	"golang.org/x/tools/go/analysis/passes/inspect"
26	"golang.org/x/tools/go/ast/inspector"
27	"golang.org/x/tools/go/types/typeutil"
28)
29
30func CheckPackageComment(pass *analysis.Pass) (interface{}, error) {
31	// - At least one file in a non-main package should have a package comment
32	//
33	// - The comment should be of the form
34	// "Package x ...". This has a slight potential for false
35	// positives, as multiple files can have package comments, in
36	// which case they get appended. But that doesn't happen a lot in
37	// the real world.
38
39	if pass.Pkg.Name() == "main" {
40		return nil, nil
41	}
42	hasDocs := false
43	for _, f := range pass.Files {
44		if code.IsInTest(pass, f) {
45			continue
46		}
47		if f.Doc != nil && len(f.Doc.List) > 0 {
48			hasDocs = true
49			prefix := "Package " + f.Name.Name + " "
50			if !strings.HasPrefix(strings.TrimSpace(f.Doc.Text()), prefix) {
51				report.Report(pass, f.Doc, fmt.Sprintf(`package comment should be of the form "%s..."`, prefix))
52			}
53			f.Doc.Text()
54		}
55	}
56
57	if !hasDocs {
58		for _, f := range pass.Files {
59			if code.IsInTest(pass, f) {
60				continue
61			}
62			report.Report(pass, f, "at least one file in a package should have a package comment", report.ShortRange())
63		}
64	}
65	return nil, nil
66}
67
68func CheckDotImports(pass *analysis.Pass) (interface{}, error) {
69	for _, f := range pass.Files {
70	imports:
71		for _, imp := range f.Imports {
72			path := imp.Path.Value
73			path = path[1 : len(path)-1]
74			for _, w := range config.For(pass).DotImportWhitelist {
75				if w == path {
76					continue imports
77				}
78			}
79
80			if imp.Name != nil && imp.Name.Name == "." && !code.IsInTest(pass, f) {
81				report.Report(pass, imp, "should not use dot imports", report.FilterGenerated())
82			}
83		}
84	}
85	return nil, nil
86}
87
88func CheckDuplicatedImports(pass *analysis.Pass) (interface{}, error) {
89	for _, f := range pass.Files {
90		// Collect all imports by their import path
91		imports := make(map[string][]*ast.ImportSpec, len(f.Imports))
92		for _, imp := range f.Imports {
93			imports[imp.Path.Value] = append(imports[imp.Path.Value], imp)
94		}
95
96		for path, value := range imports {
97			if path[1:len(path)-1] == "unsafe" {
98				// Don't flag unsafe. Cgo generated code imports
99				// unsafe using the blank identifier, and most
100				// user-written cgo code also imports unsafe
101				// explicitly.
102				continue
103			}
104			// If there's more than one import per path, we flag that
105			if len(value) > 1 {
106				s := fmt.Sprintf("package %s is being imported more than once", path)
107				opts := []report.Option{report.FilterGenerated()}
108				for _, imp := range value[1:] {
109					opts = append(opts, report.Related(imp, fmt.Sprintf("other import of %s", path)))
110				}
111				report.Report(pass, value[0], s, opts...)
112			}
113		}
114	}
115	return nil, nil
116}
117
118func CheckBlankImports(pass *analysis.Pass) (interface{}, error) {
119	fset := pass.Fset
120	for _, f := range pass.Files {
121		if code.IsMainLike(pass) || code.IsInTest(pass, f) {
122			continue
123		}
124
125		// Collect imports of the form `import _ "foo"`, i.e. with no
126		// parentheses, as their comment will be associated with the
127		// (paren-free) GenDecl, not the import spec itself.
128		//
129		// We don't directly process the GenDecl so that we can
130		// correctly handle the following:
131		//
132		//  import _ "foo"
133		//  import _ "bar"
134		//
135		// where only the first import should get flagged.
136		skip := map[ast.Spec]bool{}
137		ast.Inspect(f, func(node ast.Node) bool {
138			switch node := node.(type) {
139			case *ast.File:
140				return true
141			case *ast.GenDecl:
142				if node.Tok != token.IMPORT {
143					return false
144				}
145				if node.Lparen == token.NoPos && node.Doc != nil {
146					skip[node.Specs[0]] = true
147				}
148				return false
149			}
150			return false
151		})
152		for i, imp := range f.Imports {
153			pos := fset.Position(imp.Pos())
154
155			if !code.IsBlank(imp.Name) {
156				continue
157			}
158			// Only flag the first blank import in a group of imports,
159			// or don't flag any of them, if the first one is
160			// commented
161			if i > 0 {
162				prev := f.Imports[i-1]
163				prevPos := fset.Position(prev.Pos())
164				if pos.Line-1 == prevPos.Line && code.IsBlank(prev.Name) {
165					continue
166				}
167			}
168
169			if imp.Doc == nil && imp.Comment == nil && !skip[imp] {
170				report.Report(pass, imp, "a blank import should be only in a main or test package, or have a comment justifying it")
171			}
172		}
173	}
174	return nil, nil
175}
176
177func CheckIncDec(pass *analysis.Pass) (interface{}, error) {
178	// TODO(dh): this can be noisy for function bodies that look like this:
179	// 	x += 3
180	// 	...
181	// 	x += 2
182	// 	...
183	// 	x += 1
184	fn := func(node ast.Node) {
185		assign := node.(*ast.AssignStmt)
186		if assign.Tok != token.ADD_ASSIGN && assign.Tok != token.SUB_ASSIGN {
187			return
188		}
189		if (len(assign.Lhs) != 1 || len(assign.Rhs) != 1) ||
190			!code.IsIntLiteral(assign.Rhs[0], "1") {
191			return
192		}
193
194		suffix := ""
195		switch assign.Tok {
196		case token.ADD_ASSIGN:
197			suffix = "++"
198		case token.SUB_ASSIGN:
199			suffix = "--"
200		}
201
202		report.Report(pass, assign, fmt.Sprintf("should replace %s with %s%s", report.Render(pass, assign), report.Render(pass, assign.Lhs[0]), suffix))
203	}
204	code.Preorder(pass, fn, (*ast.AssignStmt)(nil))
205	return nil, nil
206}
207
208func CheckErrorReturn(pass *analysis.Pass) (interface{}, error) {
209fnLoop:
210	for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
211		sig := fn.Type().(*types.Signature)
212		rets := sig.Results()
213		if rets == nil || rets.Len() < 2 {
214			continue
215		}
216
217		if rets.At(rets.Len()-1).Type() == types.Universe.Lookup("error").Type() {
218			// Last return type is error. If the function also returns
219			// errors in other positions, that's fine.
220			continue
221		}
222		for i := rets.Len() - 2; i >= 0; i-- {
223			if rets.At(i).Type() == types.Universe.Lookup("error").Type() {
224				report.Report(pass, rets.At(i), "error should be returned as the last argument", report.ShortRange())
225				continue fnLoop
226			}
227		}
228	}
229	return nil, nil
230}
231
232// CheckUnexportedReturn checks that exported functions on exported
233// types do not return unexported types.
234func CheckUnexportedReturn(pass *analysis.Pass) (interface{}, error) {
235	for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
236		if fn.Synthetic != "" || fn.Parent() != nil {
237			continue
238		}
239		if !ast.IsExported(fn.Name()) || code.IsMain(pass) || code.IsInTest(pass, fn) {
240			continue
241		}
242		sig := fn.Type().(*types.Signature)
243		if sig.Recv() != nil && !ast.IsExported(code.Dereference(sig.Recv().Type()).(*types.Named).Obj().Name()) {
244			continue
245		}
246		res := sig.Results()
247		for i := 0; i < res.Len(); i++ {
248			if named, ok := code.DereferenceR(res.At(i).Type()).(*types.Named); ok &&
249				!ast.IsExported(named.Obj().Name()) &&
250				named != types.Universe.Lookup("error").Type() {
251				report.Report(pass, fn, "should not return unexported type")
252			}
253		}
254	}
255	return nil, nil
256}
257
258func CheckReceiverNames(pass *analysis.Pass) (interface{}, error) {
259	irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg
260	for _, m := range irpkg.Members {
261		if T, ok := m.Object().(*types.TypeName); ok && !T.IsAlias() {
262			ms := typeutil.IntuitiveMethodSet(T.Type(), nil)
263			for _, sel := range ms {
264				fn := sel.Obj().(*types.Func)
265				recv := fn.Type().(*types.Signature).Recv()
266				if code.Dereference(recv.Type()) != T.Type() {
267					// skip embedded methods
268					continue
269				}
270				if recv.Name() == "self" || recv.Name() == "this" {
271					report.Report(pass, recv, `receiver name should be a reflection of its identity; don't use generic names such as "this" or "self"`, report.FilterGenerated())
272				}
273				if recv.Name() == "_" {
274					report.Report(pass, recv, "receiver name should not be an underscore, omit the name if it is unused", report.FilterGenerated())
275				}
276			}
277		}
278	}
279	return nil, nil
280}
281
282func CheckReceiverNamesIdentical(pass *analysis.Pass) (interface{}, error) {
283	irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg
284	for _, m := range irpkg.Members {
285		names := map[string]int{}
286
287		var firstFn *types.Func
288		if T, ok := m.Object().(*types.TypeName); ok && !T.IsAlias() {
289			ms := typeutil.IntuitiveMethodSet(T.Type(), nil)
290			for _, sel := range ms {
291				fn := sel.Obj().(*types.Func)
292				recv := fn.Type().(*types.Signature).Recv()
293				if code.IsGenerated(pass, recv.Pos()) {
294					// Don't concern ourselves with methods in generated code
295					continue
296				}
297				if code.Dereference(recv.Type()) != T.Type() {
298					// skip embedded methods
299					continue
300				}
301				if firstFn == nil {
302					firstFn = fn
303				}
304				if recv.Name() != "" && recv.Name() != "_" {
305					names[recv.Name()]++
306				}
307			}
308		}
309
310		if len(names) > 1 {
311			var seen []string
312			for name, count := range names {
313				seen = append(seen, fmt.Sprintf("%dx %q", count, name))
314			}
315			sort.Strings(seen)
316
317			report.Report(pass, firstFn, fmt.Sprintf("methods on the same type should have the same receiver name (seen %s)", strings.Join(seen, ", ")))
318		}
319	}
320	return nil, nil
321}
322
323func CheckContextFirstArg(pass *analysis.Pass) (interface{}, error) {
324	// TODO(dh): this check doesn't apply to test helpers. Example from the stdlib:
325	// 	func helperCommandContext(t *testing.T, ctx context.Context, s ...string) (cmd *exec.Cmd) {
326fnLoop:
327	for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
328		if fn.Synthetic != "" || fn.Parent() != nil {
329			continue
330		}
331		params := fn.Signature.Params()
332		if params.Len() < 2 {
333			continue
334		}
335		if types.TypeString(params.At(0).Type(), nil) == "context.Context" {
336			continue
337		}
338		for i := 1; i < params.Len(); i++ {
339			param := params.At(i)
340			if types.TypeString(param.Type(), nil) == "context.Context" {
341				report.Report(pass, param, "context.Context should be the first argument of a function", report.ShortRange())
342				continue fnLoop
343			}
344		}
345	}
346	return nil, nil
347}
348
349func CheckErrorStrings(pass *analysis.Pass) (interface{}, error) {
350	objNames := map[*ir.Package]map[string]bool{}
351	irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg
352	objNames[irpkg] = map[string]bool{}
353	for _, m := range irpkg.Members {
354		if typ, ok := m.(*ir.Type); ok {
355			objNames[irpkg][typ.Name()] = true
356		}
357	}
358	for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
359		objNames[fn.Package()][fn.Name()] = true
360	}
361
362	for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
363		if code.IsInTest(pass, fn) {
364			// We don't care about malformed error messages in tests;
365			// they're usually for direct human consumption, not part
366			// of an API
367			continue
368		}
369		for _, block := range fn.Blocks {
370		instrLoop:
371			for _, ins := range block.Instrs {
372				call, ok := ins.(*ir.Call)
373				if !ok {
374					continue
375				}
376				if !code.IsCallToAny(call.Common(), "errors.New", "fmt.Errorf") {
377					continue
378				}
379
380				k, ok := call.Common().Args[0].(*ir.Const)
381				if !ok {
382					continue
383				}
384
385				s := constant.StringVal(k.Value)
386				if len(s) == 0 {
387					continue
388				}
389				switch s[len(s)-1] {
390				case '.', ':', '!', '\n':
391					report.Report(pass, call, "error strings should not end with punctuation or a newline")
392				}
393				idx := strings.IndexByte(s, ' ')
394				if idx == -1 {
395					// single word error message, probably not a real
396					// error but something used in tests or during
397					// debugging
398					continue
399				}
400				word := s[:idx]
401				first, n := utf8.DecodeRuneInString(word)
402				if !unicode.IsUpper(first) {
403					continue
404				}
405				for _, c := range word[n:] {
406					if unicode.IsUpper(c) {
407						// Word is probably an initialism or
408						// multi-word function name
409						continue instrLoop
410					}
411				}
412
413				if strings.ContainsRune(word, '(') {
414					// Might be a function call
415					continue instrLoop
416				}
417				word = strings.TrimRightFunc(word, func(r rune) bool { return unicode.IsPunct(r) })
418				if objNames[fn.Package()][word] {
419					// Word is probably the name of a function or type in this package
420					continue
421				}
422				// First word in error starts with a capital
423				// letter, and the word doesn't contain any other
424				// capitals, making it unlikely to be an
425				// initialism or multi-word function name.
426				//
427				// It could still be a proper noun, though.
428
429				report.Report(pass, call, "error strings should not be capitalized")
430			}
431		}
432	}
433	return nil, nil
434}
435
436func CheckTimeNames(pass *analysis.Pass) (interface{}, error) {
437	suffixes := []string{
438		"Sec", "Secs", "Seconds",
439		"Msec", "Msecs",
440		"Milli", "Millis", "Milliseconds",
441		"Usec", "Usecs", "Microseconds",
442		"MS", "Ms",
443	}
444	fn := func(names []*ast.Ident) {
445		for _, name := range names {
446			if _, ok := pass.TypesInfo.Defs[name]; !ok {
447				continue
448			}
449			T := pass.TypesInfo.TypeOf(name)
450			if !code.IsType(T, "time.Duration") && !code.IsType(T, "*time.Duration") {
451				continue
452			}
453			for _, suffix := range suffixes {
454				if strings.HasSuffix(name.Name, suffix) {
455					report.Report(pass, name, fmt.Sprintf("var %s is of type %v; don't use unit-specific suffix %q", name.Name, T, suffix))
456					break
457				}
458			}
459		}
460	}
461
462	fn2 := func(node ast.Node) {
463		switch node := node.(type) {
464		case *ast.ValueSpec:
465			fn(node.Names)
466		case *ast.FieldList:
467			for _, field := range node.List {
468				fn(field.Names)
469			}
470		case *ast.AssignStmt:
471			if node.Tok != token.DEFINE {
472				break
473			}
474			var names []*ast.Ident
475			for _, lhs := range node.Lhs {
476				if lhs, ok := lhs.(*ast.Ident); ok {
477					names = append(names, lhs)
478				}
479			}
480			fn(names)
481		}
482	}
483
484	code.Preorder(pass, fn2, (*ast.ValueSpec)(nil), (*ast.FieldList)(nil), (*ast.AssignStmt)(nil))
485	return nil, nil
486}
487
488func CheckErrorVarNames(pass *analysis.Pass) (interface{}, error) {
489	for _, f := range pass.Files {
490		for _, decl := range f.Decls {
491			gen, ok := decl.(*ast.GenDecl)
492			if !ok || gen.Tok != token.VAR {
493				continue
494			}
495			for _, spec := range gen.Specs {
496				spec := spec.(*ast.ValueSpec)
497				if len(spec.Names) != len(spec.Values) {
498					continue
499				}
500
501				for i, name := range spec.Names {
502					val := spec.Values[i]
503					if !code.IsCallToAnyAST(pass, val, "errors.New", "fmt.Errorf") {
504						continue
505					}
506
507					if pass.Pkg.Path() == "net/http" && strings.HasPrefix(name.Name, "http2err") {
508						// special case for internal variable names of
509						// bundled HTTP 2 code in net/http
510						continue
511					}
512					prefix := "err"
513					if name.IsExported() {
514						prefix = "Err"
515					}
516					if !strings.HasPrefix(name.Name, prefix) {
517						report.Report(pass, name, fmt.Sprintf("error var %s should have name of the form %sFoo", name.Name, prefix))
518					}
519				}
520			}
521		}
522	}
523	return nil, nil
524}
525
526var httpStatusCodes = map[int]string{
527	100: "StatusContinue",
528	101: "StatusSwitchingProtocols",
529	102: "StatusProcessing",
530	200: "StatusOK",
531	201: "StatusCreated",
532	202: "StatusAccepted",
533	203: "StatusNonAuthoritativeInfo",
534	204: "StatusNoContent",
535	205: "StatusResetContent",
536	206: "StatusPartialContent",
537	207: "StatusMultiStatus",
538	208: "StatusAlreadyReported",
539	226: "StatusIMUsed",
540	300: "StatusMultipleChoices",
541	301: "StatusMovedPermanently",
542	302: "StatusFound",
543	303: "StatusSeeOther",
544	304: "StatusNotModified",
545	305: "StatusUseProxy",
546	307: "StatusTemporaryRedirect",
547	308: "StatusPermanentRedirect",
548	400: "StatusBadRequest",
549	401: "StatusUnauthorized",
550	402: "StatusPaymentRequired",
551	403: "StatusForbidden",
552	404: "StatusNotFound",
553	405: "StatusMethodNotAllowed",
554	406: "StatusNotAcceptable",
555	407: "StatusProxyAuthRequired",
556	408: "StatusRequestTimeout",
557	409: "StatusConflict",
558	410: "StatusGone",
559	411: "StatusLengthRequired",
560	412: "StatusPreconditionFailed",
561	413: "StatusRequestEntityTooLarge",
562	414: "StatusRequestURITooLong",
563	415: "StatusUnsupportedMediaType",
564	416: "StatusRequestedRangeNotSatisfiable",
565	417: "StatusExpectationFailed",
566	418: "StatusTeapot",
567	422: "StatusUnprocessableEntity",
568	423: "StatusLocked",
569	424: "StatusFailedDependency",
570	426: "StatusUpgradeRequired",
571	428: "StatusPreconditionRequired",
572	429: "StatusTooManyRequests",
573	431: "StatusRequestHeaderFieldsTooLarge",
574	451: "StatusUnavailableForLegalReasons",
575	500: "StatusInternalServerError",
576	501: "StatusNotImplemented",
577	502: "StatusBadGateway",
578	503: "StatusServiceUnavailable",
579	504: "StatusGatewayTimeout",
580	505: "StatusHTTPVersionNotSupported",
581	506: "StatusVariantAlsoNegotiates",
582	507: "StatusInsufficientStorage",
583	508: "StatusLoopDetected",
584	510: "StatusNotExtended",
585	511: "StatusNetworkAuthenticationRequired",
586}
587
588func CheckHTTPStatusCodes(pass *analysis.Pass) (interface{}, error) {
589	whitelist := map[string]bool{}
590	for _, code := range config.For(pass).HTTPStatusCodeWhitelist {
591		whitelist[code] = true
592	}
593	fn := func(node ast.Node) {
594		call := node.(*ast.CallExpr)
595
596		var arg int
597		switch code.CallNameAST(pass, call) {
598		case "net/http.Error":
599			arg = 2
600		case "net/http.Redirect":
601			arg = 3
602		case "net/http.StatusText":
603			arg = 0
604		case "net/http.RedirectHandler":
605			arg = 1
606		default:
607			return
608		}
609		lit, ok := call.Args[arg].(*ast.BasicLit)
610		if !ok {
611			return
612		}
613		if whitelist[lit.Value] {
614			return
615		}
616
617		n, err := strconv.Atoi(lit.Value)
618		if err != nil {
619			return
620		}
621		s, ok := httpStatusCodes[n]
622		if !ok {
623			return
624		}
625		report.Report(pass, lit, fmt.Sprintf("should use constant http.%s instead of numeric literal %d", s, n),
626			report.FilterGenerated(),
627			report.Fixes(edit.Fix(fmt.Sprintf("use http.%s instead of %d", s, n), edit.ReplaceWithString(pass.Fset, lit, "http."+s))))
628	}
629	code.Preorder(pass, fn, (*ast.CallExpr)(nil))
630	return nil, nil
631}
632
633func CheckDefaultCaseOrder(pass *analysis.Pass) (interface{}, error) {
634	fn := func(node ast.Node) {
635		stmt := node.(*ast.SwitchStmt)
636		list := stmt.Body.List
637		for i, c := range list {
638			if c.(*ast.CaseClause).List == nil && i != 0 && i != len(list)-1 {
639				report.Report(pass, c, "default case should be first or last in switch statement", report.FilterGenerated())
640				break
641			}
642		}
643	}
644	code.Preorder(pass, fn, (*ast.SwitchStmt)(nil))
645	return nil, nil
646}
647
648var (
649	checkYodaConditionsQ = pattern.MustParse(`(BinaryExpr left@(BasicLit _ _) tok@(Or "==" "!=") right@(Not (BasicLit _ _)))`)
650	checkYodaConditionsR = pattern.MustParse(`(BinaryExpr right tok left)`)
651)
652
653func CheckYodaConditions(pass *analysis.Pass) (interface{}, error) {
654	fn := func(node ast.Node) {
655		if _, edits, ok := MatchAndEdit(pass, checkYodaConditionsQ, checkYodaConditionsR, node); ok {
656			report.Report(pass, node, "don't use Yoda conditions",
657				report.FilterGenerated(),
658				report.Fixes(edit.Fix("un-Yoda-fy", edits...)))
659		}
660	}
661	code.Preorder(pass, fn, (*ast.BinaryExpr)(nil))
662	return nil, nil
663}
664
665func CheckInvisibleCharacters(pass *analysis.Pass) (interface{}, error) {
666	fn := func(node ast.Node) {
667		lit := node.(*ast.BasicLit)
668		if lit.Kind != token.STRING {
669			return
670		}
671
672		type invalid struct {
673			r   rune
674			off int
675		}
676		var invalids []invalid
677		hasFormat := false
678		hasControl := false
679		for off, r := range lit.Value {
680			if unicode.Is(unicode.Cf, r) {
681				invalids = append(invalids, invalid{r, off})
682				hasFormat = true
683			} else if unicode.Is(unicode.Cc, r) && r != '\n' && r != '\t' && r != '\r' {
684				invalids = append(invalids, invalid{r, off})
685				hasControl = true
686			}
687		}
688
689		switch len(invalids) {
690		case 0:
691			return
692		case 1:
693			var kind string
694			if hasFormat {
695				kind = "format"
696			} else if hasControl {
697				kind = "control"
698			} else {
699				panic("unreachable")
700			}
701
702			r := invalids[0]
703			msg := fmt.Sprintf("string literal contains the Unicode %s character %U, consider using the %q escape sequence instead", kind, r.r, r.r)
704
705			replacement := strconv.QuoteRune(r.r)
706			replacement = replacement[1 : len(replacement)-1]
707			edit := analysis.SuggestedFix{
708				Message: fmt.Sprintf("replace %s character %U with %q", kind, r.r, r.r),
709				TextEdits: []analysis.TextEdit{{
710					Pos:     lit.Pos() + token.Pos(r.off),
711					End:     lit.Pos() + token.Pos(r.off) + token.Pos(utf8.RuneLen(r.r)),
712					NewText: []byte(replacement),
713				}},
714			}
715			delete := analysis.SuggestedFix{
716				Message: fmt.Sprintf("delete %s character %U", kind, r),
717				TextEdits: []analysis.TextEdit{{
718					Pos: lit.Pos() + token.Pos(r.off),
719					End: lit.Pos() + token.Pos(r.off) + token.Pos(utf8.RuneLen(r.r)),
720				}},
721			}
722			report.Report(pass, lit, msg, report.Fixes(edit, delete))
723		default:
724			var kind string
725			if hasFormat && hasControl {
726				kind = "format and control"
727			} else if hasFormat {
728				kind = "format"
729			} else if hasControl {
730				kind = "control"
731			} else {
732				panic("unreachable")
733			}
734
735			msg := fmt.Sprintf("string literal contains Unicode %s characters, consider using escape sequences instead", kind)
736			var edits []analysis.TextEdit
737			var deletions []analysis.TextEdit
738			for _, r := range invalids {
739				replacement := strconv.QuoteRune(r.r)
740				replacement = replacement[1 : len(replacement)-1]
741				edits = append(edits, analysis.TextEdit{
742					Pos:     lit.Pos() + token.Pos(r.off),
743					End:     lit.Pos() + token.Pos(r.off) + token.Pos(utf8.RuneLen(r.r)),
744					NewText: []byte(replacement),
745				})
746				deletions = append(deletions, analysis.TextEdit{
747					Pos: lit.Pos() + token.Pos(r.off),
748					End: lit.Pos() + token.Pos(r.off) + token.Pos(utf8.RuneLen(r.r)),
749				})
750			}
751			edit := analysis.SuggestedFix{
752				Message:   fmt.Sprintf("replace all %s characters with escape sequences", kind),
753				TextEdits: edits,
754			}
755			delete := analysis.SuggestedFix{
756				Message:   fmt.Sprintf("delete all %s characters", kind),
757				TextEdits: deletions,
758			}
759			report.Report(pass, lit, msg, report.Fixes(edit, delete))
760		}
761	}
762	code.Preorder(pass, fn, (*ast.BasicLit)(nil))
763	return nil, nil
764}
765
766func CheckExportedFunctionDocs(pass *analysis.Pass) (interface{}, error) {
767	fn := func(node ast.Node) {
768		if code.IsInTest(pass, node) {
769			return
770		}
771
772		decl := node.(*ast.FuncDecl)
773		if decl.Doc == nil {
774			return
775		}
776		if !ast.IsExported(decl.Name.Name) {
777			return
778		}
779		kind := "function"
780		if decl.Recv != nil {
781			kind = "method"
782			switch T := decl.Recv.List[0].Type.(type) {
783			case *ast.StarExpr:
784				if !ast.IsExported(T.X.(*ast.Ident).Name) {
785					return
786				}
787			case *ast.Ident:
788				if !ast.IsExported(T.Name) {
789					return
790				}
791			default:
792				ExhaustiveTypeSwitch(T)
793			}
794		}
795		prefix := decl.Name.Name + " "
796		if !strings.HasPrefix(decl.Doc.Text(), prefix) {
797			report.Report(pass, decl.Doc, fmt.Sprintf(`comment on exported %s %s should be of the form "%s..."`, kind, decl.Name.Name, prefix), report.FilterGenerated())
798		}
799	}
800
801	code.Preorder(pass, fn, (*ast.FuncDecl)(nil))
802	return nil, nil
803}
804
805func CheckExportedTypeDocs(pass *analysis.Pass) (interface{}, error) {
806	var genDecl *ast.GenDecl
807	fn := func(node ast.Node, push bool) bool {
808		if !push {
809			genDecl = nil
810			return false
811		}
812		if code.IsInTest(pass, node) {
813			return false
814		}
815
816		switch node := node.(type) {
817		case *ast.GenDecl:
818			if node.Tok == token.IMPORT {
819				return false
820			}
821			genDecl = node
822			return true
823		case *ast.TypeSpec:
824			if !ast.IsExported(node.Name.Name) {
825				return false
826			}
827
828			doc := node.Doc
829			if doc == nil {
830				if len(genDecl.Specs) != 1 {
831					// more than one spec in the GenDecl, don't validate the
832					// docstring
833					return false
834				}
835				if genDecl.Lparen.IsValid() {
836					// 'type ( T )' is weird, don't guess the user's intention
837					return false
838				}
839				doc = genDecl.Doc
840				if doc == nil {
841					return false
842				}
843			}
844
845			s := doc.Text()
846			articles := [...]string{"A", "An", "The"}
847			for _, a := range articles {
848				if strings.HasPrefix(s, a+" ") {
849					s = s[len(a)+1:]
850					break
851				}
852			}
853			if !strings.HasPrefix(s, node.Name.Name+" ") {
854				report.Report(pass, doc, fmt.Sprintf(`comment on exported type %s should be of the form "%s ..." (with optional leading article)`, node.Name.Name, node.Name.Name), report.FilterGenerated())
855			}
856			return false
857		case *ast.FuncLit, *ast.FuncDecl:
858			return false
859		default:
860			ExhaustiveTypeSwitch(node)
861			return false
862		}
863	}
864
865	pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Nodes([]ast.Node{(*ast.GenDecl)(nil), (*ast.TypeSpec)(nil), (*ast.FuncLit)(nil), (*ast.FuncDecl)(nil)}, fn)
866	return nil, nil
867}
868
869func CheckExportedVarDocs(pass *analysis.Pass) (interface{}, error) {
870	var genDecl *ast.GenDecl
871	fn := func(node ast.Node, push bool) bool {
872		if !push {
873			genDecl = nil
874			return false
875		}
876		if code.IsInTest(pass, node) {
877			return false
878		}
879
880		switch node := node.(type) {
881		case *ast.GenDecl:
882			if node.Tok == token.IMPORT {
883				return false
884			}
885			genDecl = node
886			return true
887		case *ast.ValueSpec:
888			if genDecl.Lparen.IsValid() || len(node.Names) > 1 {
889				// Don't try to guess the user's intention
890				return false
891			}
892			name := node.Names[0].Name
893			if !ast.IsExported(name) {
894				return false
895			}
896			if genDecl.Doc == nil {
897				return false
898			}
899			prefix := name + " "
900			if !strings.HasPrefix(genDecl.Doc.Text(), prefix) {
901				kind := "var"
902				if genDecl.Tok == token.CONST {
903					kind = "const"
904				}
905				report.Report(pass, genDecl.Doc, fmt.Sprintf(`comment on exported %s %s should be of the form "%s..."`, kind, name, prefix), report.FilterGenerated())
906			}
907			return false
908		case *ast.FuncLit, *ast.FuncDecl:
909			return false
910		default:
911			ExhaustiveTypeSwitch(node)
912			return false
913		}
914	}
915
916	pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Nodes([]ast.Node{(*ast.GenDecl)(nil), (*ast.ValueSpec)(nil), (*ast.FuncLit)(nil), (*ast.FuncDecl)(nil)}, fn)
917	return nil, nil
918}
919