blob: 385d96e8eded5e4124acc1f0637aa576a6b56fb1 [file] [log] [blame]
// Copyright (c) 2013 The Go Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd.
// Package lint contains a linter for Go source code.
package lint
import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"regexp"
"strings"
"unicode"
)
// A Linter lints Go source code.
type Linter struct {
}
// Problem represents a problem in some source code.
type Problem struct {
Position token.Position // position in source file
Text string // the prose that describes the problem
Confidence float64 // a value in (0,1] estimating the confidence in this problem's correctness
LineText string // the source line
}
func (p *Problem) String() string {
return p.Text
}
// Lint lints src.
func (l *Linter) Lint(filename string, src []byte) ([]Problem, error) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "", src, parser.ParseComments)
if err != nil {
return nil, err
}
return (&file{fset: fset, f: f, src: src, filename: filename}).lint(), nil
}
// file represents a file being linted.
type file struct {
fset *token.FileSet
f *ast.File
src []byte
filename string
// sortable is the set of types in the file that implement sort.Interface.
sortable map[string]bool
problems []Problem
}
func (f *file) isTest() bool { return strings.HasSuffix(f.filename, "_test.go") }
func (f *file) lint() []Problem {
f.scanSortable()
f.lintPackageComment()
f.lintImports()
f.lintExported()
f.lintNames()
f.lintVarDecls()
f.lintElses()
return f.problems
}
func (f *file) errorf(n ast.Node, confidence float64, format string, a ...interface{}) {
p := f.fset.Position(n.Pos())
f.problems = append(f.problems, Problem{
Position: p,
Text: fmt.Sprintf(format, a...),
Confidence: confidence,
LineText: srcLine(f.src, p),
})
}
func (f *file) scanSortable() {
f.sortable = make(map[string]bool)
// bitfield for which methods exist on each type.
const (
Len = 1 << iota
Less
Swap
)
nmap := map[string]int{"Len": Len, "Less": Less, "Swap": Swap}
has := make(map[string]int)
f.walk(func(n ast.Node) bool {
fn, ok := n.(*ast.FuncDecl)
if !ok || fn.Recv == nil {
return true
}
// TODO(dsymonds): We could check the signature to be more precise.
recv := receiverName(fn)
if i, ok := nmap[fn.Name.Name]; ok {
has[recv] |= i
}
return false
})
for typ, ms := range has {
if ms == Len|Less|Swap {
f.sortable[typ] = true
}
}
}
// lintPackageComment checks package comments. It complains if
// there is no package comment, or if it is not of the right form.
// This has a notable false positive in that a package comment
// could rightfully appear in a different file of the same package,
// but that's not easy to fix since this linter is file-oriented.
func (f *file) lintPackageComment() {
if f.isTest() {
return
}
if f.f.Doc == nil {
f.errorf(f.f, 0.2, "should have a package comment, unless it's in another file for this package")
return
}
s := f.f.Doc.Text()
prefix := "Package " + f.f.Name.Name + " "
if ts := strings.TrimLeft(s, " \t"); ts != s {
f.errorf(f.f.Doc, 1, "package comment should not have leading space")
s = ts
}
// Only non-main packages need to keep to this form.
if f.f.Name.Name != "main" && !strings.HasPrefix(s, prefix) {
f.errorf(f.f.Doc, 1, `package comment should be of the form "%s..."`, prefix)
}
}
// lintImports examines import blocks.
func (f *file) lintImports() {
for i, is := range f.f.Imports {
_ = i
if is.Name != nil && is.Name.Name == "." && !f.isTest() {
f.errorf(is, 1, "should not use dot imports")
}
}
}
// lintExported examines the doc comments of exported names.
// It complains if any required doc comments are missing,
// or if they are not of the right form. The exact rules are in
// lintFuncDoc, lintTypeDoc and lintValueSpecDoc; this function
// also tracks the GenDecl structure being traversed to permit
// doc comments for constants to be on top of the const block.
func (f *file) lintExported() {
if f.isTest() {
return
}
var lastGen *ast.GenDecl // last GenDecl entered.
// Set of GenDecls that have already had missing comments flagged.
genDeclMissingComments := make(map[*ast.GenDecl]bool)
f.walk(func(node ast.Node) bool {
switch v := node.(type) {
case *ast.GenDecl:
if v.Tok == token.IMPORT {
return false
}
// token.CONST, token.TYPE or token.VAR
lastGen = v
return true
case *ast.FuncDecl:
f.lintFuncDoc(v)
// Don't proceed inside funcs.
return false
case *ast.TypeSpec:
// inside a GenDecl, which usually has the doc
doc := v.Doc
if doc == nil {
doc = lastGen.Doc
}
f.lintTypeDoc(v, doc)
// Don't proceed inside types.
return false
case *ast.ValueSpec:
f.lintValueSpecDoc(v, lastGen, genDeclMissingComments)
return false
}
return true
})
}
var allCapsRE = regexp.MustCompile(`^[A-Z0-9_]+$`)
// lintNames examines all names in the file.
// It complains if any use underscores or incorrect known initialisms.
func (f *file) lintNames() {
// Package names need slightly different handling than other names.
if strings.Contains(f.f.Name.Name, "_") && !strings.HasSuffix(f.f.Name.Name, "_test") {
f.errorf(f.f, 1, "don't use an underscore in package name")
}
check := func(id *ast.Ident, thing string) {
if id.Name == "_" {
return
}
// Handle two common styles from other languages that don't belong in Go.
if len(id.Name) >= 5 && allCapsRE.MatchString(id.Name) && strings.Contains(id.Name, "_") {
f.errorf(id, 0.8, "don't use ALL_CAPS in Go names; use CamelCase")
return
}
if len(id.Name) > 2 && id.Name[0] == 'k' && id.Name[1] >= 'A' && id.Name[1] <= 'Z' {
should := string(id.Name[1]+'a'-'A') + id.Name[2:]
f.errorf(id, 0.8, "don't use leading k in Go names; %s %s should be %s", thing, id.Name, should)
}
should := lintName(id.Name)
if id.Name == should {
return
}
if strings.Contains(id.Name, "_") {
f.errorf(id, 0.9, "don't use underscores in Go names; %s %s should be %s", thing, id.Name, should)
return
}
f.errorf(id, 0.8, "%s %s should be %s", thing, id.Name, should)
}
f.walk(func(node ast.Node) bool {
switch v := node.(type) {
case *ast.AssignStmt:
for _, exp := range v.Lhs {
if id, ok := exp.(*ast.Ident); ok {
check(id, "var")
}
}
case *ast.FuncDecl:
if f.isTest() && strings.HasPrefix(v.Name.Name, "Example") {
return true
}
check(v.Name, "func")
case *ast.GenDecl:
if v.Tok == token.IMPORT {
return true
}
var thing string
switch v.Tok {
case token.CONST:
thing = "const"
case token.TYPE:
thing = "type"
case token.VAR:
thing = "var"
}
for _, spec := range v.Specs {
switch s := spec.(type) {
case *ast.TypeSpec:
check(s.Name, thing)
case *ast.ValueSpec:
for _, id := range s.Names {
check(id, thing)
}
}
}
case *ast.StructType:
for _, f := range v.Fields.List {
for _, id := range f.Names {
check(id, "struct field")
}
}
}
return true
})
}
// lintName returns a different name if it should be different.
func lintName(name string) (should string) {
// Fast path for simple cases: "_" and all lowercase.
if name == "_" {
return name
}
allLower := true
for _, r := range name {
if !unicode.IsLower(r) {
allLower = false
break
}
}
if allLower {
return name
}
// Split camelCase at any lower->upper transition, and split on underscores.
// Check each word for common initialisms.
runes := []rune(name)
w, i := 0, 0 // index of start of word, scan
for i+1 <= len(runes) {
eow := false // whether we hit the end of a word
if i+1 == len(runes) {
eow = true
} else if runes[i+1] == '_' {
// underscore; shift the remainder forward over any run of underscores
eow = true
n := 1
for i+n+1 < len(runes) && runes[i+n+1] == '_' {
n++
}
copy(runes[i+1:], runes[i+n+1:])
runes = runes[:len(runes)-n]
} else if unicode.IsLower(runes[i]) && unicode.IsUpper(runes[i+1]) {
// lower->upper
eow = true
}
i++
if !eow {
continue
}
// [w,i) is a word.
word := string(runes[w:i])
if u := strings.ToUpper(word); commonInitialisms[u] {
// Keep consistent case, which is lowercase only at the start.
if w == 0 && unicode.IsLower(runes[w]) {
u = strings.ToLower(u)
}
// All the common initialisms are ASCII,
// so we can replace the bytes exactly.
copy(runes[w:], []rune(u))
} else if w > 0 && strings.ToLower(word) == word {
// already all lowercase, and not the first word, so uppercase the first character.
runes[w] = unicode.ToUpper(runes[w])
}
w = i
}
return string(runes)
}
// commonInitialisms is a set of common initialisms.
// Only add entries that are highly unlikely to be non-initialisms.
// For instance, "ID" is fine (Freudian code is rare), but "AND" is not.
var commonInitialisms = map[string]bool{
"ASCII": true,
"API": true,
"EOF": true,
"HTML": true,
"HTTP": true,
"ID": true,
"IP": true,
"JSON": true,
"RPC": true,
"URL": true,
"XML": true,
}
// lintTypeDoc examines the doc comment on a type.
// It complains if they are missing from an exported type,
// or if they are not of the standard form.
func (f *file) lintTypeDoc(t *ast.TypeSpec, doc *ast.CommentGroup) {
if !ast.IsExported(t.Name.Name) {
return
}
if doc == nil {
f.errorf(t, 1, "exported type %v should have comment", t.Name)
return
}
s := doc.Text()
articles := [...]string{"A", "An", "The"}
for _, a := range articles {
if strings.HasPrefix(s, a+" ") {
s = s[len(a)+1:]
break
}
}
if !strings.HasPrefix(s, t.Name.Name+" ") {
f.errorf(doc, 1, `comment on exported type %v should be of the form "%v ..." (with optional leading article)`, t.Name, t.Name)
}
}
var commonMethods = map[string]bool{
"Error": true,
"Read": true,
"ServeHTTP": true,
"String": true,
"Write": true,
}
// lintFuncDoc examines doc comments on functions and methods.
// It complains if they are missing, or not of the right form.
// It has specific exclusions for well-known methods (see commonMethods above).
func (f *file) lintFuncDoc(fn *ast.FuncDecl) {
if !ast.IsExported(fn.Name.Name) {
// func is unexported
return
}
kind := "function"
name := fn.Name.Name
if fn.Recv != nil {
// method
kind = "method"
recv := receiverName(fn)
if !ast.IsExported(recv) {
// receiver is unexported
return
}
if commonMethods[name] {
return
}
switch name {
case "Len", "Less", "Swap":
if f.sortable[recv] {
return
}
}
name = recv + "." + name
}
if fn.Doc == nil {
f.errorf(fn, 1, "exported %s %s should have comment", kind, name)
return
}
s := fn.Doc.Text()
prefix := fn.Name.Name + " "
if !strings.HasPrefix(s, prefix) {
f.errorf(fn.Doc, 1, `comment on exported %s %s should be of the form "%s..."`, kind, name, prefix)
}
}
// lintValueSpecDoc examines package-global variables and constants.
// It complains if they are not individually declared,
// or if they are not suitably documented in the right form (unless they are in a block that is commented).
func (f *file) lintValueSpecDoc(vs *ast.ValueSpec, gd *ast.GenDecl, genDeclMissingComments map[*ast.GenDecl]bool) {
kind := "var"
if gd.Tok == token.CONST {
kind = "const"
}
if len(vs.Names) > 1 {
// Check that none are exported.
for _, n := range vs.Names {
if ast.IsExported(n.Name) {
f.errorf(vs, 1, "exported %s %s should have its own declaration", kind, n.Name)
return
}
}
}
// Only one name.
name := vs.Names[0].Name
if !ast.IsExported(name) {
return
}
if vs.Doc == nil {
if gd.Doc == nil && !genDeclMissingComments[gd] {
f.errorf(vs, 1, "exported %s %s should have comment", kind, name)
genDeclMissingComments[gd] = true
}
return
}
prefix := name + " "
if !strings.HasPrefix(vs.Doc.Text(), prefix) {
f.errorf(vs.Doc, 1, `comment on exported %s %s should be of the form "%s..."`, kind, name, prefix)
}
}
// zeroLiteral is a set of ast.BasicLit values that are zero values.
// It is not exhaustive.
var zeroLiteral = map[string]bool{
"false": true, // bool
// runes
`'\x00'`: true,
`'\000'`: true,
// strings
`""`: true,
"``": true,
// numerics
"0": true,
"0.": true,
"0.0": true,
"0i": true,
}
// lintVarDecls examines variable declarations. It complains about declarations with
// redundant LHS types that can be inferred from the RHS.
func (f *file) lintVarDecls() {
var lastGen *ast.GenDecl // last GenDecl entered.
f.walk(func(node ast.Node) bool {
switch v := node.(type) {
case *ast.GenDecl:
if v.Tok != token.CONST && v.Tok != token.VAR {
return false
}
lastGen = v
return true
case *ast.ValueSpec:
if lastGen.Tok == token.CONST {
return false
}
if len(v.Names) > 1 || v.Type == nil || len(v.Values) == 0 {
return false
}
rhs := v.Values[0]
// An underscore var appears in a common idiom for compile-time interface satisfaction,
// as in "var _ Interface = (*Concrete)(nil)".
if isIdent(v.Names[0], "_") {
return false
}
// If the RHS is a zero value, suggest dropping it.
zero := false
if lit, ok := rhs.(*ast.BasicLit); ok {
zero = zeroLiteral[lit.Value]
} else if isIdent(rhs, "nil") {
zero = true
}
if zero {
f.errorf(rhs, 0.9, "should drop = %s from declaration of var %s; it is the zero value", f.render(rhs), v.Names[0])
return false
}
// If the LHS type is an interface, don't warn, since it is probably a
// concrete type on the RHS. Note that our feeble lexical check here
// will only pick up interface{} and other literal interface types;
// that covers most of the cases we care to exclude right now.
// TODO(dsymonds): Use typechecker to make this heuristic more accurate.
if _, ok := v.Type.(*ast.InterfaceType); ok {
return false
}
// If the RHS is an int literal, only warn if the LHS type is "int".
if lit, ok := rhs.(*ast.BasicLit); ok && lit.Kind == token.INT {
if !isIdent(v.Type, "int") {
return false
}
}
f.errorf(v.Type, 0.8, "should omit type %s from declaration of var %s; it will be inferred from the right-hand side", f.render(v.Type), v.Names[0])
return false
}
return true
})
}
// lintElses examines else blocks. It complains about any else block whose if block ends in a return.
func (f *file) lintElses() {
// We don't want to flag if { } else if { } else { } constructions.
// They will appear as an IfStmt whose Else field is also an IfStmt.
// Record such a node so we ignore it when we visit it.
ignore := make(map[*ast.IfStmt]bool)
f.walk(func(node ast.Node) bool {
ifStmt, ok := node.(*ast.IfStmt)
if !ok || ifStmt.Else == nil {
return true
}
if ignore[ifStmt] {
return true
}
if elseif, ok := ifStmt.Else.(*ast.IfStmt); ok {
ignore[elseif] = true
return true
}
if _, ok := ifStmt.Else.(*ast.BlockStmt); !ok {
// only care about elses without conditions
return true
}
if len(ifStmt.Body.List) == 0 {
return true
}
lastStmt := ifStmt.Body.List[len(ifStmt.Body.List)-1]
if _, ok := lastStmt.(*ast.ReturnStmt); ok {
f.errorf(ifStmt.Else, 1, "if block ends with a return statement, so drop this else and outdent its block")
}
return true
})
}
func receiverName(fn *ast.FuncDecl) string {
switch e := fn.Recv.List[0].Type.(type) {
case *ast.Ident:
return e.Name
case *ast.StarExpr:
return e.X.(*ast.Ident).Name
}
panic(fmt.Sprintf("unknown method receiver AST node type %T", fn.Recv.List[0].Type))
}
func (f *file) walk(fn func(ast.Node) bool) {
ast.Walk(walker(fn), f.f)
}
func (f *file) render(x interface{}) string {
var buf bytes.Buffer
if err := printer.Fprint(&buf, f.fset, x); err != nil {
panic(err)
}
return buf.String()
}
// walker adapts a function to satisfy the ast.Visitor interface.
// The function return whether the walk should proceed into the node's children.
type walker func(ast.Node) bool
func (w walker) Visit(node ast.Node) ast.Visitor {
if w(node) {
return w
}
return nil
}
func isIdent(expr ast.Expr, ident string) bool {
id, ok := expr.(*ast.Ident)
return ok && id.Name == ident
}
// srcLine returns the complete line at p, including the terminating newline.
func srcLine(src []byte, p token.Position) string {
// Run to end of line in both directions if not at line start/end.
lo, hi := p.Offset, p.Offset+1
for lo > 0 && src[lo-1] != '\n' {
lo--
}
for hi < len(src) && src[hi-1] != '\n' {
hi++
}
return string(src[lo:hi])
}