From 5fc5366972c874b919f93165bb4ed4e2bcb7c350 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 17:40:11 +0000 Subject: mod: bump github.com/golangci/golangci-lint from 1.55.2 to 1.56.2 Bumps [github.com/golangci/golangci-lint](https://github.com/golangci/golangci-lint) from 1.55.2 to 1.56.2. - [Release notes](https://github.com/golangci/golangci-lint/releases) - [Changelog](https://github.com/golangci/golangci-lint/blob/master/CHANGELOG.md) - [Commits](https://github.com/golangci/golangci-lint/compare/v1.55.2...v1.56.2) --- updated-dependencies: - dependency-name: github.com/golangci/golangci-lint dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- vendor/github.com/bombsimon/wsl/v3/.gitignore | 70 -- vendor/github.com/bombsimon/wsl/v3/.golangci.yml | 78 -- vendor/github.com/bombsimon/wsl/v3/LICENSE | 21 - vendor/github.com/bombsimon/wsl/v3/README.md | 126 -- vendor/github.com/bombsimon/wsl/v3/wsl.go | 1264 ------------------- vendor/github.com/bombsimon/wsl/v4/.gitignore | 70 ++ vendor/github.com/bombsimon/wsl/v4/.golangci.yml | 81 ++ vendor/github.com/bombsimon/wsl/v4/LICENSE | 21 + vendor/github.com/bombsimon/wsl/v4/README.md | 98 ++ vendor/github.com/bombsimon/wsl/v4/analyzer.go | 141 +++ vendor/github.com/bombsimon/wsl/v4/wsl.go | 1411 ++++++++++++++++++++++ 11 files changed, 1822 insertions(+), 1559 deletions(-) delete mode 100644 vendor/github.com/bombsimon/wsl/v3/.gitignore delete mode 100644 vendor/github.com/bombsimon/wsl/v3/.golangci.yml delete mode 100644 vendor/github.com/bombsimon/wsl/v3/LICENSE delete mode 100644 vendor/github.com/bombsimon/wsl/v3/README.md delete mode 100644 vendor/github.com/bombsimon/wsl/v3/wsl.go create mode 100644 vendor/github.com/bombsimon/wsl/v4/.gitignore create mode 100644 vendor/github.com/bombsimon/wsl/v4/.golangci.yml create mode 100644 vendor/github.com/bombsimon/wsl/v4/LICENSE create mode 100644 vendor/github.com/bombsimon/wsl/v4/README.md create mode 100644 vendor/github.com/bombsimon/wsl/v4/analyzer.go create mode 100644 vendor/github.com/bombsimon/wsl/v4/wsl.go (limited to 'vendor/github.com/bombsimon') diff --git a/vendor/github.com/bombsimon/wsl/v3/.gitignore b/vendor/github.com/bombsimon/wsl/v3/.gitignore deleted file mode 100644 index 1c8eba613..000000000 --- a/vendor/github.com/bombsimon/wsl/v3/.gitignore +++ /dev/null @@ -1,70 +0,0 @@ - -# Created by https://www.gitignore.io/api/go,vim,macos - -### Go ### -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, build with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -### Go Patch ### -/vendor/ -/Godeps/ - -### macOS ### -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -### Vim ### -# Swap -[._]*.s[a-v][a-z] -[._]*.sw[a-p] -[._]s[a-rt-v][a-z] -[._]ss[a-gi-z] -[._]sw[a-p] - -# Session -Session.vim - -# Temporary -.netrwhist -*~ -# Auto-generated tag files -tags -# Persistent undo -[._]*.un~ - - -# End of https://www.gitignore.io/api/go,vim,macos diff --git a/vendor/github.com/bombsimon/wsl/v3/.golangci.yml b/vendor/github.com/bombsimon/wsl/v3/.golangci.yml deleted file mode 100644 index 336ad4bc8..000000000 --- a/vendor/github.com/bombsimon/wsl/v3/.golangci.yml +++ /dev/null @@ -1,78 +0,0 @@ ---- -run: - deadline: 1m - issues-exit-code: 1 - tests: true - skip-dirs: - - vendor$ - -output: - format: colored-line-number - print-issued-lines: false - -linters-settings: - gocognit: - min-complexity: 10 - - depguard: - list-type: blacklist - include-go-root: false - packages: - - github.com/davecgh/go-spew/spew - - misspell: - locale: US - - gocritic: - # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` - # to see all tags and checks. Empty list by default. See - # https://github.com/go-critic/go-critic#usage -> section "Tags". - enabled-tags: - - diagnostic - - experimental - - opinionated - - performance - - style - -linters: - enable-all: true - disable: - - cyclop - - deadcode - - dupl - - dupword - - exhaustivestruct - - exhaustruct - - forbidigo - - funlen - - gci - - gocognit - - gocyclo - - godox - - golint - - gomnd - - ifshort - - interfacer - - lll - - maintidx - - maligned - - nakedret - - nestif - - nlreturn - - nosnakecase - - paralleltest - - prealloc - - scopelint - - structcheck - - testpackage - - varcheck - - varnamelen - fast: false - - -issues: - exclude-use-default: true - max-issues-per-linter: 0 - max-same-issues: 0 - -# vim: set sw=2 ts=2 et: diff --git a/vendor/github.com/bombsimon/wsl/v3/LICENSE b/vendor/github.com/bombsimon/wsl/v3/LICENSE deleted file mode 100644 index 4dade6d1c..000000000 --- a/vendor/github.com/bombsimon/wsl/v3/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 Simon Sawert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/bombsimon/wsl/v3/README.md b/vendor/github.com/bombsimon/wsl/v3/README.md deleted file mode 100644 index 8ff74392b..000000000 --- a/vendor/github.com/bombsimon/wsl/v3/README.md +++ /dev/null @@ -1,126 +0,0 @@ -# WSL - Whitespace Linter - -[![forthebadge](https://forthebadge.com/images/badges/made-with-go.svg)](https://forthebadge.com) -[![forthebadge](https://forthebadge.com/images/badges/built-with-love.svg)](https://forthebadge.com) - -[![GitHub Actions](https://github.com/bombsimon/wsl/actions/workflows/go.yml/badge.svg)](https://github.com/bombsimon/wsl/actions/workflows/go.yml) -[![Coverage Status](https://coveralls.io/repos/github/bombsimon/wsl/badge.svg?branch=master)](https://coveralls.io/github/bombsimon/wsl?branch=master) - -WSL is a linter that enforces a very **non scientific** vision of how to make -code more readable by enforcing empty lines at the right places. - -I think too much code out there is to cuddly and a bit too warm for it's own -good, making it harder for other people to read and understand. The linter will -warn about newlines in and around blocks, in the beginning of files and other -places in the code. - -**I know this linter is aggressive** and a lot of projects I've tested it on -have failed miserably. For this linter to be useful at all I want to be open to -new ideas, configurations and discussions! Also note that some of the warnings -might be bugs or unintentional false positives so I would love an -[issue](https://github.com/bombsimon/wsl/issues/new) to fix, discuss, change or -make something configurable! - -## Installation - -### By `go get` (local installation) - -You can do that by using: - -```sh -go get -u github.com/bombsimon/wsl/v3/cmd/... -``` - -### By golangci-lint (CI automation) - -`wsl` is already integrated with -[golangci-lint](https://github.com/golangci/golangci-lint). Please refer to the -instructions there. - -## Usage - -How to use depends on how you install `wsl`. - -### With local binary - -The general command format for `wsl` is: - -```sh -$ wsl [flags] [files...] -$ wsl [flags] - -# Examples - -$ wsl ./main.go -$ wsl --no-test ./main.go -$ wsl --allow-cuddle-declarations ./main.go -$ wsl --no-test --allow-cuddle-declaration ./main.go -$ wsl --no-test --allow-trailing-comment ./myProject/... -``` - -The "..." wildcard is not used like other `go` commands but instead can only -be to a relative or absolute path. - -By default, the linter will run on `./...` which means all go files in the -current path and all subsequent paths, including test files. To disable linting -test files, use `-n` or `--no-test`. - -### By `golangci-lint` (CI automation) - -The recommended command is: - -```sh -golangci-lint run --disable-all --enable wsl -``` - -For more information, please refer to -[golangci-lint](https://github.com/golangci/golangci-lint)'s documentation. - -## Issues and configuration - -The linter suppers a few ways to configure it to satisfy more than one kind of -code style. These settings could be set either with flags or with YAML -configuration if used via `golangci-lint`. - -The supported configuration can be found [in the documentation](doc/configuration.md). - -Below are the available checklist for any hit from `wsl`. If you do not see any, -feel free to raise an [issue](https://github.com/bombsimon/wsl/issues/new). - -> **Note**: this linter doesn't take in consideration the issues that will be -> fixed with `go fmt -s` so ensure that the code is properly formatted before -> use. - -* [Anonymous switch statements should never be cuddled](doc/rules.md#anonymous-switch-statements-should-never-be-cuddled) -* [Append only allowed to cuddle with appended value](doc/rules.md#append-only-allowed-to-cuddle-with-appended-value) -* [Assignments should only be cuddled with other assignments](doc/rules.md#assignments-should-only-be-cuddled-with-other-assignments) -* [Block should not end with a whitespace (or comment)](doc/rules.md#block-should-not-end-with-a-whitespace-or-comment) -* [Block should not start with a whitespace](doc/rules.md#block-should-not-start-with-a-whitespace) -* [Case block should end with newline at this size](doc/rules.md#case-block-should-end-with-newline-at-this-size) -* [Branch statements should not be cuddled if block has more than two lines](doc/rules.md#branch-statements-should-not-be-cuddled-if-block-has-more-than-two-lines) -* [Declarations should never be cuddled](doc/rules.md#declarations-should-never-be-cuddled) -* [Defer statements should only be cuddled with expressions on same variable](doc/rules.md#defer-statements-should-only-be-cuddled-with-expressions-on-same-variable) -* [Expressions should not be cuddled with blocks](doc/rules.md#expressions-should-not-be-cuddled-with-blocks) -* [Expressions should not be cuddled with declarations or returns](doc/rules.md#expressions-should-not-be-cuddled-with-declarations-or-returns) -* [For statement without condition should never be cuddled](doc/rules.md#for-statement-without-condition-should-never-be-cuddled) -* [For statements should only be cuddled with assignments used in the iteration](doc/rules.md#for-statements-should-only-be-cuddled-with-assignments-used-in-the-iteration) -* [Go statements can only invoke functions assigned on line above](doc/rules.md#go-statements-can-only-invoke-functions-assigned-on-line-above) -* [If statements should only be cuddled with assignments](doc/rules.md#if-statements-should-only-be-cuddled-with-assignments) -* [If statements should only be cuddled with assignments used in the if - statement - itself](doc/rules.md#if-statements-should-only-be-cuddled-with-assignments-used-in-the-if-statement-itself) -* [If statements that check an error must be cuddled with the statement that assigned the error](doc/rules.md#if-statements-that-check-an-error-must-be-cuddled-with-the-statement-that-assigned-the-error) -* [Only cuddled expressions if assigning variable or using from line - above](doc/rules.md#only-cuddled-expressions-if-assigning-variable-or-using-from-line-above) -* [Only one cuddle assignment allowed before defer statement](doc/rules.md#only-one-cuddle-assignment-allowed-before-defer-statement) -* [Only one cuddle assginment allowed before for statement](doc/rules.md#only-one-cuddle-assignment-allowed-before-for-statement) -* [Only one cuddle assignment allowed before go statement](doc/rules.md#only-one-cuddle-assignment-allowed-before-go-statement) -* [Only one cuddle assignment allowed before if statement](doc/rules.md#only-one-cuddle-assignment-allowed-before-if-statement) -* [Only one cuddle assignment allowed before range statement](doc/rules.md#only-one-cuddle-assignment-allowed-before-range-statement) -* [Only one cuddle assignment allowed before switch statement](doc/rules.md#only-one-cuddle-assignment-allowed-before-switch-statement) -* [Only one cuddle assignment allowed before type switch statement](doc/rules.md#only-one-cuddle-assignment-allowed-before-type-switch-statement) -* [Ranges should only be cuddled with assignments used in the iteration](doc/rules.md#ranges-should-only-be-cuddled-with-assignments-used-in-the-iteration) -* [Return statements should not be cuddled if block has more than two lines](doc/rules.md#return-statements-should-not-be-cuddled-if-block-has-more-than-two-lines) -* [Short declarations should cuddle only with other short declarations](doc/rules.md#short-declaration-should-cuddle-only-with-other-short-declarations) -* [Switch statements should only be cuddled with variables switched](doc/rules.md#switch-statements-should-only-be-cuddled-with-variables-switched) -* [Type switch statements should only be cuddled with variables switched](doc/rules.md#type-switch-statements-should-only-be-cuddled-with-variables-switched) diff --git a/vendor/github.com/bombsimon/wsl/v3/wsl.go b/vendor/github.com/bombsimon/wsl/v3/wsl.go deleted file mode 100644 index 1b139c047..000000000 --- a/vendor/github.com/bombsimon/wsl/v3/wsl.go +++ /dev/null @@ -1,1264 +0,0 @@ -package wsl - -import ( - "fmt" - "go/ast" - "go/parser" - "go/token" - "os" - "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. -// -//nolint:gocritic // It's fine to copy config struct -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 // Don't want named returns -func (p *Processor) ProcessFiles(filenames []string) ([]Result, []string) { - for _, filename := range filenames { - data, err := os.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. -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. - 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 - // } - if len(statements) == 2 && 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 c, ok := t.Call.Fun.(*ast.SelectorExpr); ok { - goCallArgs := append(p.findLHS(c.X), p.findRHS(c.X)...) - - if atLeastOneInListsMatch(calledOnLineAbove, goCallArgs) { - continue - } - } - - if c, ok := t.Call.Fun.(*ast.FuncLit); ok { - goCallArgs := append(p.findLHS(c.Body), p.findRHS(c.Body)...) - - if atLeastOneInListsMatch(assignedOnLineAbove, goCallArgs) { - 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.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)...) - case *ast.KeyValueExpr: - rhs = p.findRHS(t.Key) - rhs = append(rhs, p.findRHS(t.Value)...) - 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. -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++ - } - } - - // 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 { - 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), - ) -} diff --git a/vendor/github.com/bombsimon/wsl/v4/.gitignore b/vendor/github.com/bombsimon/wsl/v4/.gitignore new file mode 100644 index 000000000..1c8eba613 --- /dev/null +++ b/vendor/github.com/bombsimon/wsl/v4/.gitignore @@ -0,0 +1,70 @@ + +# Created by https://www.gitignore.io/api/go,vim,macos + +### Go ### +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +### Go Patch ### +/vendor/ +/Godeps/ + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + + +# End of https://www.gitignore.io/api/go,vim,macos diff --git a/vendor/github.com/bombsimon/wsl/v4/.golangci.yml b/vendor/github.com/bombsimon/wsl/v4/.golangci.yml new file mode 100644 index 000000000..543012008 --- /dev/null +++ b/vendor/github.com/bombsimon/wsl/v4/.golangci.yml @@ -0,0 +1,81 @@ +--- +run: + deadline: 1m + issues-exit-code: 1 + tests: true + skip-dirs: + - vendor$ + +output: + format: colored-line-number + print-issued-lines: false + +linters-settings: + gocognit: + min-complexity: 10 + + depguard: + list-type: blacklist + include-go-root: false + packages: + - github.com/davecgh/go-spew/spew + + misspell: + locale: US + + gocritic: + # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` + # to see all tags and checks. Empty list by default. See + # https://github.com/go-critic/go-critic#usage -> section "Tags". + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + +linters: + enable-all: true + disable: + - cyclop + - deadcode + - depguard + - dupl + - dupword + - exhaustivestruct + - exhaustruct + - forbidigo + - funlen + - gci + - gocognit + - gocyclo + - godox + - golint + - gomnd + - ifshort + - interfacer + - lll + - maintidx + - maligned + - nakedret + - nestif + - nlreturn + - nosnakecase + - paralleltest + - prealloc + - rowserrcheck + - scopelint + - structcheck + - testpackage + - varcheck + - varnamelen + - wastedassign + fast: false + + +issues: + exclude-use-default: true + max-issues-per-linter: 0 + max-same-issues: 0 + +# vim: set sw=2 ts=2 et: diff --git a/vendor/github.com/bombsimon/wsl/v4/LICENSE b/vendor/github.com/bombsimon/wsl/v4/LICENSE new file mode 100644 index 000000000..4dade6d1c --- /dev/null +++ b/vendor/github.com/bombsimon/wsl/v4/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Simon Sawert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/bombsimon/wsl/v4/README.md b/vendor/github.com/bombsimon/wsl/v4/README.md new file mode 100644 index 000000000..0bcf01d96 --- /dev/null +++ b/vendor/github.com/bombsimon/wsl/v4/README.md @@ -0,0 +1,98 @@ +# wsl - Whitespace Linter + +[![forthebadge](https://forthebadge.com/images/badges/made-with-go.svg)](https://forthebadge.com) +[![forthebadge](https://forthebadge.com/images/badges/built-with-love.svg)](https://forthebadge.com) + +[![GitHub Actions](https://github.com/bombsimon/wsl/actions/workflows/go.yml/badge.svg)](https://github.com/bombsimon/wsl/actions/workflows/go.yml) +[![Coverage Status](https://coveralls.io/repos/github/bombsimon/wsl/badge.svg?branch=master)](https://coveralls.io/github/bombsimon/wsl?branch=master) + +`wsl` is a linter that enforces a very **non scientific** vision of how to make +code more readable by enforcing empty lines at the right places. + +**This linter is aggressive** and a lot of projects I've tested it on have +failed miserably. For this linter to be useful at all I want to be open to new +ideas, configurations and discussions! Also note that some of the warnings might +be bugs or unintentional false positives so I would love an +[issue](https://github.com/bombsimon/wsl/issues/new) to fix, discuss, change or +make something configurable! + +## Installation + +```sh +# Latest release +go install github.com/bombsimon/wsl/v4/cmd/wsl + +# Main branch +go install github.com/bombsimon/wsl/v4/cmd/wsl@master +``` + +## Usage + +> **Note**: This linter provides a fixer that can fix most issues with the +> `--fix` flag. However, currently `golangci-lint` [does not support suggested +> fixes](https://github.com/golangci/golangci-lint/issues/1779) so the `--fix` +> flag in `golangci-lint` will **not** work. + +`wsl` uses the [analysis](https://pkg.go.dev/golang.org/x/tools/go/analysis) +package meaning it will operate on package level with the default analysis flags +and way of working. + +```sh +wsl --help +wsl [flags] + +wsl --allow-cuddle-declarations --fix ./... +``` + +`wsl` is also integrated in [`golangci-lint`](https://golangci-lint.run) + +```sh +golangci-lint run --no-config --disable-all --enable wsl +``` + +## Issues and configuration + +The linter suppers a few ways to configure it to satisfy more than one kind of +code style. These settings could be set either with flags or with YAML +configuration if used via `golangci-lint`. + +The supported configuration can be found [in the +documentation](doc/configuration.md). + +Below are the available checklist for any hit from `wsl`. If you do not see any, +feel free to raise an [issue](https://github.com/bombsimon/wsl/issues/new). + +> **Note**: this linter doesn't take in consideration the issues that will be +> fixed with `go fmt -s` so ensure that the code is properly formatted before +> use. + +* [Anonymous switch statements should never be cuddled](doc/rules.md#anonymous-switch-statements-should-never-be-cuddled) +* [Append only allowed to cuddle with appended value](doc/rules.md#append-only-allowed-to-cuddle-with-appended-value) +* [Assignments should only be cuddled with other assignments](doc/rules.md#assignments-should-only-be-cuddled-with-other-assignments) +* [Block should not end with a whitespace (or comment)](doc/rules.md#block-should-not-end-with-a-whitespace-or-comment) +* [Block should not start with a whitespace](doc/rules.md#block-should-not-start-with-a-whitespace) +* [Case block should end with newline at this size](doc/rules.md#case-block-should-end-with-newline-at-this-size) +* [Branch statements should not be cuddled if block has more than two lines](doc/rules.md#branch-statements-should-not-be-cuddled-if-block-has-more-than-two-lines) +* [Declarations should never be cuddled](doc/rules.md#declarations-should-never-be-cuddled) +* [Defer statements should only be cuddled with expressions on same variable](doc/rules.md#defer-statements-should-only-be-cuddled-with-expressions-on-same-variable) +* [Expressions should not be cuddled with blocks](doc/rules.md#expressions-should-not-be-cuddled-with-blocks) +* [Expressions should not be cuddled with declarations or returns](doc/rules.md#expressions-should-not-be-cuddled-with-declarations-or-returns) +* [For statement without condition should never be cuddled](doc/rules.md#for-statement-without-condition-should-never-be-cuddled) +* [For statements should only be cuddled with assignments used in the iteration](doc/rules.md#for-statements-should-only-be-cuddled-with-assignments-used-in-the-iteration) +* [Go statements can only invoke functions assigned on line above](doc/rules.md#go-statements-can-only-invoke-functions-assigned-on-line-above) +* [If statements should only be cuddled with assignments](doc/rules.md#if-statements-should-only-be-cuddled-with-assignments) +* [If statements should only be cuddled with assignments used in the if statement itself](doc/rules.md#if-statements-should-only-be-cuddled-with-assignments-used-in-the-if-statement-itself) +* [If statements that check an error must be cuddled with the statement that assigned the error](doc/rules.md#if-statements-that-check-an-error-must-be-cuddled-with-the-statement-that-assigned-the-error) +* [Only cuddled expressions if assigning variable or using from line above](doc/rules.md#only-cuddled-expressions-if-assigning-variable-or-using-from-line-above) +* [Only one cuddle assignment allowed before defer statement](doc/rules.md#only-one-cuddle-assignment-allowed-before-defer-statement) +* [Only one cuddle assignment allowed before for statement](doc/rules.md#only-one-cuddle-assignment-allowed-before-for-statement) +* [Only one cuddle assignment allowed before go statement](doc/rules.md#only-one-cuddle-assignment-allowed-before-go-statement) +* [Only one cuddle assignment allowed before if statement](doc/rules.md#only-one-cuddle-assignment-allowed-before-if-statement) +* [Only one cuddle assignment allowed before range statement](doc/rules.md#only-one-cuddle-assignment-allowed-before-range-statement) +* [Only one cuddle assignment allowed before switch statement](doc/rules.md#only-one-cuddle-assignment-allowed-before-switch-statement) +* [Only one cuddle assignment allowed before type switch statement](doc/rules.md#only-one-cuddle-assignment-allowed-before-type-switch-statement) +* [Ranges should only be cuddled with assignments used in the iteration](doc/rules.md#ranges-should-only-be-cuddled-with-assignments-used-in-the-iteration) +* [Return statements should not be cuddled if block has more than two lines](doc/rules.md#return-statements-should-not-be-cuddled-if-block-has-more-than-two-lines) +* [Short declarations should cuddle only with other short declarations](doc/rules.md#short-declaration-should-cuddle-only-with-other-short-declarations) +* [Switch statements should only be cuddled with variables switched](doc/rules.md#switch-statements-should-only-be-cuddled-with-variables-switched) +* [Type switch statements should only be cuddled with variables switched](doc/rules.md#type-switch-statements-should-only-be-cuddled-with-variables-switched) diff --git a/vendor/github.com/bombsimon/wsl/v4/analyzer.go b/vendor/github.com/bombsimon/wsl/v4/analyzer.go new file mode 100644 index 000000000..b8eac1587 --- /dev/null +++ b/vendor/github.com/bombsimon/wsl/v4/analyzer.go @@ -0,0 +1,141 @@ +package wsl + +import ( + "flag" + "strings" + + "golang.org/x/tools/go/analysis" +) + +func NewAnalyzer(config *Configuration) *analysis.Analyzer { + wa := &wslAnalyzer{config: config} + + return &analysis.Analyzer{ + Name: "wsl", + Doc: "add or remove empty lines", + Flags: wa.flags(), + Run: wa.run, + RunDespiteErrors: true, + } +} + +func defaultConfig() *Configuration { + return &Configuration{ + AllowAssignAndAnythingCuddle: false, + AllowAssignAndCallCuddle: true, + AllowCuddleDeclaration: false, + AllowMultiLineAssignCuddle: true, + AllowSeparatedLeadingComment: false, + AllowTrailingComment: false, + ForceCuddleErrCheckAndAssign: false, + ForceExclusiveShortDeclarations: false, + StrictAppend: true, + AllowCuddleWithCalls: []string{"Lock", "RLock"}, + AllowCuddleWithRHS: []string{"Unlock", "RUnlock"}, + ErrorVariableNames: []string{"err"}, + ForceCaseTrailingWhitespaceLimit: 0, + } +} + +// wslAnalyzer is a wrapper around the configuration which is used to be able to +// set the configuration when creating the analyzer and later be able to update +// flags and running method. +type wslAnalyzer struct { + config *Configuration +} + +func (wa *wslAnalyzer) flags() flag.FlagSet { + flags := flag.NewFlagSet("", flag.ExitOnError) + + // If we have a configuration set we're not running from the command line so + // we don't use any flags. + if wa.config != nil { + return *flags + } + + wa.config = defaultConfig() + + flags.BoolVar(&wa.config.AllowAssignAndAnythingCuddle, "allow-assign-and-anything", false, "Allow assignments and anything to be cuddled") + flags.BoolVar(&wa.config.AllowAssignAndCallCuddle, "allow-assign-and-call", true, "Allow assignments and calls to be cuddled (if using same variable/type)") + flags.BoolVar(&wa.config.AllowCuddleDeclaration, "allow-cuddle-declarations", false, "Allow declarations to be cuddled") + flags.BoolVar(&wa.config.AllowMultiLineAssignCuddle, "allow-multi-line-assign", true, "Allow cuddling with multi line assignments") + flags.BoolVar(&wa.config.AllowSeparatedLeadingComment, "allow-separated-leading-comment", false, "Allow empty newlines in leading comments") + flags.BoolVar(&wa.config.AllowTrailingComment, "allow-trailing-comment", false, "Allow blocks to end with a comment") + flags.BoolVar(&wa.config.ForceCuddleErrCheckAndAssign, "force-err-cuddling", false, "Force cuddling of error checks with error var assignment") + flags.BoolVar(&wa.config.ForceExclusiveShortDeclarations, "force-short-decl-cuddling", false, "Force short declarations to cuddle by themselves") + flags.BoolVar(&wa.config.StrictAppend, "strict-append", true, "Strict rules for append") + flags.IntVar(&wa.config.ForceCaseTrailingWhitespaceLimit, "force-case-trailing-whitespace", 0, "Force newlines for case blocks > this number.") + + flags.Var(&multiStringValue{slicePtr: &wa.config.AllowCuddleWithCalls}, "allow-cuddle-with-calls", "Comma separated list of idents that can have cuddles after") + flags.Var(&multiStringValue{slicePtr: &wa.config.AllowCuddleWithRHS}, "allow-cuddle-with-rhs", "Comma separated list of idents that can have cuddles before") + flags.Var(&multiStringValue{slicePtr: &wa.config.ErrorVariableNames}, "error-variable-names", "Comma separated list of error variable names") + + return *flags +} + +func (wa *wslAnalyzer) run(pass *analysis.Pass) (interface{}, error) { + for _, file := range pass.Files { + filename := pass.Fset.PositionFor(file.Pos(), false).Filename + if !strings.HasSuffix(filename, ".go") { + continue + } + + processor := newProcessorWithConfig(file, pass.Fset, wa.config) + processor.parseAST() + + for pos, fix := range processor.result { + textEdits := []analysis.TextEdit{} + for _, f := range fix.fixRanges { + textEdits = append(textEdits, analysis.TextEdit{ + Pos: f.fixRangeStart, + End: f.fixRangeEnd, + NewText: []byte("\n"), + }) + } + + pass.Report(analysis.Diagnostic{ + Pos: pos, + Category: "whitespace", + Message: fix.reason, + SuggestedFixes: []analysis.SuggestedFix{ + { + TextEdits: textEdits, + }, + }, + }) + } + } + + //nolint:nilnil // A pass don't need to return anything. + return nil, nil +} + +// multiStringValue is a flag that supports multiple values. It's implemented to +// contain a pointer to a string slice that will be overwritten when the flag's +// `Set` method is called. +type multiStringValue struct { + slicePtr *[]string +} + +// Set implements the flag.Value interface and will overwrite the pointer to the +// slice with a new pointer after splitting the flag by comma. +func (m *multiStringValue) Set(value string) error { + s := []string{} + + for _, v := range strings.Split(value, ",") { + s = append(s, strings.TrimSpace(v)) + } + + *m.slicePtr = s + + return nil +} + +// Set implements the flag.Value interface. +func (m *multiStringValue) String() string { + if m.slicePtr == nil { + return "" + } + + return strings.Join(*m.slicePtr, ", ") +} diff --git a/vendor/github.com/bombsimon/wsl/v4/wsl.go b/vendor/github.com/bombsimon/wsl/v4/wsl.go new file mode 100644 index 000000000..6fd33335a --- /dev/null +++ b/vendor/github.com/bombsimon/wsl/v4/wsl.go @@ -0,0 +1,1411 @@ +package wsl + +import ( + "fmt" + "go/ast" + "go/token" + "reflect" + "sort" + "strings" +) + +// Error reason strings. +const ( + reasonAnonSwitchCuddled = "anonymous switch statements should never be cuddled" + reasonAppendCuddledWithoutUse = "append only allowed to cuddle with appended value" + reasonAssignsCuddleAssign = "assignments should only be cuddled with other assignments" + reasonBlockEndsWithWS = "block should not end with a whitespace (or comment)" + reasonBlockStartsWithWS = "block should not start with a whitespace" + reasonCaseBlockTooCuddly = "case block should end with newline at this size" + reasonDeferCuddledWithOtherVar = "defer statements should only be cuddled with expressions on same variable" + reasonExprCuddlingNonAssignedVar = "only cuddled expressions if assigning variable or using from line above" + reasonExpressionCuddledWithBlock = "expressions should not be cuddled with blocks" + reasonExpressionCuddledWithDeclOrRet = "expressions should not be cuddled with declarations or returns" + reasonForCuddledAssignWithoutUse = "for statements should only be cuddled with assignments used in the iteration" + reasonForWithoutCondition = "for statement without condition should never be cuddled" + reasonGoFuncWithoutAssign = "go statements can only invoke functions assigned on line above" + reasonMultiLineBranchCuddle = "branch statements should not be cuddled if block has more than two lines" + reasonMustCuddleErrCheck = "if statements that check an error must be cuddled with the statement that assigned the error" + reasonNeverCuddleDeclare = "declarations should never be cuddled" + reasonOnlyCuddle2LineReturn = "return statements should not be cuddled if block has more than two lines" + reasonOnlyCuddleIfWithAssign = "if statements should only be cuddled with assignments" + reasonOnlyCuddleWithUsedAssign = "if statements should only be cuddled with assignments used in the if statement itself" + reasonOnlyOneCuddleBeforeDefer = "only one cuddle assignment allowed before defer statement" + reasonOnlyOneCuddleBeforeFor = "only one cuddle assignment allowed before for statement" + reasonOnlyOneCuddleBeforeGo = "only one cuddle assignment allowed before go statement" + reasonOnlyOneCuddleBeforeIf = "only one cuddle assignment allowed before if statement" + reasonOnlyOneCuddleBeforeRange = "only one cuddle assignment allowed before range statement" + reasonOnlyOneCuddleBeforeSwitch = "only one cuddle assignment allowed before switch statement" + reasonOnlyOneCuddleBeforeTypeSwitch = "only one cuddle assignment allowed before type switch statement" + reasonRangeCuddledWithoutUse = "ranges should only be cuddled with assignments used in the iteration" + reasonShortDeclNotExclusive = "short declaration should cuddle only with other short declarations" + reasonSwitchCuddledWithoutUse = "switch statements should only be cuddled with variables switched" + reasonTypeSwitchCuddledWithoutUse = "type switch statements should only be cuddled with variables switched" +) + +// 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" +) + +// Configuration represents configurable settings for the linter. +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 + + // AllowAssignAndAnythingCuddle 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 +} + +// fix is a range to fixup. +type fix struct { + fixRangeStart token.Pos + fixRangeEnd token.Pos +} + +// result represents the result of one error. +type result struct { + fixRanges []fix + reason string +} + +// processor is the type that keeps track of the file and fileset and holds the +// results from parsing the AST. +type processor struct { + config *Configuration + file *ast.File + fileSet *token.FileSet + result map[token.Pos]result + warnings []string +} + +// newProcessorWithConfig will create a Processor with the passed configuration. +func newProcessorWithConfig(file *ast.File, fileSet *token.FileSet, cfg *Configuration) *processor { + return &processor{ + config: cfg, + file: file, + fileSet: fileSet, + result: make(map[token.Pos]result), + } +} + +// parseAST will parse the AST attached to the Processor instance. +func (p *processor) parseAST() { + 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. +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. + 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 + } + + nStatementsBefore := func(n int) bool { + if i < n { + return false + } + + for j := 1; j < n; j++ { + s1 := statements[i-j] + s2 := statements[i-(j+1)] + + if p.nodeStart(s1)-1 != p.nodeEnd(s2) { + return false + } + } + + return true + } + + nStatementsAfter := func(n int) bool { + if len(statements)-1 < i+n { + return false + } + + for j := 0; j < n; j++ { + s1 := statements[i+j] + s2 := statements[i+j+1] + + if p.nodeEnd(s1)+1 != p.nodeStart(s2) { + return false + } + } + + return true + } + + 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 + // } + if len(statements) == 2 && 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) { + var reportNode ast.Node = previousStatement + + cm := ast.NewCommentMap(p.fileSet, stmt, p.file.Comments) + if cg, ok := cm[stmt]; ok && len(cg) > 0 { + for _, c := range cg { + if c.Pos() > previousStatement.End() && c.End() < stmt.Pos() { + reportNode = c + } + } + } + + p.addErrorRange( + stmt.Pos(), + reportNode.End(), + reportNode.End(), + reasonShortDeclNotExclusive, + ) + } else if p.isShortDecl(previousStatement) && !p.isShortDecl(stmt) { + p.addErrorRange( + previousStatement.Pos(), + stmt.Pos(), + stmt.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 + } + } + + reportNewlineTwoLinesAbove := func(n1, n2 ast.Node, reason string) { + if atLeastOneInListsMatch(rightAndLeftHandSide, assignedOnLineAbove) || + atLeastOneInListsMatch(assignedOnLineAbove, calledOrAssignedFirstInBlock) { + // If both the assignment on the line above _and_ the assignment + // two lines above is part of line or first in block, add the + // newline as if non were. + _, isAssignmentTwoLinesAbove := statements[i-2].(*ast.AssignStmt) + assignedTwoLinesAbove := p.findLHS(statements[i-2]) + + if isAssignmentTwoLinesAbove && + (atLeastOneInListsMatch(rightAndLeftHandSide, assignedTwoLinesAbove) || + atLeastOneInListsMatch(assignedTwoLinesAbove, calledOrAssignedFirstInBlock)) { + p.addWhitespaceBeforeError(n1, reason) + } else { + // If the variable on the line above is allowed to be + // cuddled, break two lines above so we keep the proper + // cuddling. + p.addErrorRange(n1.Pos(), n2.Pos(), n2.Pos(), reason) + } + } else { + // If not, break here so we separate the cuddled variable. + p.addWhitespaceBeforeError(n1, reason) + } + } + + 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.addErrorRange( + stmt.Pos(), + previousStatement.End(), + stmt.Pos(), + reasonMustCuddleErrCheck, + ) + } + } + + continue + } + + if len(assignedOnLineAbove) == 0 { + p.addWhitespaceBeforeError(t, reasonOnlyCuddleIfWithAssign) + continue + } + + if nStatementsBefore(2) { + reportNewlineTwoLinesAbove(t, statements[i-1], reasonOnlyOneCuddleBeforeIf) + continue + } + + if atLeastOneInListsMatch(rightAndLeftHandSide, assignedOnLineAbove) { + continue + } + + if atLeastOneInListsMatch(assignedOnLineAbove, calledOrAssignedFirstInBlock) { + continue + } + + p.addWhitespaceBeforeError(t, reasonOnlyCuddleWithUsedAssign) + case *ast.ReturnStmt: + if isLastStatementInBlockOfOnlyTwoLines() { + continue + } + + p.addWhitespaceBeforeError(t, reasonOnlyCuddle2LineReturn) + case *ast.BranchStmt: + if isLastStatementInBlockOfOnlyTwoLines() { + continue + } + + p.addWhitespaceBeforeError(t, 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.addWhitespaceBeforeError(t, reasonAppendCuddledWithoutUse) + } + } + + continue + } + + switch previousStatement.(type) { + case *ast.AssignStmt, *ast.IncDecStmt: + 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.addWhitespaceBeforeError(t, reasonAssignsCuddleAssign) + case *ast.IncDecStmt: + switch previousStatement.(type) { + case *ast.AssignStmt, *ast.IncDecStmt: + continue + } + + p.addWhitespaceBeforeError(t, reasonAssignsCuddleAssign) + + case *ast.DeclStmt: + if !p.config.AllowCuddleDeclaration { + p.addWhitespaceBeforeError(t, reasonNeverCuddleDeclare) + } + case *ast.ExprStmt: + switch previousStatement.(type) { + case *ast.DeclStmt, *ast.ReturnStmt: + if p.config.AllowAssignAndCallCuddle && p.config.AllowCuddleDeclaration { + continue + } + + p.addWhitespaceBeforeError(t, reasonExpressionCuddledWithDeclOrRet) + case *ast.IfStmt, *ast.RangeStmt, *ast.SwitchStmt: + p.addWhitespaceBeforeError(t, 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.addWhitespaceBeforeError(t, reasonExprCuddlingNonAssignedVar) + } + case *ast.RangeStmt: + if nStatementsBefore(2) { + reportNewlineTwoLinesAbove(t, statements[i-1], reasonOnlyOneCuddleBeforeRange) + continue + } + + if !atLeastOneInListsMatch(rightAndLeftHandSide, assignedOnLineAbove) { + if !atLeastOneInListsMatch(assignedOnLineAbove, calledOrAssignedFirstInBlock) { + p.addWhitespaceBeforeError(t, reasonRangeCuddledWithoutUse) + } + } + case *ast.DeferStmt: + if _, ok := previousStatement.(*ast.DeferStmt); ok { + // We may cuddle multiple defers to group logic. + continue + } + + if nStatementsBefore(2) { + // We allow cuddling defer if the defer references something + // used two lines above. + // There are several reasons to why we do this. + // Originally there was a special use case only for "Close" + // + // https://github.com/bombsimon/wsl/issues/31 which links to + // resp, err := client.Do(req) + // if err != nil { + // return err + // } + // defer resp.Body.Close() + // + // After a discussion in a followup issue it makes sense to not + // only hard code `Close` but for anything that's referenced two + // statements above. + // + // https://github.com/bombsimon/wsl/issues/85 + // db, err := OpenDB() + // require.NoError(t, err) + // defer db.Close() + // + // All of this is only allowed if there's exactly three cuddled + // statements, otherwise the regular rules apply. + if !nStatementsBefore(3) && !nStatementsAfter(1) { + variablesTwoLinesAbove := append(p.findLHS(statements[i-2]), p.findRHS(statements[i-2])...) + if atLeastOneInListsMatch(rightHandSide, variablesTwoLinesAbove) { + continue + } + } + + reportNewlineTwoLinesAbove(t, statements[i-1], reasonOnlyOneCuddleBeforeDefer) + + 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 + // above. 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.addWhitespaceBeforeError(t, reasonDeferCuddledWithOtherVar) + } + case *ast.ForStmt: + if len(rightAndLeftHandSide) == 0 { + p.addWhitespaceBeforeError(t, reasonForWithoutCondition) + continue + } + + if nStatementsBefore(2) { + reportNewlineTwoLinesAbove(t, statements[i-1], reasonOnlyOneCuddleBeforeFor) + 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.addWhitespaceBeforeError(t, reasonForCuddledAssignWithoutUse) + } + } + case *ast.GoStmt: + if _, ok := previousStatement.(*ast.GoStmt); ok { + continue + } + + if nStatementsBefore(2) { + reportNewlineTwoLinesAbove(t, statements[i-1], reasonOnlyOneCuddleBeforeGo) + continue + } + + if c, ok := t.Call.Fun.(*ast.SelectorExpr); ok { + goCallArgs := append(p.findLHS(c.X), p.findRHS(c.X)...) + + if atLeastOneInListsMatch(calledOnLineAbove, goCallArgs) { + continue + } + } + + if c, ok := t.Call.Fun.(*ast.FuncLit); ok { + goCallArgs := append(p.findLHS(c.Body), p.findRHS(c.Body)...) + + if atLeastOneInListsMatch(assignedOnLineAbove, goCallArgs) { + continue + } + } + + if !atLeastOneInListsMatch(rightAndLeftHandSide, assignedOnLineAbove) { + p.addWhitespaceBeforeError(t, reasonGoFuncWithoutAssign) + } + case *ast.SwitchStmt: + if nStatementsBefore(2) { + reportNewlineTwoLinesAbove(t, statements[i-1], reasonOnlyOneCuddleBeforeSwitch) + continue + } + + if !atLeastOneInListsMatch(rightAndLeftHandSide, assignedOnLineAbove) { + if len(rightAndLeftHandSide) == 0 { + p.addWhitespaceBeforeError(t, reasonAnonSwitchCuddled) + } else { + p.addWhitespaceBeforeError(t, reasonSwitchCuddledWithoutUse) + } + } + case *ast.TypeSwitchStmt: + if nStatementsBefore(2) { + reportNewlineTwoLinesAbove(t, statements[i-1], reasonOnlyOneCuddleBeforeTypeSwitch) + 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.addWhitespaceBeforeError(t, reasonTypeSwitchCuddledWithoutUse) + } + } + case *ast.CaseClause, *ast.CommClause: + // Case clauses will be checked by not allowing leading of 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] + } + } + } + + // If statement bodies will be parsed already when finding block bodies. + // The reason is because if/else-if/else chains is nested in the AST + // where the else bit is a part of the if statement. Since if statements + // is the only statement that can be chained like this we exclude it + // from parsing it again here. + if _, ok := stmt.(*ast.IfStmt); !ok { + 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.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)...) + case *ast.KeyValueExpr: + rhs = p.findRHS(t.Key) + rhs = append(rhs, p.findRHS(t.Value)...) + 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.BlockStmt: + return []*ast.BlockStmt{t} + case *ast.AssignStmt: + for _, x := range t.Rhs { + blocks = append(blocks, p.findBlockStmt(x)...) + } + case *ast.CallExpr: + blocks = append(blocks, p.findBlockStmt(t.Fun)...) + + for _, x := range t.Args { + blocks = append(blocks, p.findBlockStmt(x)...) + } + 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)...) + case *ast.IfStmt: + blocks = append([]*ast.BlockStmt{t.Body}, p.findBlockStmt(t.Else)...) + } + + 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. +func (p *processor) findLeadingAndTrailingWhitespaces(ident *ast.Ident, stmt, nextStatement ast.Node) { + var ( + commentMap = ast.NewCommentMap(p.fileSet, stmt, p.file.Comments) + blockStatements []ast.Stmt + blockStartLine int + blockEndLine int + blockStartPos token.Pos + blockEndPos token.Pos + isCase bool + ) + + // 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 + isCase = true + case *ast.CommClause: + blockStatements = t.Body + blockStartPos = t.Colon + isCase = true + 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.PositionFor(blockStartPos, false).Line + blockEndLine = p.fileSet.PositionFor(blockEndPos, false).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] + ) + + // Get the comment related to the first statement, we do allow comments in + // the beginning of a block before the first statement. + var ( + openingNodePos = blockStartPos + 1 + lastLeadingComment ast.Node + ) + + var ( + firstStatementCommentGroups []*ast.CommentGroup + lastStatementCommentGroups []*ast.CommentGroup + ) + + if cg, ok := commentMap[firstStatement]; ok && !isCase { + firstStatementCommentGroups = cg + } else { + // TODO: Just like with trailing whitespaces comments in a case block is + // tied to the last token of the first statement. For now we iterate over + // all comments in the stmt and grab those that's after colon and before + // first statement. + for _, cg := range commentMap { + if len(cg) < 1 { + continue + } + + // If we have comments and the last comment ends before the first + // statement and the node is after the colon, this must be the node + // mapped to comments. + for _, c := range cg { + if c.End() < firstStatement.Pos() && c.Pos() > blockStartPos { + firstStatementCommentGroups = append(firstStatementCommentGroups, c) + } + } + + // And same if we have comments where the first comment is after the + // last statement but before the next statement (next case). As with + // the other things, if there is not next statement it's no next + // case and the logic will be handled when parsing the block. + if nextStatement == nil { + continue + } + + for _, c := range cg { + if c.Pos() > lastStatement.End() && c.End() < nextStatement.Pos() { + lastStatementCommentGroups = append(lastStatementCommentGroups, c) + } + } + } + + // Since the comments come from a map they might not be ordered meaning + // that the last and first comment groups can be in the wrong order. We + // fix this by sorting all comments by pos after adding them all to the + // slice. + sort.Slice(firstStatementCommentGroups, func(i, j int) bool { + return firstStatementCommentGroups[i].Pos() < firstStatementCommentGroups[j].Pos() + }) + + sort.Slice(lastStatementCommentGroups, func(i, j int) bool { + return lastStatementCommentGroups[i].Pos() < lastStatementCommentGroups[j].Pos() + }) + } + + for _, commentGroup := range firstStatementCommentGroups { + // If the comment group is on the same line as the block start + // (LBrace) we should not consider it. + if p.nodeEnd(commentGroup) == blockStartLine { + openingNodePos = commentGroup.End() + 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 never allow leading whitespace for the first comment. + if lastLeadingComment == nil && p.nodeStart(commentGroup)-1 != blockStartLine { + p.addErrorRange( + openingNodePos, + openingNodePos, + commentGroup.Pos(), + reasonBlockStartsWithWS, + ) + } + + // If lastLeadingComment is set this is not the first comment so we + // should remove whitespace between them if we don't explicitly + // allow it. + if lastLeadingComment != nil && !p.config.AllowSeparatedLeadingComment { + if p.nodeStart(commentGroup)+1 != p.nodeEnd(lastLeadingComment) { + p.addErrorRange( + openingNodePos, + lastLeadingComment.End(), + commentGroup.Pos(), + reasonBlockStartsWithWS, + ) + } + } + + lastLeadingComment = commentGroup + } + + lastNodePos := openingNodePos + if lastLeadingComment != nil { + lastNodePos = lastLeadingComment.End() + blockStartLine = p.nodeEnd(lastLeadingComment) + } + + // Check if we have a whitespace between the last node which can be the + // Lbrace, a comment on the same line or the last comment if we have + // comments inside the actual block and the first statement. This is never + // allowed. + if p.nodeStart(firstStatement)-1 != blockStartLine { + p.addErrorRange( + openingNodePos, + lastNodePos, + firstStatement.Pos(), + reasonBlockStartsWithWS, + ) + } + + // If the blockEndLine is not 0 we're a regular block (not case). + if blockEndLine != 0 { + // We don't want to reject example functions since they have to end with + // a comment. + if isExampleFunc(ident) { + return + } + + var ( + lastNode ast.Node = lastStatement + trailingComments []ast.Node + ) + + // Check if we have an comments _after_ the last statement and update + // the last node if so. + if c, ok := commentMap[lastStatement]; ok { + lastComment := c[len(c)-1] + if lastComment.Pos() > lastStatement.End() && lastComment.Pos() < stmt.End() { + lastNode = lastComment + } + } + + // TODO: This should be improved. + // The trailing comments are mapped to the last statement item which can + // be anything depending on what the last statement is. + // In `fmt.Println("hello")`, trailing comments will be mapped to + // `*ast.BasicLit` for the "hello" string. + // A short term improvement can be to cache this but for now we naively + // iterate over all items when we check a block. + for _, commentGroups := range commentMap { + for _, commentGroup := range commentGroups { + if commentGroup.Pos() < lastNode.End() || commentGroup.End() > stmt.End() { + continue + } + + trailingComments = append(trailingComments, commentGroup) + } + } + + // TODO: Should this be relaxed? + // Given the old code we only allowed trailing newline if it was + // directly tied to the last statement so for backwards compatibility + // we'll do the same. This means we fail all but the last whitespace + // even when allowing trailing comments. + for _, comment := range trailingComments { + if p.nodeStart(comment)-p.nodeEnd(lastNode) > 1 { + p.addErrorRange( + blockEndPos, + lastNode.End(), + comment.Pos(), + reasonBlockEndsWithWS, + ) + } + + lastNode = comment + } + + if !p.config.AllowTrailingComment && p.nodeEnd(stmt)-1 != p.nodeEnd(lastStatement) { + p.addErrorRange( + blockEndPos, + lastNode.End(), + stmt.End()-1, + reasonBlockEndsWithWS, + ) + } + + return + } + + // Nothing to do if we're not looking for enforced newline. + if p.config.ForceCaseTrailingWhitespaceLimit == 0 { + 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 nextStatement.(type) { + case *ast.CaseClause, *ast.CommClause: + default: + // No more cases + return + } + + var closingNode ast.Node = lastStatement + for _, commentGroup := range lastStatementCommentGroups { + // TODO: In future versions we might want to close the gaps between + // comments. However this is not currently reported in v3 so we + // won't add this for now. + // if p.nodeStart(commentGroup)-1 != p.nodeEnd(closingNode) {} + closingNode = commentGroup + } + + totalRowsInCase := p.nodeEnd(closingNode) - blockStartLine + if totalRowsInCase < p.config.ForceCaseTrailingWhitespaceLimit { + return + } + + if p.nodeEnd(closingNode)+1 == p.nodeStart(nextStatement) { + p.addErrorRange( + closingNode.Pos(), + closingNode.End(), + closingNode.End(), + 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.PositionFor(node.Pos(), false).Line +} + +func (p *processor) nodeEnd(node ast.Node) int { + line := p.fileSet.PositionFor(node.End(), false).Line + + if isEmptyLabeledStmt(node) { + return p.fileSet.PositionFor(node.Pos(), false).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 +} + +func (p *processor) addWhitespaceBeforeError(node ast.Node, reason string) { + p.addErrorRange(node.Pos(), node.Pos(), node.Pos(), reason) +} + +func (p *processor) addErrorRange(reportAt, start, end token.Pos, reason string) { + report, ok := p.result[reportAt] + if !ok { + report = result{ + reason: reason, + fixRanges: []fix{}, + } + } + + report.fixRanges = append(report.fixRanges, fix{ + fixRangeStart: start, + fixRangeEnd: end, + }) + + p.result[reportAt] = report +} + +func (p *processor) addWarning(w string, pos token.Pos, t interface{}) { + position := p.fileSet.PositionFor(pos, false) + + p.warnings = append(p.warnings, + fmt.Sprintf("%s:%d: %s (%T)", position.Filename, position.Line, w, t), + ) +} -- cgit mrf-deployment