2019-12-03 08:18:49 -05:00
package stylecheck // import "honnef.co/go/tools/stylecheck"
import (
"fmt"
"go/ast"
"go/constant"
"go/token"
"go/types"
2020-05-28 05:02:09 -04:00
"sort"
2019-12-03 08:18:49 -05:00
"strconv"
"strings"
"unicode"
"unicode/utf8"
2020-05-28 05:02:09 -04:00
"honnef.co/go/tools/code"
2019-12-03 08:18:49 -05:00
"honnef.co/go/tools/config"
2020-05-28 05:02:09 -04:00
"honnef.co/go/tools/edit"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/ir"
2019-12-03 08:18:49 -05:00
. "honnef.co/go/tools/lint/lintdsl"
2020-05-28 05:02:09 -04:00
"honnef.co/go/tools/pattern"
"honnef.co/go/tools/report"
2019-12-03 08:18:49 -05:00
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil"
)
func CheckPackageComment ( pass * analysis . Pass ) ( interface { } , error ) {
// - At least one file in a non-main package should have a package comment
//
// - The comment should be of the form
// "Package x ...". This has a slight potential for false
// positives, as multiple files can have package comments, in
// which case they get appended. But that doesn't happen a lot in
// the real world.
if pass . Pkg . Name ( ) == "main" {
return nil , nil
}
hasDocs := false
for _ , f := range pass . Files {
2020-05-28 05:02:09 -04:00
if code . IsInTest ( pass , f ) {
2019-12-03 08:18:49 -05:00
continue
}
if f . Doc != nil && len ( f . Doc . List ) > 0 {
hasDocs = true
prefix := "Package " + f . Name . Name + " "
if ! strings . HasPrefix ( strings . TrimSpace ( f . Doc . Text ( ) ) , prefix ) {
2020-05-28 05:02:09 -04:00
report . Report ( pass , f . Doc , fmt . Sprintf ( ` package comment should be of the form "%s..." ` , prefix ) )
2019-12-03 08:18:49 -05:00
}
f . Doc . Text ( )
}
}
if ! hasDocs {
for _ , f := range pass . Files {
2020-05-28 05:02:09 -04:00
if code . IsInTest ( pass , f ) {
2019-12-03 08:18:49 -05:00
continue
}
2020-05-28 05:02:09 -04:00
report . Report ( pass , f , "at least one file in a package should have a package comment" , report . ShortRange ( ) )
2019-12-03 08:18:49 -05:00
}
}
return nil , nil
}
func CheckDotImports ( pass * analysis . Pass ) ( interface { } , error ) {
for _ , f := range pass . Files {
imports :
for _ , imp := range f . Imports {
path := imp . Path . Value
path = path [ 1 : len ( path ) - 1 ]
for _ , w := range config . For ( pass ) . DotImportWhitelist {
if w == path {
continue imports
}
}
2020-05-28 05:02:09 -04:00
if imp . Name != nil && imp . Name . Name == "." && ! code . IsInTest ( pass , f ) {
report . Report ( pass , imp , "should not use dot imports" , report . FilterGenerated ( ) )
}
}
}
return nil , nil
}
func CheckDuplicatedImports ( pass * analysis . Pass ) ( interface { } , error ) {
for _ , f := range pass . Files {
// Collect all imports by their import path
imports := make ( map [ string ] [ ] * ast . ImportSpec , len ( f . Imports ) )
for _ , imp := range f . Imports {
imports [ imp . Path . Value ] = append ( imports [ imp . Path . Value ] , imp )
}
for path , value := range imports {
if path [ 1 : len ( path ) - 1 ] == "unsafe" {
// Don't flag unsafe. Cgo generated code imports
// unsafe using the blank identifier, and most
// user-written cgo code also imports unsafe
// explicitly.
continue
}
// If there's more than one import per path, we flag that
if len ( value ) > 1 {
s := fmt . Sprintf ( "package %s is being imported more than once" , path )
opts := [ ] report . Option { report . FilterGenerated ( ) }
for _ , imp := range value [ 1 : ] {
opts = append ( opts , report . Related ( imp , fmt . Sprintf ( "other import of %s" , path ) ) )
}
report . Report ( pass , value [ 0 ] , s , opts ... )
2019-12-03 08:18:49 -05:00
}
}
}
return nil , nil
}
func CheckBlankImports ( pass * analysis . Pass ) ( interface { } , error ) {
fset := pass . Fset
for _ , f := range pass . Files {
2020-05-28 05:02:09 -04:00
if code . IsMainLike ( pass ) || code . IsInTest ( pass , f ) {
2019-12-03 08:18:49 -05:00
continue
}
// Collect imports of the form `import _ "foo"`, i.e. with no
// parentheses, as their comment will be associated with the
// (paren-free) GenDecl, not the import spec itself.
//
// We don't directly process the GenDecl so that we can
// correctly handle the following:
//
// import _ "foo"
// import _ "bar"
//
// where only the first import should get flagged.
skip := map [ ast . Spec ] bool { }
ast . Inspect ( f , func ( node ast . Node ) bool {
switch node := node . ( type ) {
case * ast . File :
return true
case * ast . GenDecl :
if node . Tok != token . IMPORT {
return false
}
if node . Lparen == token . NoPos && node . Doc != nil {
skip [ node . Specs [ 0 ] ] = true
}
return false
}
return false
} )
for i , imp := range f . Imports {
pos := fset . Position ( imp . Pos ( ) )
2020-05-28 05:02:09 -04:00
if ! code . IsBlank ( imp . Name ) {
2019-12-03 08:18:49 -05:00
continue
}
// Only flag the first blank import in a group of imports,
// or don't flag any of them, if the first one is
// commented
if i > 0 {
prev := f . Imports [ i - 1 ]
prevPos := fset . Position ( prev . Pos ( ) )
2020-05-28 05:02:09 -04:00
if pos . Line - 1 == prevPos . Line && code . IsBlank ( prev . Name ) {
2019-12-03 08:18:49 -05:00
continue
}
}
if imp . Doc == nil && imp . Comment == nil && ! skip [ imp ] {
2020-05-28 05:02:09 -04:00
report . Report ( pass , imp , "a blank import should be only in a main or test package, or have a comment justifying it" )
2019-12-03 08:18:49 -05:00
}
}
}
return nil , nil
}
func CheckIncDec ( pass * analysis . Pass ) ( interface { } , error ) {
// TODO(dh): this can be noisy for function bodies that look like this:
// x += 3
// ...
// x += 2
// ...
// x += 1
fn := func ( node ast . Node ) {
assign := node . ( * ast . AssignStmt )
if assign . Tok != token . ADD_ASSIGN && assign . Tok != token . SUB_ASSIGN {
return
}
if ( len ( assign . Lhs ) != 1 || len ( assign . Rhs ) != 1 ) ||
2020-05-28 05:02:09 -04:00
! code . IsIntLiteral ( assign . Rhs [ 0 ] , "1" ) {
2019-12-03 08:18:49 -05:00
return
}
suffix := ""
switch assign . Tok {
case token . ADD_ASSIGN :
suffix = "++"
case token . SUB_ASSIGN :
suffix = "--"
}
2020-05-28 05:02:09 -04:00
report . Report ( pass , assign , fmt . Sprintf ( "should replace %s with %s%s" , report . Render ( pass , assign ) , report . Render ( pass , assign . Lhs [ 0 ] ) , suffix ) )
2019-12-03 08:18:49 -05:00
}
2020-05-28 05:02:09 -04:00
code . Preorder ( pass , fn , ( * ast . AssignStmt ) ( nil ) )
2019-12-03 08:18:49 -05:00
return nil , nil
}
func CheckErrorReturn ( pass * analysis . Pass ) ( interface { } , error ) {
fnLoop :
2020-05-28 05:02:09 -04:00
for _ , fn := range pass . ResultOf [ buildir . Analyzer ] . ( * buildir . IR ) . SrcFuncs {
2019-12-03 08:18:49 -05:00
sig := fn . Type ( ) . ( * types . Signature )
rets := sig . Results ( )
if rets == nil || rets . Len ( ) < 2 {
continue
}
if rets . At ( rets . Len ( ) - 1 ) . Type ( ) == types . Universe . Lookup ( "error" ) . Type ( ) {
// Last return type is error. If the function also returns
// errors in other positions, that's fine.
continue
}
for i := rets . Len ( ) - 2 ; i >= 0 ; i -- {
if rets . At ( i ) . Type ( ) == types . Universe . Lookup ( "error" ) . Type ( ) {
2020-05-28 05:02:09 -04:00
report . Report ( pass , rets . At ( i ) , "error should be returned as the last argument" , report . ShortRange ( ) )
2019-12-03 08:18:49 -05:00
continue fnLoop
}
}
}
return nil , nil
}
// CheckUnexportedReturn checks that exported functions on exported
// types do not return unexported types.
func CheckUnexportedReturn ( pass * analysis . Pass ) ( interface { } , error ) {
2020-05-28 05:02:09 -04:00
for _ , fn := range pass . ResultOf [ buildir . Analyzer ] . ( * buildir . IR ) . SrcFuncs {
2019-12-03 08:18:49 -05:00
if fn . Synthetic != "" || fn . Parent ( ) != nil {
continue
}
2020-05-28 05:02:09 -04:00
if ! ast . IsExported ( fn . Name ( ) ) || code . IsMain ( pass ) || code . IsInTest ( pass , fn ) {
2019-12-03 08:18:49 -05:00
continue
}
sig := fn . Type ( ) . ( * types . Signature )
2020-05-28 05:02:09 -04:00
if sig . Recv ( ) != nil && ! ast . IsExported ( code . Dereference ( sig . Recv ( ) . Type ( ) ) . ( * types . Named ) . Obj ( ) . Name ( ) ) {
2019-12-03 08:18:49 -05:00
continue
}
res := sig . Results ( )
for i := 0 ; i < res . Len ( ) ; i ++ {
2020-05-28 05:02:09 -04:00
if named , ok := code . DereferenceR ( res . At ( i ) . Type ( ) ) . ( * types . Named ) ; ok &&
2019-12-03 08:18:49 -05:00
! ast . IsExported ( named . Obj ( ) . Name ( ) ) &&
named != types . Universe . Lookup ( "error" ) . Type ( ) {
2020-05-28 05:02:09 -04:00
report . Report ( pass , fn , "should not return unexported type" )
2019-12-03 08:18:49 -05:00
}
}
}
return nil , nil
}
func CheckReceiverNames ( pass * analysis . Pass ) ( interface { } , error ) {
2020-05-28 05:02:09 -04:00
irpkg := pass . ResultOf [ buildir . Analyzer ] . ( * buildir . IR ) . Pkg
for _ , m := range irpkg . Members {
2019-12-03 08:18:49 -05:00
if T , ok := m . Object ( ) . ( * types . TypeName ) ; ok && ! T . IsAlias ( ) {
ms := typeutil . IntuitiveMethodSet ( T . Type ( ) , nil )
for _ , sel := range ms {
fn := sel . Obj ( ) . ( * types . Func )
recv := fn . Type ( ) . ( * types . Signature ) . Recv ( )
2020-05-28 05:02:09 -04:00
if code . Dereference ( recv . Type ( ) ) != T . Type ( ) {
2019-12-03 08:18:49 -05:00
// skip embedded methods
continue
}
if recv . Name ( ) == "self" || recv . Name ( ) == "this" {
2020-05-28 05:02:09 -04:00
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 ( ) )
2019-12-03 08:18:49 -05:00
}
if recv . Name ( ) == "_" {
2020-05-28 05:02:09 -04:00
report . Report ( pass , recv , "receiver name should not be an underscore, omit the name if it is unused" , report . FilterGenerated ( ) )
2019-12-03 08:18:49 -05:00
}
}
}
}
return nil , nil
}
func CheckReceiverNamesIdentical ( pass * analysis . Pass ) ( interface { } , error ) {
2020-05-28 05:02:09 -04:00
irpkg := pass . ResultOf [ buildir . Analyzer ] . ( * buildir . IR ) . Pkg
for _ , m := range irpkg . Members {
2019-12-03 08:18:49 -05:00
names := map [ string ] int { }
var firstFn * types . Func
if T , ok := m . Object ( ) . ( * types . TypeName ) ; ok && ! T . IsAlias ( ) {
ms := typeutil . IntuitiveMethodSet ( T . Type ( ) , nil )
for _ , sel := range ms {
fn := sel . Obj ( ) . ( * types . Func )
recv := fn . Type ( ) . ( * types . Signature ) . Recv ( )
2020-05-28 05:02:09 -04:00
if code . IsGenerated ( pass , recv . Pos ( ) ) {
// Don't concern ourselves with methods in generated code
continue
}
if code . Dereference ( recv . Type ( ) ) != T . Type ( ) {
2019-12-03 08:18:49 -05:00
// skip embedded methods
continue
}
if firstFn == nil {
firstFn = fn
}
if recv . Name ( ) != "" && recv . Name ( ) != "_" {
names [ recv . Name ( ) ] ++
}
}
}
if len ( names ) > 1 {
var seen [ ] string
for name , count := range names {
seen = append ( seen , fmt . Sprintf ( "%dx %q" , count , name ) )
}
2020-05-28 05:02:09 -04:00
sort . Strings ( seen )
2019-12-03 08:18:49 -05:00
2020-05-28 05:02:09 -04:00
report . Report ( pass , firstFn , fmt . Sprintf ( "methods on the same type should have the same receiver name (seen %s)" , strings . Join ( seen , ", " ) ) )
2019-12-03 08:18:49 -05:00
}
}
return nil , nil
}
func CheckContextFirstArg ( pass * analysis . Pass ) ( interface { } , error ) {
// TODO(dh): this check doesn't apply to test helpers. Example from the stdlib:
// func helperCommandContext(t *testing.T, ctx context.Context, s ...string) (cmd *exec.Cmd) {
fnLoop :
2020-05-28 05:02:09 -04:00
for _ , fn := range pass . ResultOf [ buildir . Analyzer ] . ( * buildir . IR ) . SrcFuncs {
2019-12-03 08:18:49 -05:00
if fn . Synthetic != "" || fn . Parent ( ) != nil {
continue
}
params := fn . Signature . Params ( )
if params . Len ( ) < 2 {
continue
}
if types . TypeString ( params . At ( 0 ) . Type ( ) , nil ) == "context.Context" {
continue
}
for i := 1 ; i < params . Len ( ) ; i ++ {
param := params . At ( i )
if types . TypeString ( param . Type ( ) , nil ) == "context.Context" {
2020-05-28 05:02:09 -04:00
report . Report ( pass , param , "context.Context should be the first argument of a function" , report . ShortRange ( ) )
2019-12-03 08:18:49 -05:00
continue fnLoop
}
}
}
return nil , nil
}
func CheckErrorStrings ( pass * analysis . Pass ) ( interface { } , error ) {
2020-05-28 05:02:09 -04:00
objNames := map [ * ir . Package ] map [ string ] bool { }
irpkg := pass . ResultOf [ buildir . Analyzer ] . ( * buildir . IR ) . Pkg
objNames [ irpkg ] = map [ string ] bool { }
for _ , m := range irpkg . Members {
if typ , ok := m . ( * ir . Type ) ; ok {
objNames [ irpkg ] [ typ . Name ( ) ] = true
2019-12-03 08:18:49 -05:00
}
}
2020-05-28 05:02:09 -04:00
for _ , fn := range pass . ResultOf [ buildir . Analyzer ] . ( * buildir . IR ) . SrcFuncs {
2019-12-03 08:18:49 -05:00
objNames [ fn . Package ( ) ] [ fn . Name ( ) ] = true
}
2020-05-28 05:02:09 -04:00
for _ , fn := range pass . ResultOf [ buildir . Analyzer ] . ( * buildir . IR ) . SrcFuncs {
if code . IsInTest ( pass , fn ) {
2019-12-03 08:18:49 -05:00
// We don't care about malformed error messages in tests;
// they're usually for direct human consumption, not part
// of an API
continue
}
for _ , block := range fn . Blocks {
instrLoop :
for _ , ins := range block . Instrs {
2020-05-28 05:02:09 -04:00
call , ok := ins . ( * ir . Call )
2019-12-03 08:18:49 -05:00
if ! ok {
continue
}
2020-05-28 05:02:09 -04:00
if ! code . IsCallToAny ( call . Common ( ) , "errors.New" , "fmt.Errorf" ) {
2019-12-03 08:18:49 -05:00
continue
}
2020-05-28 05:02:09 -04:00
k , ok := call . Common ( ) . Args [ 0 ] . ( * ir . Const )
2019-12-03 08:18:49 -05:00
if ! ok {
continue
}
s := constant . StringVal ( k . Value )
if len ( s ) == 0 {
continue
}
switch s [ len ( s ) - 1 ] {
case '.' , ':' , '!' , '\n' :
2020-05-28 05:02:09 -04:00
report . Report ( pass , call , "error strings should not end with punctuation or a newline" )
2019-12-03 08:18:49 -05:00
}
idx := strings . IndexByte ( s , ' ' )
if idx == - 1 {
// single word error message, probably not a real
// error but something used in tests or during
// debugging
continue
}
word := s [ : idx ]
first , n := utf8 . DecodeRuneInString ( word )
if ! unicode . IsUpper ( first ) {
continue
}
for _ , c := range word [ n : ] {
if unicode . IsUpper ( c ) {
// Word is probably an initialism or
// multi-word function name
continue instrLoop
}
}
word = strings . TrimRightFunc ( word , func ( r rune ) bool { return unicode . IsPunct ( r ) } )
if objNames [ fn . Package ( ) ] [ word ] {
// Word is probably the name of a function or type in this package
continue
}
// First word in error starts with a capital
// letter, and the word doesn't contain any other
// capitals, making it unlikely to be an
// initialism or multi-word function name.
//
// It could still be a proper noun, though.
2020-05-28 05:02:09 -04:00
report . Report ( pass , call , "error strings should not be capitalized" )
2019-12-03 08:18:49 -05:00
}
}
}
return nil , nil
}
func CheckTimeNames ( pass * analysis . Pass ) ( interface { } , error ) {
suffixes := [ ] string {
"Sec" , "Secs" , "Seconds" ,
"Msec" , "Msecs" ,
"Milli" , "Millis" , "Milliseconds" ,
"Usec" , "Usecs" , "Microseconds" ,
"MS" , "Ms" ,
}
2020-05-28 05:02:09 -04:00
fn := func ( names [ ] * ast . Ident ) {
2019-12-03 08:18:49 -05:00
for _ , name := range names {
2020-05-28 05:02:09 -04:00
if _ , ok := pass . TypesInfo . Defs [ name ] ; ! ok {
continue
}
T := pass . TypesInfo . TypeOf ( name )
if ! code . IsType ( T , "time.Duration" ) && ! code . IsType ( T , "*time.Duration" ) {
continue
}
2019-12-03 08:18:49 -05:00
for _ , suffix := range suffixes {
if strings . HasSuffix ( name . Name , suffix ) {
2020-05-28 05:02:09 -04:00
report . Report ( pass , name , fmt . Sprintf ( "var %s is of type %v; don't use unit-specific suffix %q" , name . Name , T , suffix ) )
2019-12-03 08:18:49 -05:00
break
}
}
}
}
2020-05-28 05:02:09 -04:00
fn2 := func ( node ast . Node ) {
switch node := node . ( type ) {
case * ast . ValueSpec :
fn ( node . Names )
case * ast . FieldList :
for _ , field := range node . List {
fn ( field . Names )
}
case * ast . AssignStmt :
if node . Tok != token . DEFINE {
break
}
var names [ ] * ast . Ident
for _ , lhs := range node . Lhs {
if lhs , ok := lhs . ( * ast . Ident ) ; ok {
names = append ( names , lhs )
2019-12-03 08:18:49 -05:00
}
}
2020-05-28 05:02:09 -04:00
fn ( names )
}
2019-12-03 08:18:49 -05:00
}
2020-05-28 05:02:09 -04:00
code . Preorder ( pass , fn2 , ( * ast . ValueSpec ) ( nil ) , ( * ast . FieldList ) ( nil ) , ( * ast . AssignStmt ) ( nil ) )
2019-12-03 08:18:49 -05:00
return nil , nil
}
func CheckErrorVarNames ( pass * analysis . Pass ) ( interface { } , error ) {
for _ , f := range pass . Files {
for _ , decl := range f . Decls {
gen , ok := decl . ( * ast . GenDecl )
if ! ok || gen . Tok != token . VAR {
continue
}
for _ , spec := range gen . Specs {
spec := spec . ( * ast . ValueSpec )
if len ( spec . Names ) != len ( spec . Values ) {
continue
}
for i , name := range spec . Names {
val := spec . Values [ i ]
2020-05-28 05:02:09 -04:00
if ! code . IsCallToAnyAST ( pass , val , "errors.New" , "fmt.Errorf" ) {
2019-12-03 08:18:49 -05:00
continue
}
2020-05-28 05:02:09 -04:00
if pass . Pkg . Path ( ) == "net/http" && strings . HasPrefix ( name . Name , "http2err" ) {
// special case for internal variable names of
// bundled HTTP 2 code in net/http
continue
}
2019-12-03 08:18:49 -05:00
prefix := "err"
if name . IsExported ( ) {
prefix = "Err"
}
if ! strings . HasPrefix ( name . Name , prefix ) {
2020-05-28 05:02:09 -04:00
report . Report ( pass , name , fmt . Sprintf ( "error var %s should have name of the form %sFoo" , name . Name , prefix ) )
2019-12-03 08:18:49 -05:00
}
}
}
}
}
return nil , nil
}
var httpStatusCodes = map [ int ] string {
100 : "StatusContinue" ,
101 : "StatusSwitchingProtocols" ,
102 : "StatusProcessing" ,
200 : "StatusOK" ,
201 : "StatusCreated" ,
202 : "StatusAccepted" ,
203 : "StatusNonAuthoritativeInfo" ,
204 : "StatusNoContent" ,
205 : "StatusResetContent" ,
206 : "StatusPartialContent" ,
207 : "StatusMultiStatus" ,
208 : "StatusAlreadyReported" ,
226 : "StatusIMUsed" ,
300 : "StatusMultipleChoices" ,
301 : "StatusMovedPermanently" ,
302 : "StatusFound" ,
303 : "StatusSeeOther" ,
304 : "StatusNotModified" ,
305 : "StatusUseProxy" ,
307 : "StatusTemporaryRedirect" ,
308 : "StatusPermanentRedirect" ,
400 : "StatusBadRequest" ,
401 : "StatusUnauthorized" ,
402 : "StatusPaymentRequired" ,
403 : "StatusForbidden" ,
404 : "StatusNotFound" ,
405 : "StatusMethodNotAllowed" ,
406 : "StatusNotAcceptable" ,
407 : "StatusProxyAuthRequired" ,
408 : "StatusRequestTimeout" ,
409 : "StatusConflict" ,
410 : "StatusGone" ,
411 : "StatusLengthRequired" ,
412 : "StatusPreconditionFailed" ,
413 : "StatusRequestEntityTooLarge" ,
414 : "StatusRequestURITooLong" ,
415 : "StatusUnsupportedMediaType" ,
416 : "StatusRequestedRangeNotSatisfiable" ,
417 : "StatusExpectationFailed" ,
418 : "StatusTeapot" ,
422 : "StatusUnprocessableEntity" ,
423 : "StatusLocked" ,
424 : "StatusFailedDependency" ,
426 : "StatusUpgradeRequired" ,
428 : "StatusPreconditionRequired" ,
429 : "StatusTooManyRequests" ,
431 : "StatusRequestHeaderFieldsTooLarge" ,
451 : "StatusUnavailableForLegalReasons" ,
500 : "StatusInternalServerError" ,
501 : "StatusNotImplemented" ,
502 : "StatusBadGateway" ,
503 : "StatusServiceUnavailable" ,
504 : "StatusGatewayTimeout" ,
505 : "StatusHTTPVersionNotSupported" ,
506 : "StatusVariantAlsoNegotiates" ,
507 : "StatusInsufficientStorage" ,
508 : "StatusLoopDetected" ,
510 : "StatusNotExtended" ,
511 : "StatusNetworkAuthenticationRequired" ,
}
func CheckHTTPStatusCodes ( pass * analysis . Pass ) ( interface { } , error ) {
whitelist := map [ string ] bool { }
for _ , code := range config . For ( pass ) . HTTPStatusCodeWhitelist {
whitelist [ code ] = true
}
2020-05-28 05:02:09 -04:00
fn := func ( node ast . Node ) {
call := node . ( * ast . CallExpr )
2019-12-03 08:18:49 -05:00
var arg int
2020-05-28 05:02:09 -04:00
switch code . CallNameAST ( pass , call ) {
2019-12-03 08:18:49 -05:00
case "net/http.Error" :
arg = 2
case "net/http.Redirect" :
arg = 3
case "net/http.StatusText" :
arg = 0
case "net/http.RedirectHandler" :
arg = 1
default :
2020-05-28 05:02:09 -04:00
return
2019-12-03 08:18:49 -05:00
}
lit , ok := call . Args [ arg ] . ( * ast . BasicLit )
if ! ok {
2020-05-28 05:02:09 -04:00
return
2019-12-03 08:18:49 -05:00
}
if whitelist [ lit . Value ] {
2020-05-28 05:02:09 -04:00
return
2019-12-03 08:18:49 -05:00
}
n , err := strconv . Atoi ( lit . Value )
if err != nil {
2020-05-28 05:02:09 -04:00
return
2019-12-03 08:18:49 -05:00
}
s , ok := httpStatusCodes [ n ]
if ! ok {
2020-05-28 05:02:09 -04:00
return
2019-12-03 08:18:49 -05:00
}
2020-05-28 05:02:09 -04:00
report . Report ( pass , lit , fmt . Sprintf ( "should use constant http.%s instead of numeric literal %d" , s , n ) ,
report . FilterGenerated ( ) ,
report . Fixes ( edit . Fix ( fmt . Sprintf ( "use http.%s instead of %d" , s , n ) , edit . ReplaceWithString ( pass . Fset , lit , "http." + s ) ) ) )
2019-12-03 08:18:49 -05:00
}
2020-05-28 05:02:09 -04:00
code . Preorder ( pass , fn , ( * ast . CallExpr ) ( nil ) )
2019-12-03 08:18:49 -05:00
return nil , nil
}
func CheckDefaultCaseOrder ( pass * analysis . Pass ) ( interface { } , error ) {
fn := func ( node ast . Node ) {
stmt := node . ( * ast . SwitchStmt )
list := stmt . Body . List
for i , c := range list {
if c . ( * ast . CaseClause ) . List == nil && i != 0 && i != len ( list ) - 1 {
2020-05-28 05:02:09 -04:00
report . Report ( pass , c , "default case should be first or last in switch statement" , report . FilterGenerated ( ) )
2019-12-03 08:18:49 -05:00
break
}
}
}
2020-05-28 05:02:09 -04:00
code . Preorder ( pass , fn , ( * ast . SwitchStmt ) ( nil ) )
2019-12-03 08:18:49 -05:00
return nil , nil
}
2020-05-28 05:02:09 -04:00
var (
checkYodaConditionsQ = pattern . MustParse ( ` (BinaryExpr left@(BasicLit _ _) tok@(Or "==" "!=") right@(Not (BasicLit _ _))) ` )
checkYodaConditionsR = pattern . MustParse ( ` (BinaryExpr right tok left) ` )
)
2019-12-03 08:18:49 -05:00
func CheckYodaConditions ( pass * analysis . Pass ) ( interface { } , error ) {
fn := func ( node ast . Node ) {
2020-05-28 05:02:09 -04:00
if _ , edits , ok := MatchAndEdit ( pass , checkYodaConditionsQ , checkYodaConditionsR , node ) ; ok {
report . Report ( pass , node , "don't use Yoda conditions" ,
report . FilterGenerated ( ) ,
report . Fixes ( edit . Fix ( "un-Yoda-fy" , edits ... ) ) )
2019-12-03 08:18:49 -05:00
}
}
2020-05-28 05:02:09 -04:00
code . Preorder ( pass , fn , ( * ast . BinaryExpr ) ( nil ) )
2019-12-03 08:18:49 -05:00
return nil , nil
}
func CheckInvisibleCharacters ( pass * analysis . Pass ) ( interface { } , error ) {
fn := func ( node ast . Node ) {
lit := node . ( * ast . BasicLit )
if lit . Kind != token . STRING {
return
}
2020-05-28 05:02:09 -04:00
type invalid struct {
r rune
off int
}
var invalids [ ] invalid
hasFormat := false
hasControl := false
for off , r := range lit . Value {
2019-12-03 08:18:49 -05:00
if unicode . Is ( unicode . Cf , r ) {
2020-05-28 05:02:09 -04:00
invalids = append ( invalids , invalid { r , off } )
hasFormat = true
2019-12-03 08:18:49 -05:00
} else if unicode . Is ( unicode . Cc , r ) && r != '\n' && r != '\t' && r != '\r' {
2020-05-28 05:02:09 -04:00
invalids = append ( invalids , invalid { r , off } )
hasControl = true
2019-12-03 08:18:49 -05:00
}
}
2020-05-28 05:02:09 -04:00
switch len ( invalids ) {
case 0 :
return
case 1 :
var kind string
if hasFormat {
kind = "format"
} else if hasControl {
kind = "control"
} else {
panic ( "unreachable" )
}
r := invalids [ 0 ]
msg := fmt . Sprintf ( "string literal contains the Unicode %s character %U, consider using the %q escape sequence instead" , kind , r . r , r . r )
replacement := strconv . QuoteRune ( r . r )
replacement = replacement [ 1 : len ( replacement ) - 1 ]
edit := analysis . SuggestedFix {
Message : fmt . Sprintf ( "replace %s character %U with %q" , kind , r . r , r . r ) ,
TextEdits : [ ] analysis . TextEdit { {
Pos : lit . Pos ( ) + token . Pos ( r . off ) ,
End : lit . Pos ( ) + token . Pos ( r . off ) + token . Pos ( utf8 . RuneLen ( r . r ) ) ,
NewText : [ ] byte ( replacement ) ,
} } ,
}
delete := analysis . SuggestedFix {
Message : fmt . Sprintf ( "delete %s character %U" , kind , r ) ,
TextEdits : [ ] analysis . TextEdit { {
Pos : lit . Pos ( ) + token . Pos ( r . off ) ,
End : lit . Pos ( ) + token . Pos ( r . off ) + token . Pos ( utf8 . RuneLen ( r . r ) ) ,
} } ,
}
report . Report ( pass , lit , msg , report . Fixes ( edit , delete ) )
default :
var kind string
if hasFormat && hasControl {
kind = "format and control"
} else if hasFormat {
kind = "format"
} else if hasControl {
kind = "control"
} else {
panic ( "unreachable" )
}
msg := fmt . Sprintf ( "string literal contains Unicode %s characters, consider using escape sequences instead" , kind )
var edits [ ] analysis . TextEdit
var deletions [ ] analysis . TextEdit
for _ , r := range invalids {
replacement := strconv . QuoteRune ( r . r )
replacement = replacement [ 1 : len ( replacement ) - 1 ]
edits = append ( edits , analysis . TextEdit {
Pos : lit . Pos ( ) + token . Pos ( r . off ) ,
End : lit . Pos ( ) + token . Pos ( r . off ) + token . Pos ( utf8 . RuneLen ( r . r ) ) ,
NewText : [ ] byte ( replacement ) ,
} )
deletions = append ( deletions , analysis . TextEdit {
Pos : lit . Pos ( ) + token . Pos ( r . off ) ,
End : lit . Pos ( ) + token . Pos ( r . off ) + token . Pos ( utf8 . RuneLen ( r . r ) ) ,
} )
}
edit := analysis . SuggestedFix {
Message : fmt . Sprintf ( "replace all %s characters with escape sequences" , kind ) ,
TextEdits : edits ,
}
delete := analysis . SuggestedFix {
Message : fmt . Sprintf ( "delete all %s characters" , kind ) ,
TextEdits : deletions ,
}
report . Report ( pass , lit , msg , report . Fixes ( edit , delete ) )
}
2019-12-03 08:18:49 -05:00
}
2020-05-28 05:02:09 -04:00
code . Preorder ( pass , fn , ( * ast . BasicLit ) ( nil ) )
return nil , nil
}
func CheckExportedFunctionDocs ( pass * analysis . Pass ) ( interface { } , error ) {
fn := func ( node ast . Node ) {
if code . IsInTest ( pass , node ) {
return
}
decl := node . ( * ast . FuncDecl )
if decl . Doc == nil {
return
}
if ! ast . IsExported ( decl . Name . Name ) {
return
}
kind := "function"
if decl . Recv != nil {
kind = "method"
switch T := decl . Recv . List [ 0 ] . Type . ( type ) {
case * ast . StarExpr :
if ! ast . IsExported ( T . X . ( * ast . Ident ) . Name ) {
return
}
case * ast . Ident :
if ! ast . IsExported ( T . Name ) {
return
}
default :
ExhaustiveTypeSwitch ( T )
}
}
prefix := decl . Name . Name + " "
if ! strings . HasPrefix ( decl . Doc . Text ( ) , prefix ) {
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 ( ) )
}
}
code . Preorder ( pass , fn , ( * ast . FuncDecl ) ( nil ) )
return nil , nil
}
func CheckExportedTypeDocs ( pass * analysis . Pass ) ( interface { } , error ) {
var genDecl * ast . GenDecl
fn := func ( node ast . Node , push bool ) bool {
if ! push {
genDecl = nil
return false
}
if code . IsInTest ( pass , node ) {
return false
}
switch node := node . ( type ) {
case * ast . GenDecl :
if node . Tok == token . IMPORT {
return false
}
genDecl = node
return true
case * ast . TypeSpec :
if ! ast . IsExported ( node . Name . Name ) {
return false
}
doc := node . Doc
if doc == nil {
if len ( genDecl . Specs ) != 1 {
// more than one spec in the GenDecl, don't validate the
// docstring
return false
}
if genDecl . Lparen . IsValid ( ) {
// 'type ( T )' is weird, don't guess the user's intention
return false
}
doc = genDecl . Doc
if doc == nil {
return false
}
}
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 , node . Name . Name + " " ) {
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 ( ) )
}
return false
case * ast . FuncLit , * ast . FuncDecl :
return false
default :
ExhaustiveTypeSwitch ( node )
return false
}
}
pass . ResultOf [ inspect . Analyzer ] . ( * inspector . Inspector ) . Nodes ( [ ] ast . Node { ( * ast . GenDecl ) ( nil ) , ( * ast . TypeSpec ) ( nil ) , ( * ast . FuncLit ) ( nil ) , ( * ast . FuncDecl ) ( nil ) } , fn )
return nil , nil
}
func CheckExportedVarDocs ( pass * analysis . Pass ) ( interface { } , error ) {
var genDecl * ast . GenDecl
fn := func ( node ast . Node , push bool ) bool {
if ! push {
genDecl = nil
return false
}
if code . IsInTest ( pass , node ) {
return false
}
switch node := node . ( type ) {
case * ast . GenDecl :
if node . Tok == token . IMPORT {
return false
}
genDecl = node
return true
case * ast . ValueSpec :
if genDecl . Lparen . IsValid ( ) || len ( node . Names ) > 1 {
// Don't try to guess the user's intention
return false
}
name := node . Names [ 0 ] . Name
if ! ast . IsExported ( name ) {
return false
}
if genDecl . Doc == nil {
return false
}
prefix := name + " "
if ! strings . HasPrefix ( genDecl . Doc . Text ( ) , prefix ) {
kind := "var"
if genDecl . Tok == token . CONST {
kind = "const"
}
report . Report ( pass , genDecl . Doc , fmt . Sprintf ( ` comment on exported %s %s should be of the form "%s..." ` , kind , name , prefix ) , report . FilterGenerated ( ) )
}
return false
case * ast . FuncLit , * ast . FuncDecl :
return false
default :
ExhaustiveTypeSwitch ( node )
return false
}
}
pass . ResultOf [ inspect . Analyzer ] . ( * inspector . Inspector ) . Nodes ( [ ] ast . Node { ( * ast . GenDecl ) ( nil ) , ( * ast . ValueSpec ) ( nil ) , ( * ast . FuncLit ) ( nil ) , ( * ast . FuncDecl ) ( nil ) } , fn )
2019-12-03 08:18:49 -05:00
return nil , nil
}