mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-12 11:36:29 +00:00
1248 lines
38 KiB
Go
1248 lines
38 KiB
Go
|
package wsl
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"go/ast"
|
||
|
"go/parser"
|
||
|
"go/token"
|
||
|
"io/ioutil"
|
||
|
"reflect"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
// Error reason strings
|
||
|
const (
|
||
|
reasonMustCuddleErrCheck = "if statements that check an error must be cuddled with the statement that assigned the error"
|
||
|
reasonOnlyCuddleIfWithAssign = "if statements should only be cuddled with assignments"
|
||
|
reasonOnlyOneCuddle = "only one cuddle assignment allowed before if statement"
|
||
|
reasonOnlyCuddleWithUsedAssign = "if statements should only be cuddled with assignments used in the if statement itself"
|
||
|
reasonOnlyCuddle2LineReturn = "return statements should not be cuddled if block has more than two lines"
|
||
|
reasonMultiLineBranchCuddle = "branch statements should not be cuddled if block has more than two lines"
|
||
|
reasonAppendCuddledWithoutUse = "append only allowed to cuddle with appended value"
|
||
|
reasonAssignsCuddleAssign = "assignments should only be cuddled with other assignments"
|
||
|
reasonNeverCuddleDeclare = "declarations should never be cuddled"
|
||
|
reasonExpressionCuddledWithDeclOrRet = "expressions should not be cuddled with declarations or returns"
|
||
|
reasonExpressionCuddledWithBlock = "expressions should not be cuddled with blocks"
|
||
|
reasonExprCuddlingNonAssignedVar = "only cuddled expressions if assigning variable or using from line above"
|
||
|
reasonOneCuddleBeforeRange = "only one cuddle assignment allowed before range statement"
|
||
|
reasonRangeCuddledWithoutUse = "ranges should only be cuddled with assignments used in the iteration"
|
||
|
reasonOneCuddleBeforeDefer = "only one cuddle assignment allowed before defer statement"
|
||
|
reasonDeferCuddledWithOtherVar = "defer statements should only be cuddled with expressions on same variable"
|
||
|
reasonForWithoutCondition = "for statement without condition should never be cuddled"
|
||
|
reasonForWithMoreThanOneCuddle = "only one cuddle assignment allowed before for statement"
|
||
|
reasonForCuddledAssignWithoutUse = "for statements should only be cuddled with assignments used in the iteration"
|
||
|
reasonOneCuddleBeforeGo = "only one cuddle assignment allowed before go statement"
|
||
|
reasonGoFuncWithoutAssign = "go statements can only invoke functions assigned on line above"
|
||
|
reasonSwitchManyCuddles = "only one cuddle assignment allowed before switch statement"
|
||
|
reasonAnonSwitchCuddled = "anonymous switch statements should never be cuddled"
|
||
|
reasonSwitchCuddledWithoutUse = "switch statements should only be cuddled with variables switched"
|
||
|
reasonTypeSwitchTooCuddled = "only one cuddle assignment allowed before type switch statement"
|
||
|
reasonTypeSwitchCuddledWithoutUse = "type switch statements should only be cuddled with variables switched"
|
||
|
reasonBlockStartsWithWS = "block should not start with a whitespace"
|
||
|
reasonBlockEndsWithWS = "block should not end with a whitespace (or comment)"
|
||
|
reasonCaseBlockTooCuddly = "case block should end with newline at this size"
|
||
|
reasonShortDeclNotExclusive = "short declaration should cuddle only with other short declarations"
|
||
|
)
|
||
|
|
||
|
// Warning strings
|
||
|
const (
|
||
|
warnTypeNotImplement = "type not implemented"
|
||
|
warnStmtNotImplemented = "stmt type not implemented"
|
||
|
warnBodyStmtTypeNotImplemented = "body statement type not implemented "
|
||
|
warnWSNodeTypeNotImplemented = "whitespace node type not implemented "
|
||
|
warnUnknownLHS = "UNKNOWN LHS"
|
||
|
warnUnknownRHS = "UNKNOWN RHS"
|
||
|
)
|
||
|
|
||
|
type Configuration struct {
|
||
|
// StrictAppend will do strict checking when assigning from append (x =
|
||
|
// append(x, y)). If this is set to true the append call must append either
|
||
|
// a variable assigned, called or used on the line above. Example on not
|
||
|
// allowed when this is true:
|
||
|
//
|
||
|
// x := []string{}
|
||
|
// y := "not going in X"
|
||
|
// x = append(x, "not y") // This is not allowed with StrictAppend
|
||
|
// z := "going in X"
|
||
|
//
|
||
|
// x = append(x, z) // This is allowed with StrictAppend
|
||
|
//
|
||
|
// m := transform(z)
|
||
|
// x = append(x, z) // So is this because Z is used above.
|
||
|
StrictAppend bool
|
||
|
|
||
|
// AllowAssignAndCallCuddle allows assignments to be cuddled with variables
|
||
|
// used in calls on line above and calls to be cuddled with assignments of
|
||
|
// variables used in call on line above.
|
||
|
// Example supported with this set to true:
|
||
|
//
|
||
|
// x.Call()
|
||
|
// x = Assign()
|
||
|
// x.AnotherCall()
|
||
|
// x = AnotherAssign()
|
||
|
AllowAssignAndCallCuddle bool
|
||
|
|
||
|
// AllowAssignAndCallCuddle allows assignments to be cuddled with anything.
|
||
|
// Example supported with this set to true:
|
||
|
// if x == 1 {
|
||
|
// x = 0
|
||
|
// }
|
||
|
// z := x + 2
|
||
|
// fmt.Println("x")
|
||
|
// y := "x"
|
||
|
AllowAssignAndAnythingCuddle bool
|
||
|
|
||
|
// AllowMultiLineAssignCuddle allows cuddling to assignments even if they
|
||
|
// span over multiple lines. This defaults to true which allows the
|
||
|
// following example:
|
||
|
//
|
||
|
// err := function(
|
||
|
// "multiple", "lines",
|
||
|
// )
|
||
|
// if err != nil {
|
||
|
// // ...
|
||
|
// }
|
||
|
AllowMultiLineAssignCuddle bool
|
||
|
|
||
|
// If the number of lines in a case block is equal to or lager than this
|
||
|
// number, the case *must* end white a newline.
|
||
|
ForceCaseTrailingWhitespaceLimit int
|
||
|
|
||
|
// AllowTrailingComment will allow blocks to end with comments.
|
||
|
AllowTrailingComment bool
|
||
|
|
||
|
// AllowSeparatedLeadingComment will allow multiple comments in the
|
||
|
// beginning of a block separated with newline. Example:
|
||
|
// func () {
|
||
|
// // Comment one
|
||
|
//
|
||
|
// // Comment two
|
||
|
// fmt.Println("x")
|
||
|
// }
|
||
|
AllowSeparatedLeadingComment bool
|
||
|
|
||
|
// AllowCuddleDeclaration will allow multiple var/declaration statements to
|
||
|
// be cuddled. This defaults to false but setting it to true will enable the
|
||
|
// following example:
|
||
|
// var foo bool
|
||
|
// var err error
|
||
|
AllowCuddleDeclaration bool
|
||
|
|
||
|
// AllowCuddleWithCalls is a list of call idents that everything can be
|
||
|
// cuddled with. Defaults to calls looking like locks to support a flow like
|
||
|
// this:
|
||
|
//
|
||
|
// mu.Lock()
|
||
|
// allow := thisAssignment
|
||
|
AllowCuddleWithCalls []string
|
||
|
|
||
|
// AllowCuddleWithRHS is a list of right hand side variables that is allowed
|
||
|
// to be cuddled with anything. Defaults to assignments or calls looking
|
||
|
// like unlocks to support a flow like this:
|
||
|
//
|
||
|
// allow := thisAssignment()
|
||
|
// mu.Unlock()
|
||
|
AllowCuddleWithRHS []string
|
||
|
|
||
|
// ForceCuddleErrCheckAndAssign will cause an error when an If statement that
|
||
|
// checks an error variable doesn't cuddle with the assignment of that variable.
|
||
|
// This defaults to false but setting it to true will cause the following
|
||
|
// to generate an error:
|
||
|
//
|
||
|
// err := ProduceError()
|
||
|
//
|
||
|
// if err != nil {
|
||
|
// return err
|
||
|
// }
|
||
|
ForceCuddleErrCheckAndAssign bool
|
||
|
|
||
|
// When ForceCuddleErrCheckAndAssign is enabled this is a list of names
|
||
|
// used for error variables to check for in the conditional.
|
||
|
// Defaults to just "err"
|
||
|
ErrorVariableNames []string
|
||
|
|
||
|
// ForceExclusiveShortDeclarations will cause an error if a short declaration
|
||
|
// (:=) cuddles with anything other than another short declaration. For example
|
||
|
//
|
||
|
// a := 2
|
||
|
// b := 3
|
||
|
//
|
||
|
// is allowed, but
|
||
|
//
|
||
|
// a := 2
|
||
|
// b = 3
|
||
|
//
|
||
|
// is not allowed. This logic overrides ForceCuddleErrCheckAndAssign among others.
|
||
|
ForceExclusiveShortDeclarations bool
|
||
|
}
|
||
|
|
||
|
// DefaultConfig returns default configuration
|
||
|
func DefaultConfig() Configuration {
|
||
|
return Configuration{
|
||
|
StrictAppend: true,
|
||
|
AllowAssignAndCallCuddle: true,
|
||
|
AllowAssignAndAnythingCuddle: false,
|
||
|
AllowMultiLineAssignCuddle: true,
|
||
|
AllowTrailingComment: false,
|
||
|
AllowSeparatedLeadingComment: false,
|
||
|
ForceCuddleErrCheckAndAssign: false,
|
||
|
ForceExclusiveShortDeclarations: false,
|
||
|
ForceCaseTrailingWhitespaceLimit: 0,
|
||
|
AllowCuddleWithCalls: []string{"Lock", "RLock"},
|
||
|
AllowCuddleWithRHS: []string{"Unlock", "RUnlock"},
|
||
|
ErrorVariableNames: []string{"err"},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Result represents the result of one error.
|
||
|
type Result struct {
|
||
|
FileName string
|
||
|
LineNumber int
|
||
|
Position token.Position
|
||
|
Reason string
|
||
|
}
|
||
|
|
||
|
// String returns the filename, line number and reason of a Result.
|
||
|
func (r *Result) String() string {
|
||
|
return fmt.Sprintf("%s:%d: %s", r.FileName, r.LineNumber, r.Reason)
|
||
|
}
|
||
|
|
||
|
type Processor struct {
|
||
|
config Configuration
|
||
|
result []Result
|
||
|
warnings []string
|
||
|
fileSet *token.FileSet
|
||
|
file *ast.File
|
||
|
}
|
||
|
|
||
|
// NewProcessor will create a Processor.
|
||
|
func NewProcessorWithConfig(cfg Configuration) *Processor {
|
||
|
return &Processor{
|
||
|
result: []Result{},
|
||
|
config: cfg,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NewProcessor will create a Processor.
|
||
|
func NewProcessor() *Processor {
|
||
|
return NewProcessorWithConfig(DefaultConfig())
|
||
|
}
|
||
|
|
||
|
// ProcessFiles takes a string slice with file names (full paths) and lints
|
||
|
// them.
|
||
|
// nolint: gocritic
|
||
|
func (p *Processor) ProcessFiles(filenames []string) ([]Result, []string) {
|
||
|
for _, filename := range filenames {
|
||
|
data, err := ioutil.ReadFile(filename)
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
|
||
|
p.process(filename, data)
|
||
|
}
|
||
|
|
||
|
return p.result, p.warnings
|
||
|
}
|
||
|
|
||
|
func (p *Processor) process(filename string, data []byte) {
|
||
|
fileSet := token.NewFileSet()
|
||
|
file, err := parser.ParseFile(fileSet, filename, data, parser.ParseComments)
|
||
|
|
||
|
// If the file is not parsable let's add a syntax error and move on.
|
||
|
if err != nil {
|
||
|
p.result = append(p.result, Result{
|
||
|
FileName: filename,
|
||
|
LineNumber: 0,
|
||
|
Reason: fmt.Sprintf("invalid syntax, file cannot be linted (%s)", err.Error()),
|
||
|
})
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
p.fileSet = fileSet
|
||
|
p.file = file
|
||
|
|
||
|
for _, d := range p.file.Decls {
|
||
|
switch v := d.(type) {
|
||
|
case *ast.FuncDecl:
|
||
|
p.parseBlockBody(v.Name, v.Body)
|
||
|
case *ast.GenDecl:
|
||
|
// `go fmt` will handle proper spacing for GenDecl such as imports,
|
||
|
// constants etc.
|
||
|
default:
|
||
|
p.addWarning(warnTypeNotImplement, d.Pos(), v)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// parseBlockBody will parse any kind of block statements such as switch cases
|
||
|
// and if statements. A list of Result is returned.
|
||
|
func (p *Processor) parseBlockBody(ident *ast.Ident, block *ast.BlockStmt) {
|
||
|
// Nothing to do if there's no value.
|
||
|
if reflect.ValueOf(block).IsNil() {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Start by finding leading and trailing whitespaces.
|
||
|
p.findLeadingAndTrailingWhitespaces(ident, block, nil)
|
||
|
|
||
|
// Parse the block body contents.
|
||
|
p.parseBlockStatements(block.List)
|
||
|
}
|
||
|
|
||
|
// parseBlockStatements will parse all the statements found in the body of a
|
||
|
// node. A list of Result is returned.
|
||
|
// nolint: gocognit
|
||
|
func (p *Processor) parseBlockStatements(statements []ast.Stmt) {
|
||
|
for i, stmt := range statements {
|
||
|
// Start by checking if this statement is another block (other than if,
|
||
|
// for and range). This could be assignment to a function, defer or go
|
||
|
// call with an inline function or similar. If this is found we start by
|
||
|
// parsing this body block before moving on.
|
||
|
for _, stmtBlocks := range p.findBlockStmt(stmt) {
|
||
|
p.parseBlockBody(nil, stmtBlocks)
|
||
|
}
|
||
|
|
||
|
firstBodyStatement := p.firstBodyStatement(i, statements)
|
||
|
|
||
|
// First statement, nothing to do.
|
||
|
if i == 0 {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
previousStatement := statements[i-1]
|
||
|
previousStatementIsMultiline := p.nodeStart(previousStatement) != p.nodeEnd(previousStatement)
|
||
|
cuddledWithLastStmt := p.nodeEnd(previousStatement) == p.nodeStart(stmt)-1
|
||
|
|
||
|
// If we're not cuddled and we don't need to enforce err-check cuddling
|
||
|
// then we can bail out here
|
||
|
if !cuddledWithLastStmt && !p.config.ForceCuddleErrCheckAndAssign {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// We don't force error cuddling for multilines. (#86)
|
||
|
if p.config.ForceCuddleErrCheckAndAssign && previousStatementIsMultiline && !cuddledWithLastStmt {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Extract assigned variables on the line above
|
||
|
// which is the only thing we allow cuddling with. If the assignment is
|
||
|
// made over multiple lines we should not allow cuddling.
|
||
|
var assignedOnLineAbove []string
|
||
|
|
||
|
// We want to keep track of what was called on the line above to support
|
||
|
// special handling of things such as mutexes.
|
||
|
var calledOnLineAbove []string
|
||
|
|
||
|
// Check if the previous statement spans over multiple lines.
|
||
|
var cuddledWithMultiLineAssignment = cuddledWithLastStmt && p.nodeStart(previousStatement) != p.nodeStart(stmt)-1
|
||
|
|
||
|
// Ensure previous line is not a multi line assignment and if not get
|
||
|
// rightAndLeftHandSide assigned variables.
|
||
|
if !cuddledWithMultiLineAssignment {
|
||
|
assignedOnLineAbove = p.findLHS(previousStatement)
|
||
|
calledOnLineAbove = p.findRHS(previousStatement)
|
||
|
}
|
||
|
|
||
|
// If previous assignment is multi line and we allow it, fetch
|
||
|
// assignments (but only assignments).
|
||
|
if cuddledWithMultiLineAssignment && p.config.AllowMultiLineAssignCuddle {
|
||
|
if _, ok := previousStatement.(*ast.AssignStmt); ok {
|
||
|
assignedOnLineAbove = p.findLHS(previousStatement)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// We could potentially have a block which require us to check the first
|
||
|
// argument before ruling out an allowed cuddle.
|
||
|
var calledOrAssignedFirstInBlock []string
|
||
|
|
||
|
if firstBodyStatement != nil {
|
||
|
calledOrAssignedFirstInBlock = append(p.findLHS(firstBodyStatement), p.findRHS(firstBodyStatement)...)
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
leftHandSide = p.findLHS(stmt)
|
||
|
rightHandSide = p.findRHS(stmt)
|
||
|
rightAndLeftHandSide = append(leftHandSide, rightHandSide...)
|
||
|
calledOrAssignedOnLineAbove = append(calledOnLineAbove, assignedOnLineAbove...)
|
||
|
)
|
||
|
|
||
|
// If we called some kind of lock on the line above we allow cuddling
|
||
|
// anything.
|
||
|
if atLeastOneInListsMatch(calledOnLineAbove, p.config.AllowCuddleWithCalls) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// If we call some kind of unlock on this line we allow cuddling with
|
||
|
// anything.
|
||
|
if atLeastOneInListsMatch(rightHandSide, p.config.AllowCuddleWithRHS) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
moreThanOneStatementAbove := func() bool {
|
||
|
if i < 2 {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
statementBeforePreviousStatement := statements[i-2]
|
||
|
|
||
|
return p.nodeStart(previousStatement)-1 == p.nodeEnd(statementBeforePreviousStatement)
|
||
|
}
|
||
|
|
||
|
isLastStatementInBlockOfOnlyTwoLines := func() bool {
|
||
|
// If we're the last statement, check if there's no more than two
|
||
|
// lines from the starting statement and the end of this statement.
|
||
|
// This is to support short return functions such as:
|
||
|
// func (t *Typ) X() {
|
||
|
// t.X = true
|
||
|
// return t
|
||
|
// }
|
||
|
// nolint: gocritic
|
||
|
if i == len(statements)-1 && i == 1 {
|
||
|
if p.nodeEnd(stmt)-p.nodeStart(previousStatement) <= 2 {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// If it's a short declaration we should not cuddle with anything else
|
||
|
// if ForceExclusiveShortDeclarations is set on; either this or the
|
||
|
// previous statement could be the short decl, so we'll find out which
|
||
|
// it was and use *that* statement's position
|
||
|
if p.config.ForceExclusiveShortDeclarations && cuddledWithLastStmt {
|
||
|
if p.isShortDecl(stmt) && !p.isShortDecl(previousStatement) {
|
||
|
p.addError(stmt.Pos(), reasonShortDeclNotExclusive)
|
||
|
} else if p.isShortDecl(previousStatement) && !p.isShortDecl(stmt) {
|
||
|
p.addError(previousStatement.Pos(), reasonShortDeclNotExclusive)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If it's not an if statement and we're not cuddled move on. The only
|
||
|
// reason we need to keep going for if statements is to check if we
|
||
|
// should be cuddled with an error check.
|
||
|
if _, ok := stmt.(*ast.IfStmt); !ok {
|
||
|
if !cuddledWithLastStmt {
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
|
||
|
switch t := stmt.(type) {
|
||
|
case *ast.IfStmt:
|
||
|
checkingErrInitializedInline := func() bool {
|
||
|
if t.Init == nil {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// Variables were initialized inline in the if statement
|
||
|
// Let's make sure it's the err just to be safe
|
||
|
return atLeastOneInListsMatch(p.findLHS(t.Init), p.config.ErrorVariableNames)
|
||
|
}
|
||
|
|
||
|
if !cuddledWithLastStmt {
|
||
|
checkingErr := atLeastOneInListsMatch(rightAndLeftHandSide, p.config.ErrorVariableNames)
|
||
|
if checkingErr {
|
||
|
// We only want to enforce cuddling error checks if the
|
||
|
// error was assigned on the line above. See
|
||
|
// https://github.com/bombsimon/wsl/issues/78.
|
||
|
// This is needed since `assignedOnLineAbove` is not
|
||
|
// actually just assignments but everything from LHS in the
|
||
|
// previous statement. This means that if previous line was
|
||
|
// `if err ...`, `err` will now be in the list
|
||
|
// `assignedOnLineAbove`.
|
||
|
if _, ok := previousStatement.(*ast.AssignStmt); !ok {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if checkingErrInitializedInline() {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if atLeastOneInListsMatch(assignedOnLineAbove, p.config.ErrorVariableNames) {
|
||
|
p.addError(t.Pos(), reasonMustCuddleErrCheck)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if len(assignedOnLineAbove) == 0 {
|
||
|
p.addError(t.Pos(), reasonOnlyCuddleIfWithAssign)
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if moreThanOneStatementAbove() {
|
||
|
p.addError(t.Pos(), reasonOnlyOneCuddle)
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if atLeastOneInListsMatch(rightAndLeftHandSide, assignedOnLineAbove) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if atLeastOneInListsMatch(assignedOnLineAbove, calledOrAssignedFirstInBlock) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
p.addError(t.Pos(), reasonOnlyCuddleWithUsedAssign)
|
||
|
case *ast.ReturnStmt:
|
||
|
if isLastStatementInBlockOfOnlyTwoLines() {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
p.addError(t.Pos(), reasonOnlyCuddle2LineReturn)
|
||
|
case *ast.BranchStmt:
|
||
|
if isLastStatementInBlockOfOnlyTwoLines() {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
p.addError(t.Pos(), reasonMultiLineBranchCuddle)
|
||
|
case *ast.AssignStmt:
|
||
|
// append is usually an assignment but should not be allowed to be
|
||
|
// cuddled with anything not appended.
|
||
|
if len(rightHandSide) > 0 && rightHandSide[len(rightHandSide)-1] == "append" {
|
||
|
if p.config.StrictAppend {
|
||
|
if !atLeastOneInListsMatch(calledOrAssignedOnLineAbove, rightHandSide) {
|
||
|
p.addError(t.Pos(), reasonAppendCuddledWithoutUse)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if _, ok := previousStatement.(*ast.AssignStmt); ok {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if p.config.AllowAssignAndAnythingCuddle {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if _, ok := previousStatement.(*ast.DeclStmt); ok && p.config.AllowCuddleDeclaration {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// If the assignment is from a type or variable called on the line
|
||
|
// above we can allow it by setting AllowAssignAndCallCuddle to
|
||
|
// true.
|
||
|
// Example (x is used):
|
||
|
// x.function()
|
||
|
// a.Field = x.anotherFunction()
|
||
|
if p.config.AllowAssignAndCallCuddle {
|
||
|
if atLeastOneInListsMatch(calledOrAssignedOnLineAbove, rightAndLeftHandSide) {
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
|
||
|
p.addError(t.Pos(), reasonAssignsCuddleAssign)
|
||
|
case *ast.DeclStmt:
|
||
|
if !p.config.AllowCuddleDeclaration {
|
||
|
p.addError(t.Pos(), reasonNeverCuddleDeclare)
|
||
|
}
|
||
|
case *ast.ExprStmt:
|
||
|
switch previousStatement.(type) {
|
||
|
case *ast.DeclStmt, *ast.ReturnStmt:
|
||
|
if p.config.AllowAssignAndCallCuddle && p.config.AllowCuddleDeclaration {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
p.addError(t.Pos(), reasonExpressionCuddledWithDeclOrRet)
|
||
|
case *ast.IfStmt, *ast.RangeStmt, *ast.SwitchStmt:
|
||
|
p.addError(t.Pos(), reasonExpressionCuddledWithBlock)
|
||
|
}
|
||
|
|
||
|
// If the expression is called on a type or variable used or
|
||
|
// assigned on the line we can allow it by setting
|
||
|
// AllowAssignAndCallCuddle to true.
|
||
|
// Example of allowed cuddled (x is used):
|
||
|
// a.Field = x.func()
|
||
|
// x.function()
|
||
|
if p.config.AllowAssignAndCallCuddle {
|
||
|
if atLeastOneInListsMatch(calledOrAssignedOnLineAbove, rightAndLeftHandSide) {
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If we assigned variables on the line above but didn't use them in
|
||
|
// this expression there should probably be a newline between them.
|
||
|
if len(assignedOnLineAbove) > 0 && !atLeastOneInListsMatch(rightAndLeftHandSide, assignedOnLineAbove) {
|
||
|
p.addError(t.Pos(), reasonExprCuddlingNonAssignedVar)
|
||
|
}
|
||
|
case *ast.RangeStmt:
|
||
|
if moreThanOneStatementAbove() {
|
||
|
p.addError(t.Pos(), reasonOneCuddleBeforeRange)
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if !atLeastOneInListsMatch(rightAndLeftHandSide, assignedOnLineAbove) {
|
||
|
if !atLeastOneInListsMatch(assignedOnLineAbove, calledOrAssignedFirstInBlock) {
|
||
|
p.addError(t.Pos(), reasonRangeCuddledWithoutUse)
|
||
|
}
|
||
|
}
|
||
|
case *ast.DeferStmt:
|
||
|
if _, ok := previousStatement.(*ast.DeferStmt); ok {
|
||
|
// We may cuddle multiple defers to group logic.
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Special treatment of deferring body closes after error checking
|
||
|
// according to best practices. See
|
||
|
// https://github.com/bombsimon/wsl/issues/31 which links to
|
||
|
// discussion about error handling after HTTP requests. This is hard
|
||
|
// coded and very specific but for now this is to be seen as a
|
||
|
// special case. What this does is that it *only* allows a defer
|
||
|
// statement with `Close` on the right hand side to be cuddled with
|
||
|
// an if-statement to support this:
|
||
|
// resp, err := client.Do(req)
|
||
|
// if err != nil {
|
||
|
// return err
|
||
|
// }
|
||
|
// defer resp.Body.Close()
|
||
|
if _, ok := previousStatement.(*ast.IfStmt); ok {
|
||
|
if atLeastOneInListsMatch(rightHandSide, []string{"Close"}) {
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if moreThanOneStatementAbove() {
|
||
|
p.addError(t.Pos(), reasonOneCuddleBeforeDefer)
|
||
|
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Be extra nice with RHS, it's common to use this for locks:
|
||
|
// m.Lock()
|
||
|
// defer m.Unlock()
|
||
|
previousRHS := p.findRHS(previousStatement)
|
||
|
if atLeastOneInListsMatch(rightHandSide, previousRHS) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Allow use to cuddled defer func literals with usages on line
|
||
|
// abouve. Example:
|
||
|
// b := getB()
|
||
|
// defer func() {
|
||
|
// makesSenseToUse(b)
|
||
|
// }()
|
||
|
if c, ok := t.Call.Fun.(*ast.FuncLit); ok {
|
||
|
funcLitFirstStmt := append(p.findLHS(c.Body), p.findRHS(c.Body)...)
|
||
|
|
||
|
if atLeastOneInListsMatch(assignedOnLineAbove, funcLitFirstStmt) {
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if atLeastOneInListsMatch(assignedOnLineAbove, calledOrAssignedFirstInBlock) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if !atLeastOneInListsMatch(rightAndLeftHandSide, assignedOnLineAbove) {
|
||
|
p.addError(t.Pos(), reasonDeferCuddledWithOtherVar)
|
||
|
}
|
||
|
case *ast.ForStmt:
|
||
|
if len(rightAndLeftHandSide) == 0 {
|
||
|
p.addError(t.Pos(), reasonForWithoutCondition)
|
||
|
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if moreThanOneStatementAbove() {
|
||
|
p.addError(t.Pos(), reasonForWithMoreThanOneCuddle)
|
||
|
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// The same rule applies for ranges as for if statements, see
|
||
|
// comments regarding variable usages on the line before or as the
|
||
|
// first line in the block for details.
|
||
|
if !atLeastOneInListsMatch(rightAndLeftHandSide, assignedOnLineAbove) {
|
||
|
if !atLeastOneInListsMatch(assignedOnLineAbove, calledOrAssignedFirstInBlock) {
|
||
|
p.addError(t.Pos(), reasonForCuddledAssignWithoutUse)
|
||
|
}
|
||
|
}
|
||
|
case *ast.GoStmt:
|
||
|
if _, ok := previousStatement.(*ast.GoStmt); ok {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if moreThanOneStatementAbove() {
|
||
|
p.addError(t.Pos(), reasonOneCuddleBeforeGo)
|
||
|
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if !atLeastOneInListsMatch(rightAndLeftHandSide, assignedOnLineAbove) {
|
||
|
p.addError(t.Pos(), reasonGoFuncWithoutAssign)
|
||
|
}
|
||
|
case *ast.SwitchStmt:
|
||
|
if moreThanOneStatementAbove() {
|
||
|
p.addError(t.Pos(), reasonSwitchManyCuddles)
|
||
|
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if !atLeastOneInListsMatch(rightAndLeftHandSide, assignedOnLineAbove) {
|
||
|
if len(rightAndLeftHandSide) == 0 {
|
||
|
p.addError(t.Pos(), reasonAnonSwitchCuddled)
|
||
|
} else {
|
||
|
p.addError(t.Pos(), reasonSwitchCuddledWithoutUse)
|
||
|
}
|
||
|
}
|
||
|
case *ast.TypeSwitchStmt:
|
||
|
if moreThanOneStatementAbove() {
|
||
|
p.addError(t.Pos(), reasonTypeSwitchTooCuddled)
|
||
|
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Allowed to type assert on variable assigned on line above.
|
||
|
if !atLeastOneInListsMatch(rightHandSide, assignedOnLineAbove) {
|
||
|
// Allow type assertion on variables used in the first case
|
||
|
// immediately.
|
||
|
if !atLeastOneInListsMatch(assignedOnLineAbove, calledOrAssignedFirstInBlock) {
|
||
|
p.addError(t.Pos(), reasonTypeSwitchCuddledWithoutUse)
|
||
|
}
|
||
|
}
|
||
|
case *ast.CaseClause, *ast.CommClause:
|
||
|
// Case clauses will be checked by not allowing leading ot trailing
|
||
|
// whitespaces within the block. There's nothing in the case itself
|
||
|
// that may be cuddled.
|
||
|
default:
|
||
|
p.addWarning(warnStmtNotImplemented, t.Pos(), t)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// firstBodyStatement returns the first statement inside a body block. This is
|
||
|
// because variables may be cuddled with conditions or statements if it's used
|
||
|
// directly as the first argument inside a body.
|
||
|
// The body will then be parsed as a *ast.BlockStmt (regular block) or as a list
|
||
|
// of []ast.Stmt (case block).
|
||
|
func (p *Processor) firstBodyStatement(i int, allStmt []ast.Stmt) ast.Node {
|
||
|
stmt := allStmt[i]
|
||
|
|
||
|
// Start by checking if the statement has a body (probably if-statement,
|
||
|
// a range, switch case or similar. Whenever a body is found we start by
|
||
|
// parsing it before moving on in the AST.
|
||
|
statementBody := reflect.Indirect(reflect.ValueOf(stmt)).FieldByName("Body")
|
||
|
|
||
|
// Some cases allow cuddling depending on the first statement in a body
|
||
|
// of a block or case. If possible extract the first statement.
|
||
|
var firstBodyStatement ast.Node
|
||
|
|
||
|
if !statementBody.IsValid() {
|
||
|
return firstBodyStatement
|
||
|
}
|
||
|
|
||
|
switch statementBodyContent := statementBody.Interface().(type) {
|
||
|
case *ast.BlockStmt:
|
||
|
if len(statementBodyContent.List) > 0 {
|
||
|
firstBodyStatement = statementBodyContent.List[0]
|
||
|
|
||
|
// If the first body statement is a *ast.CaseClause we're
|
||
|
// actually interested in the **next** body to know what's
|
||
|
// inside the first case.
|
||
|
if x, ok := firstBodyStatement.(*ast.CaseClause); ok {
|
||
|
if len(x.Body) > 0 {
|
||
|
firstBodyStatement = x.Body[0]
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
p.parseBlockBody(nil, statementBodyContent)
|
||
|
case []ast.Stmt:
|
||
|
// The Body field for an *ast.CaseClause or *ast.CommClause is of type
|
||
|
// []ast.Stmt. We must check leading and trailing whitespaces and then
|
||
|
// pass the statements to parseBlockStatements to parse it's content.
|
||
|
var nextStatement ast.Node
|
||
|
|
||
|
// Check if there's more statements (potential cases) after the
|
||
|
// current one.
|
||
|
if len(allStmt)-1 > i {
|
||
|
nextStatement = allStmt[i+1]
|
||
|
}
|
||
|
|
||
|
p.findLeadingAndTrailingWhitespaces(nil, stmt, nextStatement)
|
||
|
p.parseBlockStatements(statementBodyContent)
|
||
|
default:
|
||
|
p.addWarning(
|
||
|
warnBodyStmtTypeNotImplemented,
|
||
|
stmt.Pos(), statementBodyContent,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
return firstBodyStatement
|
||
|
}
|
||
|
|
||
|
func (p *Processor) findLHS(node ast.Node) []string {
|
||
|
var lhs []string
|
||
|
|
||
|
if node == nil {
|
||
|
return lhs
|
||
|
}
|
||
|
|
||
|
switch t := node.(type) {
|
||
|
case *ast.BasicLit, *ast.FuncLit, *ast.SelectStmt,
|
||
|
*ast.LabeledStmt, *ast.ForStmt, *ast.SwitchStmt,
|
||
|
*ast.ReturnStmt, *ast.GoStmt, *ast.CaseClause,
|
||
|
*ast.CommClause, *ast.CallExpr, *ast.UnaryExpr,
|
||
|
*ast.BranchStmt, *ast.TypeSpec, *ast.ChanType,
|
||
|
*ast.DeferStmt, *ast.TypeAssertExpr, *ast.RangeStmt:
|
||
|
// Nothing to add to LHS
|
||
|
case *ast.IncDecStmt:
|
||
|
return p.findLHS(t.X)
|
||
|
case *ast.Ident:
|
||
|
return []string{t.Name}
|
||
|
case *ast.AssignStmt:
|
||
|
for _, v := range t.Lhs {
|
||
|
lhs = append(lhs, p.findLHS(v)...)
|
||
|
}
|
||
|
case *ast.GenDecl:
|
||
|
for _, v := range t.Specs {
|
||
|
lhs = append(lhs, p.findLHS(v)...)
|
||
|
}
|
||
|
case *ast.ValueSpec:
|
||
|
for _, v := range t.Names {
|
||
|
lhs = append(lhs, p.findLHS(v)...)
|
||
|
}
|
||
|
case *ast.BlockStmt:
|
||
|
for _, v := range t.List {
|
||
|
lhs = append(lhs, p.findLHS(v)...)
|
||
|
}
|
||
|
case *ast.BinaryExpr:
|
||
|
return append(
|
||
|
p.findLHS(t.X),
|
||
|
p.findLHS(t.Y)...,
|
||
|
)
|
||
|
case *ast.DeclStmt:
|
||
|
return p.findLHS(t.Decl)
|
||
|
case *ast.IfStmt:
|
||
|
return p.findLHS(t.Cond)
|
||
|
case *ast.TypeSwitchStmt:
|
||
|
return p.findLHS(t.Assign)
|
||
|
case *ast.SendStmt:
|
||
|
return p.findLHS(t.Chan)
|
||
|
default:
|
||
|
if x, ok := maybeX(t); ok {
|
||
|
return p.findLHS(x)
|
||
|
}
|
||
|
|
||
|
p.addWarning(warnUnknownLHS, t.Pos(), t)
|
||
|
}
|
||
|
|
||
|
return lhs
|
||
|
}
|
||
|
|
||
|
func (p *Processor) findRHS(node ast.Node) []string {
|
||
|
var rhs []string
|
||
|
|
||
|
if node == nil {
|
||
|
return rhs
|
||
|
}
|
||
|
|
||
|
switch t := node.(type) {
|
||
|
case *ast.BasicLit, *ast.SelectStmt, *ast.ChanType,
|
||
|
*ast.LabeledStmt, *ast.DeclStmt, *ast.BranchStmt,
|
||
|
*ast.TypeSpec, *ast.ArrayType, *ast.CaseClause,
|
||
|
*ast.CommClause, *ast.KeyValueExpr, *ast.MapType,
|
||
|
*ast.FuncLit:
|
||
|
// Nothing to add to RHS
|
||
|
case *ast.Ident:
|
||
|
return []string{t.Name}
|
||
|
case *ast.SelectorExpr:
|
||
|
// TODO: Should this be RHS?
|
||
|
// t.X is needed for defer as of now and t.Sel needed for special
|
||
|
// functions such as Lock()
|
||
|
rhs = p.findRHS(t.X)
|
||
|
rhs = append(rhs, p.findRHS(t.Sel)...)
|
||
|
case *ast.AssignStmt:
|
||
|
for _, v := range t.Rhs {
|
||
|
rhs = append(rhs, p.findRHS(v)...)
|
||
|
}
|
||
|
case *ast.CallExpr:
|
||
|
for _, v := range t.Args {
|
||
|
rhs = append(rhs, p.findRHS(v)...)
|
||
|
}
|
||
|
|
||
|
rhs = append(rhs, p.findRHS(t.Fun)...)
|
||
|
case *ast.CompositeLit:
|
||
|
for _, v := range t.Elts {
|
||
|
rhs = append(rhs, p.findRHS(v)...)
|
||
|
}
|
||
|
case *ast.IfStmt:
|
||
|
rhs = append(rhs, p.findRHS(t.Cond)...)
|
||
|
rhs = append(rhs, p.findRHS(t.Init)...)
|
||
|
case *ast.BinaryExpr:
|
||
|
return append(
|
||
|
p.findRHS(t.X),
|
||
|
p.findRHS(t.Y)...,
|
||
|
)
|
||
|
case *ast.TypeSwitchStmt:
|
||
|
return p.findRHS(t.Assign)
|
||
|
case *ast.ReturnStmt:
|
||
|
for _, v := range t.Results {
|
||
|
rhs = append(rhs, p.findRHS(v)...)
|
||
|
}
|
||
|
case *ast.BlockStmt:
|
||
|
for _, v := range t.List {
|
||
|
rhs = append(rhs, p.findRHS(v)...)
|
||
|
}
|
||
|
case *ast.SwitchStmt:
|
||
|
return p.findRHS(t.Tag)
|
||
|
case *ast.GoStmt:
|
||
|
return p.findRHS(t.Call)
|
||
|
case *ast.ForStmt:
|
||
|
return p.findRHS(t.Cond)
|
||
|
case *ast.DeferStmt:
|
||
|
return p.findRHS(t.Call)
|
||
|
case *ast.SendStmt:
|
||
|
return p.findLHS(t.Value)
|
||
|
case *ast.IndexExpr:
|
||
|
rhs = append(rhs, p.findRHS(t.Index)...)
|
||
|
rhs = append(rhs, p.findRHS(t.X)...)
|
||
|
case *ast.SliceExpr:
|
||
|
rhs = append(rhs, p.findRHS(t.X)...)
|
||
|
rhs = append(rhs, p.findRHS(t.Low)...)
|
||
|
rhs = append(rhs, p.findRHS(t.High)...)
|
||
|
default:
|
||
|
if x, ok := maybeX(t); ok {
|
||
|
return p.findRHS(x)
|
||
|
}
|
||
|
|
||
|
p.addWarning(warnUnknownRHS, t.Pos(), t)
|
||
|
}
|
||
|
|
||
|
return rhs
|
||
|
}
|
||
|
|
||
|
func (p *Processor) isShortDecl(node ast.Node) bool {
|
||
|
if t, ok := node.(*ast.AssignStmt); ok {
|
||
|
return t.Tok == token.DEFINE
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
func (p *Processor) findBlockStmt(node ast.Node) []*ast.BlockStmt {
|
||
|
var blocks []*ast.BlockStmt
|
||
|
|
||
|
switch t := node.(type) {
|
||
|
case *ast.AssignStmt:
|
||
|
for _, x := range t.Rhs {
|
||
|
blocks = append(blocks, p.findBlockStmt(x)...)
|
||
|
}
|
||
|
case *ast.CallExpr:
|
||
|
blocks = append(blocks, p.findBlockStmt(t.Fun)...)
|
||
|
case *ast.FuncLit:
|
||
|
blocks = append(blocks, t.Body)
|
||
|
case *ast.ExprStmt:
|
||
|
blocks = append(blocks, p.findBlockStmt(t.X)...)
|
||
|
case *ast.ReturnStmt:
|
||
|
for _, x := range t.Results {
|
||
|
blocks = append(blocks, p.findBlockStmt(x)...)
|
||
|
}
|
||
|
case *ast.DeferStmt:
|
||
|
blocks = append(blocks, p.findBlockStmt(t.Call)...)
|
||
|
case *ast.GoStmt:
|
||
|
blocks = append(blocks, p.findBlockStmt(t.Call)...)
|
||
|
}
|
||
|
|
||
|
return blocks
|
||
|
}
|
||
|
|
||
|
// maybeX extracts the X field from an AST node and returns it with a true value
|
||
|
// if it exists. If the node doesn't have an X field nil and false is returned.
|
||
|
// Known fields with X that are handled:
|
||
|
// IndexExpr, ExprStmt, SelectorExpr, StarExpr, ParentExpr, TypeAssertExpr,
|
||
|
// RangeStmt, UnaryExpr, ParenExpr, SliceExpr, IncDecStmt.
|
||
|
func maybeX(node interface{}) (ast.Node, bool) {
|
||
|
maybeHasX := reflect.Indirect(reflect.ValueOf(node)).FieldByName("X")
|
||
|
if !maybeHasX.IsValid() {
|
||
|
return nil, false
|
||
|
}
|
||
|
|
||
|
n, ok := maybeHasX.Interface().(ast.Node)
|
||
|
if !ok {
|
||
|
return nil, false
|
||
|
}
|
||
|
|
||
|
return n, true
|
||
|
}
|
||
|
|
||
|
func atLeastOneInListsMatch(listOne, listTwo []string) bool {
|
||
|
sliceToMap := func(s []string) map[string]struct{} {
|
||
|
m := map[string]struct{}{}
|
||
|
|
||
|
for _, v := range s {
|
||
|
m[v] = struct{}{}
|
||
|
}
|
||
|
|
||
|
return m
|
||
|
}
|
||
|
|
||
|
m1 := sliceToMap(listOne)
|
||
|
m2 := sliceToMap(listTwo)
|
||
|
|
||
|
for k1 := range m1 {
|
||
|
if _, ok := m2[k1]; ok {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for k2 := range m2 {
|
||
|
if _, ok := m1[k2]; ok {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// findLeadingAndTrailingWhitespaces will find leading and trailing whitespaces
|
||
|
// in a node. The method takes comments in consideration which will make the
|
||
|
// parser more gentle.
|
||
|
// nolint: gocognit
|
||
|
func (p *Processor) findLeadingAndTrailingWhitespaces(ident *ast.Ident, stmt, nextStatement ast.Node) {
|
||
|
var (
|
||
|
allowedLinesBeforeFirstStatement = 1
|
||
|
commentMap = ast.NewCommentMap(p.fileSet, stmt, p.file.Comments)
|
||
|
blockStatements []ast.Stmt
|
||
|
blockStartLine int
|
||
|
blockEndLine int
|
||
|
blockStartPos token.Pos
|
||
|
blockEndPos token.Pos
|
||
|
)
|
||
|
|
||
|
// Depending on the block type, get the statements in the block and where
|
||
|
// the block starts (and ends).
|
||
|
switch t := stmt.(type) {
|
||
|
case *ast.BlockStmt:
|
||
|
blockStatements = t.List
|
||
|
blockStartPos = t.Lbrace
|
||
|
blockEndPos = t.Rbrace
|
||
|
case *ast.CaseClause:
|
||
|
blockStatements = t.Body
|
||
|
blockStartPos = t.Colon
|
||
|
case *ast.CommClause:
|
||
|
blockStatements = t.Body
|
||
|
blockStartPos = t.Colon
|
||
|
default:
|
||
|
p.addWarning(warnWSNodeTypeNotImplemented, stmt.Pos(), stmt)
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Ignore empty blocks even if they have newlines or just comments.
|
||
|
if len(blockStatements) < 1 {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
blockStartLine = p.fileSet.Position(blockStartPos).Line
|
||
|
blockEndLine = p.fileSet.Position(blockEndPos).Line
|
||
|
|
||
|
// No whitespace possible if LBrace and RBrace is on the same line.
|
||
|
if blockStartLine == blockEndLine {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
firstStatement = blockStatements[0]
|
||
|
lastStatement = blockStatements[len(blockStatements)-1]
|
||
|
seenCommentGroups = 0
|
||
|
)
|
||
|
|
||
|
// Get the comment related to the first statement, we do allow commends in
|
||
|
// the beginning of a block before the first statement.
|
||
|
if c, ok := commentMap[firstStatement]; ok {
|
||
|
for _, commentGroup := range c {
|
||
|
// If the comment group is on the same line as the block start
|
||
|
// (LBrace) we should not consider it.
|
||
|
if p.nodeStart(commentGroup) == blockStartLine {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// We only care about comments before our statement from the comment
|
||
|
// map. As soon as we hit comments after our statement let's break
|
||
|
// out!
|
||
|
if commentGroup.Pos() > firstStatement.Pos() {
|
||
|
break
|
||
|
}
|
||
|
|
||
|
// We store number of seen comment groups because we allow multiple
|
||
|
// groups with a newline between them; but if the first one has WS
|
||
|
// before it, we're not going to count it to force an error.
|
||
|
if p.config.AllowSeparatedLeadingComment {
|
||
|
cg := p.fileSet.Position(commentGroup.Pos()).Line
|
||
|
|
||
|
if seenCommentGroups > 0 || cg == blockStartLine+1 {
|
||
|
seenCommentGroups++
|
||
|
}
|
||
|
} else {
|
||
|
seenCommentGroups++
|
||
|
}
|
||
|
|
||
|
// Support both /* multiline */ and //single line comments
|
||
|
for _, c := range commentGroup.List {
|
||
|
allowedLinesBeforeFirstStatement += len(strings.Split(c.Text, "\n"))
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If we allow separated comments, allow for a space after each group
|
||
|
if p.config.AllowSeparatedLeadingComment {
|
||
|
if seenCommentGroups > 1 {
|
||
|
allowedLinesBeforeFirstStatement += seenCommentGroups - 1
|
||
|
} else if seenCommentGroups == 1 {
|
||
|
allowedLinesBeforeFirstStatement += 1
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// And now if the first statement is passed the number of allowed lines,
|
||
|
// then we had extra WS, possibly before the first comment group.
|
||
|
if p.nodeStart(firstStatement) > blockStartLine+allowedLinesBeforeFirstStatement {
|
||
|
p.addError(
|
||
|
blockStartPos,
|
||
|
reasonBlockStartsWithWS,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
// If the blockEndLine is not 0 we're a regular block (not case).
|
||
|
if blockEndLine != 0 {
|
||
|
if p.config.AllowTrailingComment {
|
||
|
if lastComment, ok := commentMap[lastStatement]; ok {
|
||
|
var (
|
||
|
lastCommentGroup = lastComment[len(lastComment)-1]
|
||
|
lastCommentLine = lastCommentGroup.List[len(lastCommentGroup.List)-1]
|
||
|
countNewlines = 0
|
||
|
)
|
||
|
|
||
|
countNewlines += len(strings.Split(lastCommentLine.Text, "\n"))
|
||
|
|
||
|
// No newlines between trailing comments and end of block.
|
||
|
if p.nodeStart(lastCommentLine)+countNewlines != blockEndLine-1 {
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if p.nodeEnd(lastStatement) != blockEndLine-1 && !isExampleFunc(ident) {
|
||
|
p.addError(blockEndPos, reasonBlockEndsWithWS)
|
||
|
}
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// If we don't have any nextStatement the trailing whitespace will be
|
||
|
// handled when parsing the switch. If we do have a next statement we can
|
||
|
// see where it starts by getting it's colon position. We set the end of the
|
||
|
// current case to the position of the next case.
|
||
|
switch n := nextStatement.(type) {
|
||
|
case *ast.CaseClause:
|
||
|
blockEndPos = n.Case
|
||
|
case *ast.CommClause:
|
||
|
blockEndPos = n.Case
|
||
|
default:
|
||
|
// No more cases
|
||
|
return
|
||
|
}
|
||
|
|
||
|
blockEndLine = p.fileSet.Position(blockEndPos).Line - 1
|
||
|
|
||
|
var (
|
||
|
blockSize = blockEndLine - blockStartLine
|
||
|
caseTrailingCommentLines int
|
||
|
)
|
||
|
|
||
|
// TODO: I don't know what comments are bound to in cases. For regular
|
||
|
// blocks the last comment is bound to the last statement but for cases
|
||
|
// they are bound to the case clause expression. This will however get us all
|
||
|
// comments and depending on the case expression this gets tricky.
|
||
|
//
|
||
|
// To handle this I get the comment map from the current statement (the case
|
||
|
// itself) and iterate through all groups and all comment within all groups.
|
||
|
// I then get the comments after the last statement but before the next case
|
||
|
// clause and just map each line of comment that way.
|
||
|
for _, commentGroups := range commentMap {
|
||
|
for _, commentGroup := range commentGroups {
|
||
|
for _, comment := range commentGroup.List {
|
||
|
commentLine := p.fileSet.Position(comment.Pos()).Line
|
||
|
|
||
|
// Ignore comments before the last statement.
|
||
|
if commentLine <= p.nodeStart(lastStatement) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Ignore comments after the end of this case.
|
||
|
if commentLine > blockEndLine {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// This allows /* multiline */ comments with newlines as well
|
||
|
// as regular (//) ones
|
||
|
caseTrailingCommentLines += len(strings.Split(comment.Text, "\n"))
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
hasTrailingWhitespace := p.nodeEnd(lastStatement)+caseTrailingCommentLines != blockEndLine
|
||
|
|
||
|
// If the force trailing limit is configured and we don't end with a newline.
|
||
|
if p.config.ForceCaseTrailingWhitespaceLimit > 0 && !hasTrailingWhitespace {
|
||
|
// Check if the block size is too big to miss the newline.
|
||
|
if blockSize >= p.config.ForceCaseTrailingWhitespaceLimit {
|
||
|
p.addError(lastStatement.Pos(), reasonCaseBlockTooCuddly)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func isExampleFunc(ident *ast.Ident) bool {
|
||
|
return ident != nil && strings.HasPrefix(ident.Name, "Example")
|
||
|
}
|
||
|
|
||
|
func (p *Processor) nodeStart(node ast.Node) int {
|
||
|
return p.fileSet.Position(node.Pos()).Line
|
||
|
}
|
||
|
|
||
|
func (p *Processor) nodeEnd(node ast.Node) int {
|
||
|
var line = p.fileSet.Position(node.End()).Line
|
||
|
|
||
|
if isEmptyLabeledStmt(node) {
|
||
|
return p.fileSet.Position(node.Pos()).Line
|
||
|
}
|
||
|
|
||
|
return line
|
||
|
}
|
||
|
|
||
|
func isEmptyLabeledStmt(node ast.Node) bool {
|
||
|
v, ok := node.(*ast.LabeledStmt)
|
||
|
if !ok {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
_, empty := v.Stmt.(*ast.EmptyStmt)
|
||
|
|
||
|
return empty
|
||
|
}
|
||
|
|
||
|
// Add an error for the file and line number for the current token.Pos with the
|
||
|
// given reason.
|
||
|
func (p *Processor) addError(pos token.Pos, reason string) {
|
||
|
position := p.fileSet.Position(pos)
|
||
|
|
||
|
p.result = append(p.result, Result{
|
||
|
FileName: position.Filename,
|
||
|
LineNumber: position.Line,
|
||
|
Position: position,
|
||
|
Reason: reason,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func (p *Processor) addWarning(w string, pos token.Pos, t interface{}) {
|
||
|
position := p.fileSet.Position(pos)
|
||
|
|
||
|
p.warnings = append(p.warnings,
|
||
|
fmt.Sprintf("%s:%d: %s (%T)", position.Filename, position.Line, w, t),
|
||
|
)
|
||
|
}
|