diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index d8d91e4..89bbe28 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,6 +2,8 @@ name: CI on: push: + branches: + - main pull_request: jobs: @@ -9,53 +11,51 @@ jobs: name: golangci-lint runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: '1.20' - - uses: actions/checkout@v3 - - uses: golangci/golangci-lint-action@v3 + go-version: stable + - uses: golangci/golangci-lint-action@v8 + with: + version: v2.1.6 tests: - # run after golangci-lint action to not produce duplicated errors name: tests needs: golangci-lint strategy: matrix: go: - - '1.20' + - oldstable + - stable os: - ubuntu-latest runs-on: ${{ matrix.os }} steps: - - name: Install Go - uses: actions/setup-go@v3 - with: - go-version: ${{ matrix.go }} - - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - - name: Setup cache - uses: actions/cache@v3 + - name: Install Go + uses: actions/setup-go@v5 with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: ${{ runner.os }}-go- + go-version: ${{ matrix.go }} - name: Test code run: go test -race -v ./... + - name: Run + run: go run ./cmd/wsl/ ./... + coverage: runs-on: ubuntu-latest steps: - - name: Install Go + - name: Checkout code if: success() - uses: actions/setup-go@v3 - with: - go-version: '1.20' + uses: actions/checkout@v4 - - name: Checkout code - uses: actions/checkout@v3 + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: stable - name: Calc coverage run: | diff --git a/.golangci.yml b/.golangci.yml index 5430120..8386303 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,81 +1,82 @@ --- -run: - deadline: 1m - issues-exit-code: 1 - tests: true - skip-dirs: - - vendor$ +version: "2" 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 + formats: + text: + path: stdout + print-issued-lines: false linters: - enable-all: true + default: all disable: - cyclop - - deadcode - depguard - dupl - dupword - - exhaustivestruct + - err113 - exhaustruct - forbidigo + - funcorder - funlen - - gci - gocognit - gocyclo + - godot - godox - - golint - - gomnd - - ifshort - - interfacer - lll - maintidx - - maligned + - mnd - nakedret - nestif - nlreturn - - nosnakecase - paralleltest - prealloc - rowserrcheck - - scopelint - - structcheck + - tagliatelle - testpackage - - varcheck + - tparallel - varnamelen - wastedassign - fast: false + settings: + depguard: + rules: + main: + deny: + - pkg: github.com/davecgh/go-spew/spew + desc: not allowed + gocognit: + min-complexity: 10 + 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 + misspell: + locale: US + + exclusions: + presets: + - comments + - common-false-positives + - std-error-handling issues: - exclude-use-default: true max-issues-per-linter: 0 max-same-issues: 0 - +formatters: + enable: + - gofmt + - gofumpt + - goimports + settings: + gofmt: + rewrite-rules: + - pattern: "interface{}" + replacement: "any" # vim: set sw=2 ts=2 et: diff --git a/CHECKS.md b/CHECKS.md new file mode 100644 index 0000000..a26f209 --- /dev/null +++ b/CHECKS.md @@ -0,0 +1,1287 @@ +# Checks + +This document describes all the checks done by `wsl` with examples of what's not +allowed and what's allowed. + +## `assign` + +Assign (`foo := bar`) or re-assignments (`foo = bar`) should only be cuddled +with other assignments or increment/decrement. + + + + + + + +
BadGood
+ +```go +if true { + fmt.Println("hello") +} +a := 1 // 1 + +defer func() { + fmt.Println("hello") +}() +a := 1 // 2 +``` + + + +```go +if true { + fmt.Println("hello") +} + +a := 1 + +defer func() { + fmt.Println("hello") +}() + +a := 1 + +a := 1 +b := 2 +c := 3 +``` + +
+ +1 Not an assign statement above + +2 Not an assign statement above + + + +
+ +## `branch` + +> Configurable via `branch-max-lines` + +Branch statement (`break`, `continue`, `fallthrough`, `goto`) should only be +cuddled if the block is less than `n` lines where `n` is the value of +`branch-max-statements`. + + + + + + + +
BadGood
+ +```go +for { + a, err : = SomeFn() + if err != nil { + return err + } + + fmt.Println(a) + break // 1 +} +``` + + + +```go +for { + a, err : = SomeFn() + if err != nil { + return err + } + + fmt.Println(a) + + break +} + +for { + fmt.Println("hello") + break +} +``` + +
+ +1 Block is more than 2 lines so should be a blank line above + + + +
+ +## `decl` + +Declarations should never be cuddled. When grouping multiple declarations +together they should be declared in the same group with parenthesis into a +single statement. The benefit of this is that it also aligns the declaration or +assignment increasing readability. + +> **NOTE** The fixer can't do smart adjustments and currently only add +> whitespaces. +> This is tracked in [#21](https://github.com/bombsimon/wsl/issues/121) + + + + + + + +
BadGood
+ +```go +var a string +var b int // 1 + +const a = 1 +const b = 2 // 2 + +a := 1 +var b string // 3 + +fmt.Println("hello") +var a string // 4 +``` + + + +```go +var ( + a string + b int +) + +const ( + a = 1 + b = 2 +) + +a := 1 + +var b string + +fmt.Println("hello") + +var a string +``` + +
+ +1 Multiple declarations should be grouped to one + +2 Multiple declarations should be grouped to one + +3 Declaration should always have a whitespace above + +4 Declaration should always have a whitespace above + + + +
+ +## `defer` + +Deferring execution should only be used directly in the context of what's being +deferred and there should only be one statement above. + + + + + + + +
BadGood
+ +```go +val, closeFn := SomeFn() +val2 := fmt.Sprintf("v-%s", val) +fmt.Println(val) +defer closeFn() // 1 + +defer fn1() +a := 1 +defer fn3() // 2 + +f, err := os.Open("/path/to/f.txt") +if err != nil { + return err +} + +lines := ReadFile(f) +trimLines(lines) +defer f.Close() // 3 +``` + + + +```go +val, closeFn := SomeFn() +defer closeFn() + +defer fn1() +defer fn2() +defer fn3() + +f, err := os.Open("/path/to/f.txt") +if err != nil { + return err +} +defer f.Close() + +m.Lock() +defer m.Unlock() +``` + +
+ +1 More than a single statement between `defer` and `closeFn` + +2 `a` is not used in expression + +3 More than a single statement between `defer` and `f.Close` + + + +
+ +## `expr` + +Expressions can be multiple things and a big part of them are not handled by +`wsl`. However all function calls are expressions which can be verified. + + + + + + + +
BadGood
+ +```go +a := 1 +b := 2 +fmt.Println("not b") // 1 +``` + + + +```go +a := 1 +b := 2 + +fmt.Println("not b") + +a := 1 +fmt.Println(a) +``` + +
+ +1 `b` is not used in expression + + + +
+ +## `for` + +> Configurable via `allow-first-in-block` to allow cuddling if the variable is +> used _first_ in the block (enabled by default). +> +> Configurable via `allow-whole-block` to allow cuddling if the variable is used +> _anywhere_ in the following block (disabled by default). +> +> See [Configuration](#configuration) for details. + + + + + + + +
BadGood
+ +```go +i := 0 +for j := 0; j < 3; j++ { // 1 + fmt.Println(j) +} + +a := 0 +i := 3 +for j := 0; j < i; j++ { // 2 + fmt.Println(j) +} + +x := 1 +for { // 3 + fmt.Println("hello") + break +} +``` + + + +```go +i := 0 +for j := 0; j < i; j++ { + fmt.Println(j) +} + +a := 0 + +i := 3 +for j := 0; j < i; j++ { + fmt.Println(j) +} + +// Allowed with `allow-first-in-block` +x := 1 +for { + x++ + break +} + +// Allowed with `allow-whole-block` +x := 1 +for { + fmt.Println("hello") + + if shouldIncrement() { + x++ + } +} +``` + +
+ +1 `i` is not used in expression + +2 More than one variable above statement + +3 No variable in expression + + + +
+ +## `go` + + + + + + + +
BadGood
+ +```go +someFunc := func() {} +go anotherFunc() // 1 + +x := 1 +go func () // 2 + fmt.Println(y) +}() + +someArg := 1 +go Fn(notArg) // 3 +``` + + + +```go +someFunc := func() {} +go someFunc() + +x := 1 +go func (s string) { + fmt.Println(s) +}(x) + +someArg := 1 +go Fn(someArg) +``` + +
+ +1 `someFunc` is not used in expression + +2 `x` is not used in expression + +3 `someArg` is not used in expression + + + +
+ +## `if` + +> Configurable via `allow-first-in-block` to allow cuddling if the variable is +> used _first_ in the block (enabled by default). +> +> Configurable via `allow-whole-block` to allow cuddling if the variable is used +> _anywhere_ in the following block (disabled by default). +> +> See [Configuration](#configuration) for details. + +`if` statements are one of several block statements (a statement with a block) +that can have some form of expression or condition. To make block context more +readable, only one variable is allowed immediately above the `if` statement and +the variable must be used in the condition (unless configured otherwise). + + + + + + + +
BadGood
+ +```go +x := 1 +if y > 1 { // 1 + fmt.Println("y > 1") +} + +a := 1 +b := 2 +if b > 1 { // 2 + fmt.Println("a > 1") +} + +a := 1 +b := 2 +if a > 1 { // 3 + fmt.Println("a > 1") +} + +a := 1 +b := 2 +if notEvenAOrB() { // 4 + fmt.Println("not a or b") +} + +a := 1 +x, err := SomeFn() // 5 +if err != nil { + return err +} +``` + + + +```go +x := 1 + +if y > 1 { + fmt.Println("y > 1") +} + +a := 1 + +b := 2 +if b > 1 { + fmt.Println("a > 1") +} + +b := 2 + +a := 1 +if a > 1 { + fmt.Println("a > b") +} + +a := 1 +b := 2 + +if notEvenAOrB() { + fmt.Println("not a or b") +} + +a := 1 + +x, err := SomeFn() +if err != nil { + return err +} + +// Allowed with `allow-first-in-block` +x := 1 +if xUsedFirstInBlock() { + x = 2 +} + +// Allowed with `allow-whole-block` +x := 1 +if xUsedLaterInBlock() { + fmt.Println("will use x later") + + if orEvenNestedWouldWork() { + x = 3 + } +} +``` + +
+ +1 `x` is not used in expression + +2 More than one variable above statement + +3 `b` is not used in expression and too many statements + +4 No variable in expression + +5 More than one variable above statement + + + +
+ +## `inc-dec` + + + + + + + +
BadGood
+ +```go +i := 1 + +if true { + fmt.Println("hello") +} +i++ // 1 + +defer func() { + fmt.Println("hello") +}() +i++ // 2 +``` + + + +```go +i := 1 +i++ + +i-- +j := i +j++ +``` + +
+ +1 Not an assign or inc/dec statement above + +2 Not an assign or inc/dec statement above + + + +
+ +## `label` + +Labels should never be cuddled. Labels in itself is often a symptom of big scope +and split context and because of that should always have an empty line above. + + + + + + + +
BadGood
+ +```go +L1: + if true { + _ = 1 + } +L2: // 1 + if true { + _ = 1 + } +``` + + + +```go +L1: + if true { + _ = 1 + } + +L2: + if true { + _ = 1 + } +``` + +
+ +1 Labels should always have a whitespabe above + + + +
+ +## `range` + +> Configurable via `allow-first-in-block` to allow cuddling if the variable is +> used _first_ in the block (enabled by default). +> +> Configurable via `allow-whole-block` to allow cuddling if the variable is used +> _anywhere_ in the following block (disabled by default). +> +> See [Configuration](#configuration) for details. + + + + + + + +
BadGood
+ +```go +someRange := []int{1, 2, 3} +for _, i := range thisIsNotSomeRange { // 1 + fmt.Println(i) +} + +x := 1 +for i := range make([]int, 3) { // 2 + fmt.Println("hello") + break +} + +s1 := []int{1, 2, 3} +s2 := []int{3, 2, 1} +for _, v := range s2 { // 3 + fmt.Println(v) +} +``` + + + +```go +someRange := []int{1, 2, 3} + +for _, i := range thisIsNotSomeRange { + fmt.Println(i) +} + +someRange := []int{1, 2, 3} +for _, i := range someRange { + fmt.Println(i) +} + +notARange := 1 +for i := range returnsRange(notARange) { + fmt.Println(i) +} + +s1 := []int{1, 2, 3} + +s2 := []int{3, 2, 1} +for _, v := range s2 { + fmt.Println(v) +} +``` + +
+ +1 `someRange` is not used in expression + +2 `x` is not used in expression + +3 More than one variable above statement + + + +
+ +## `return` + +> Configurable via `branch-max-lines` + +Return statements is an important statement that is easiy to miss in larger code +blocks. To better visualize the `return` statement and that the method is +returning it should always be followed by a blank line unless the scope is as +small as `branch-max-lines`. + + + + + + + +
BadGood
+ +```go +func Fn() int { + x, err := someFn() + if err != nil { + panic(err) + } + + fmt.Println(x) + return // 1 +} +``` + + + +```go +func Fn() int { + x, err := someFn() + if err != nil { + panic(err) + } + + fmt.Println(x) + + return +} +``` + +
+ +1 Block is more than 2 lines so should be a blank line above + + + +
+ +## `select` + +> Configurable via `allow-first-in-block` to allow cuddling if the variable is +> used _first_ in the block (enabled by default). +> +> Configurable via `allow-whole-block` to allow cuddling if the variable is used +> _anywhere_ in the following block (disabled by default). +> +> See [Configuration](#configuration) for details. + + + + + + + +
BadGood
+ +```go +x := 0 +select { // 1 +case <-time.After(time.Second): + // ... +case <-stop: + // ... +} +``` + + + +```go +x := 0 + +select { +case <-time.After(time.Second): + // ... +case <-stop: + // ... +} + +// Allowed with `allow-whole-block` +x := 1 +select { +case <-time.After(time.Second): + // ... +case <-stop: + Fn(x) +} +``` + +
+ +1 `x` is not used in expression + + + +
+ +## `send` + +Send statements should only be cuddled with a single variable that is used on +the line above. + + + + + + + +
BadGood
+ +```go +a := 1 +ch <- 1 // 1 + +b := 2 +<-ch // 2 +``` + + + +```go +a := 1 +ch <- a + +b := 1 + +<-ch +``` + +
+ +1 `a` is not used in expression + +2 `b` is not used in expression + + + +
+ +## `switch` + +> Configurable via `allow-first-in-block` to allow cuddling if the variable is +> used _first_ in the block (enabled by default). +> +> Configurable via `allow-whole-block` to allow cuddling if the variable is used +> _anywhere_ in the following block (disabled by default). +> +> See [Configuration](#configuration) for details. + + + + + + + +
BadGood
+ +```go +x := 0 +switch y { // 1 +case 1: + // ... +case 2: + // ... +} + + +x := 0 +y := 1 +switch y { // 2 +case 1: + // ... +case 2: + // ... +} +``` + + + +```go +x := 0 + +switch y { +case 1: + // ... +case 2: + // ... +} + + +x := 0 + +y := 1 +switch y { +case 1: + // ... +case 2: + // ... +} + +// Allowed with `allow-whole-block` +x := 1 +switch y { +case 1: + // ... +case 2: + fmt.Println(x) +} +``` + +
+ +1 `x` is not used in expression + +2 More than one variable above statement + + + +
+ +## `type-switch` + +> Configurable via `allow-first-in-block` to allow cuddling if the variable is +> used _first_ in the block (enabled by default). +> +> Configurable via `allow-whole-block` to allow cuddling if the variable is used +> _anywhere_ in the following block (disabled by default). +> +> See [Configuration](#configuration) for details. + + + + + + + +
BadGood
+ +```go +x := someType() +switch y.(type) { // 1 +case int32: + // ... +case int64: + // ... +} + + +x := 0 +y := someType() +switch y.(type) { +case int32: + // ... +case int64: + // ... +} +``` + + + +```go +x := someType() + +switch y.(type) { +case int32: + // ... +case int64: + // ... +} + + +x := 0 + +y := someType() +switch y.(type) { +case int32: + // ... +case int64: + // ... +} + +// Allowed with `allow-whole-block` +x := 1 +switch y.(type) { +case int32: + // ... +case int64: + fmt.Println(x) +} +``` + +
+ +1 `x` is not used in expression + + + +
+ +## `append` + +Append enables strict `append` checking where assignments that are +re-assignments with `append` (e.g. `x = append(x, y)`) is only allowed to be +cuddled with other assignments if the `append` uses the variable on the line +above. + + + + + + + +
BadGood
+ +```go +s := []string{} + +a := 1 +s = append(s, 2) // 1 +b := 3 +s = append(s, a) // 2 +``` + + + +```go +s := []string{} + +a := 1 +s = append(s, a) + +b := 3 + +s = append(s, 2) +``` + +
+ +1 `a` is not used in append + +2 `b` is not used in append + + + +
+ +## `assign-exclusive` + +Assign exclusive does not allow mixing new assignments (`:=`) with +re-assignments (`=`). + + + + + + + +
BadGood
+ +```go +a := 1 +b = 2 // 1 +c := 3 // 2 +d = 4 // 3 +``` + + + +```go +a := 1 +c := 3 + +b = 2 +d = 4 +``` + +
+ +1 `a` is not a re-assignment + +2 `b` is not a new assignment + +3 `c` is not a re-assignment + + + +
+ +## `assign-expr` + +Assignments are allowed to be cuddled with expressions, primarily to support +mixing assignments and function calls which can often make sense in shorter +flows. By enabling this check `wsl` will ensure assignments are not cuddled with +expressions. + + + + + + + +
BadGood
+ +```go +t1.Fn1() +x := t1.Fn2() // 1 +t1.Fn3() +``` + + + +```go +t1.Fn1() + +x := t1.Fn2() +t1.Fn3() +``` + +
+ +1 Line above is not an assignment + + + +
+ +## `err` + + + + + + + +
BadGood
+ +```go +_, err := SomeFn() + +if err != nil { // 1 + return fmt.Errorf("failed to fn: %w", err) +} +``` + + + +```go +_, err := SomeFn() +if err != nil { + return fmt.Errorf("failed to fn: %w", err) +} +``` + +
+ +1 Whitespace between error assignment and error checking + + + +
+ +## `leading-whitespace` + + + + + + +
BadGood
+ +```go +if true { + + fmt.Println("hello") +} +``` + + + +```go +if true { + fmt.Println("hello") +} +``` + +
+ +## `trailing-whitespace` + + + + + + +
BadGood
+ +```go +if true { + fmt.Println("hello") + +} +``` + + + +```go +if true { + fmt.Println("hello") +} +``` + +
+ +## Configuration + +One shared logic across different checks is the logic around statements +containing a block, i.e. a statement with a following `{}` (e.g. `if`, `for`, +`switch` etc). + +`wsl` only allows one statement immediately above and that statement must also +be referenced in the expression in the statement with the block. E.g. + +```go +someVariable := true +if someVariable { + // Here `someVariable` used in the `if` expression is the only variable + // immediately above the statement. +} +``` + +This can be configured to be more "laxed" by also allowing a single statement +immediately above if it's used either first in the following block or anywhere +inside the following block. + +### `allow-first-in-block` + +By setting this to true (default), the variable doesn't have to be used in the +expression itself but is also allowed if it's the first statement in the block +body. + +```go +someVariable := 1 +if anotherVariable { + someVariable++ +} +``` + +### `allow-whole-block` + +This is similar to `allow-first-in-block` but now allows the lack of whitespace +if it's used anywhere in the following block. + +```go +someVariable := 1 +if anotherVariable { + someFn(yetAnotherVariable) + + if stillNotSomeVariable { + someVariable++ + } +} +``` diff --git a/LICENSE b/LICENSE index 4dade6d..f881b64 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 Simon Sawert +Copyright (c) 2018 - 2025 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 diff --git a/README.md b/README.md index 3534b70..6ceea44 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,181 @@ -# 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) +# wsl - whitespace linter [![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! +[![Coverage Status](https://coveralls.io/repos/github/bombsimon/wsl/badge.svg?branch=main)](https://coveralls.io/github/bombsimon/wsl?branch=main) + +`wsl` (**w**hite**s**pace **l**inter) is a linter that wants you to use empty +lines to separate grouping of different types to increase readability. There are +also a few places where it encourages you to _remove_ whitespaces which is at +the start and the end of blocks. + +## Checks and configuration + +Each check can be disabled or enabled individually to the point where no checks +can be run. The idea with this is to attract more users. Some checks have +configuration that affect how they work but most of them can only be turned on +or off. + +### Checks + +This is an exhaustive list of all the checks that can be enabled or disabled and +their default value. The names are the same as the Go +[AST](https://pkg.go.dev/go/ast) type name for built-ins. + +The base rule is that statements that has a block (e.g. `for`, `range`, +`switch`, `if` etc) should always only be directly adjacent with a single +variable and only if it's used in the expression in the block itself. + +For more details and examples, see [CHECKS](CHECKS.md). + +✅ = enabled by default, ❌ = disabled by default + +#### Built-ins and keywords + +- ✅ **assign** - Assignments should only be cuddled with other assignments, + or increment/decrement +- ✅ **branch** - Branch statement (`break`, `continue`, `fallthrough`, `goto`) + should only be cuddled if the block is less than `n` lines where `n` is the + value of [`branch-max-lines`](#configuration) +- ✅ **decl** - Declarations should never be cuddled +- ✅ **defer** - Defer should only be cuddled with other `defer`, after error + checking or with a single variable used on the line above +- ✅ **expr** - Expressions are e.g. function calls or index expressions, they + should only be cuddled with variables used on the line above +- ✅ **for** - For loops should only be cuddled with a single variable used on + the line above +- ✅ **go** - Go should only be cuddled with other `go` or a single variable + used on the line above +- ✅ **if** - If should only be cuddled with a single variable used on the line + above +- ✅ **inc-dec** - Increment/decrement (`++/--`) has the same rules as `assign` +- ✅ **label** - Labels should never be cuddled +- ✅ **range** - Range should only be cuddled with a single variable used on the + line above +- ✅ **return** - Return should only be cuddled if the block is less than `n` + lines where `n` is the value of [`branch-max-lines`](#configuration) +- ✅ **select** - Select should only be cuddled with a single variable used on + the line above +- ✅ **send** - Send should only be cuddled with a single variable used on the + line above +- ✅ **switch** - Switch should only be cuddled with a single variable used on + the line above +- ✅ **type-switch** - Type switch should only be cuddled with a single variable + used on the line above + +#### Specific `wsl` cases + +- ✅ **append** - Only allow re-assigning with `append` if the value being + appended exist on the line above +- ❌ **assign-exclusive** - Only allow cuddling either new variables or + re-assigning of existing ones +- ❌ **assign-expr** - Don't allow assignments to be cuddled with expressions, + e.g. function calls +- ❌ **err** - Error checking must follow immediately after the error variable + is assigned +- ✅ **leading-whitespace** - Disallow leading empty lines in blocks +- ✅ **trailing-whitespace** - Disallow trailing empty lines in blocks + +### Configuration + +Other than enabling or disabling specific checks some checks can be configured +in more details. + +- ✅ **allow-first-in-block** - Allow cuddling a variable if it's used first in + the immediate following block, even if the statement with the block doesn't + use the variable (see [Configuration](CHECKS.md#allow-first-in-block) for + details) +- ❌ **allow-whole-block** - Same as above, but allows cuddling if the variable + is used _anywhere_ in the following (or nested) block (see + [Configuration](CHECKS.md#allow-whole-block) for details) +- **branch-max-lines** - If a block contains more than this number of lines the + branch statement (e.g. `return`, `break`, `continue`) need to be separated by + a whitespace (default 2) +- **case-max-lines** - If set to a non negative number, `case` blocks needs to + end with a whitespace if exceeding this number (default 0, 0 = off, 1 = + always) +- ❌ **include-generated** - Include generated files when checking ## Installation ```sh -go install github.com/bombsimon/wsl/v4/cmd...@master +# Latest release +go install github.com/bombsimon/wsl/v5/cmd/wsl@latest + +# Main branch +go install github.com/bombsimon/wsl/v5/cmd/wsl@main ``` ## 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. +> `--fix` flag. -`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. +`wsl` uses the [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 --default none --enable branch,return --fix ./... ``` -`wsl` is also integrated in [`golangci-lint`](https://golangci-lint.run) +`wsl` is also integrated in [`golangci-lint`][golangci-lint] ```sh -golangci-lint run --no-config --disable-all --enable wsl +golangci-lint run --no-config --enable-only wsl --fix ``` -## 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) +This is an exhaustive, default, configuration for `wsl` in `golangci-lint`. + +```yaml +linters: + default: none + enable: + - wsl + + settings: + wsl: + allow-first-in-block: true + allow-whole-block: false + branch-max-lines: 2 + case-max-lines: 0 + default: ~ # Can be `all`, `none`, `default` or empty + enable: + - append + - assign + - branch + - decl + - defer + - expr + - for + - go + - if + - inc-dec + - label + - range + - return + - select + - send + - switch + - type-switch + - leading-whitespace + - trailing-whitespace + disable: + - assign-exclusive + - assign-expr + - err +``` + +## See also + +- [`nlreturn`][nlreturn] - Use empty lines before `return` +- [`whitespace`][whitespace] - Don't use a blank newline at the start or end of + a block. +- [`gofumpt`][gofumpt] - Stricter formatter than `gofmt`. + + [analysis]: https://pkg.go.dev/golang.org/x/tools/go/analysis + [gofumpt]: https://github.com/mvdan/gofumpt + [golangci-lint]: https://golangci-lint.run + [nlreturn]: https://github.com/ssgreg/nlreturn + [whitespace]: https://github.com/ultraware/whitespace diff --git a/analyzer.go b/analyzer.go index 90e9dd4..db2b852 100644 --- a/analyzer.go +++ b/analyzer.go @@ -2,7 +2,10 @@ package wsl import ( "flag" + "go/ast" + "go/token" "strings" + "sync" "golang.org/x/tools/go/analysis" ) @@ -19,72 +22,97 @@ func NewAnalyzer(config *Configuration) *analysis.Analyzer { } } -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 + + // When we use flags, we need to parse the ones used for checks into + // temporary variables so we can create the check set once the flag is being + // parsed by the analyzer and we run our analyzer. + defaultChecks string + enable []string + disable []string + + // To only validate and convert the parsed flags once we use a `sync.Once` + // to only create a check set once and store the set and potential error. We + // also store if we actually had a configuration to ensure we don't + // overwrite the checks if the analyzer was created with a proper wsl + // config. + cfgOnce sync.Once + didHaveConfig bool + checkSetErr error } func (wa *wslAnalyzer) flags() flag.FlagSet { - flags := flag.NewFlagSet("", flag.ExitOnError) + flags := flag.NewFlagSet("wsl", 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 { + wa.didHaveConfig = true return *flags } - wa.config = defaultConfig() + wa.config = NewConfig() - 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.BoolVar(&wa.config.IncludeGenerated, "include-generated", false, "Include generated files") + flags.BoolVar(&wa.config.AllowFirstInBlock, "allow-first-in-block", true, "Allow cuddling if variable is used in the first statement in the block") + flags.BoolVar(&wa.config.AllowWholeBlock, "allow-whole-block", false, "Allow cuddling if variable is used anywhere in the block") + flags.IntVar(&wa.config.BranchMaxLines, "branch-max-lines", 2, "Max lines before requiring newline before branching, e.g. `return`, `break`, `continue` (0 = never)") + flags.IntVar(&wa.config.CaseMaxLines, "case-max-lines", 0, "Max lines before requiring a newline at the end of case (0 = never)") - 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") + flags.StringVar(&wa.defaultChecks, "default", "", "Can be 'all' for all checks or 'none' for no checks or empty for default checks") + flags.Var(&multiStringValue{slicePtr: &wa.enable}, "enable", "Comma separated list of checks to enable") + flags.Var(&multiStringValue{slicePtr: &wa.disable}, "disable", "Comma separated list of checks to disable") return *flags } -func (wa *wslAnalyzer) run(pass *analysis.Pass) (interface{}, error) { +func (wa *wslAnalyzer) run(pass *analysis.Pass) (any, error) { + wa.cfgOnce.Do(func() { + // No need to update checks if config was passed when creating the + // analyzer. + if wa.didHaveConfig { + return + } + + // Parse the check params once if we set our config from flags. + wa.config.Checks, wa.checkSetErr = NewCheckSet(wa.defaultChecks, wa.enable, wa.disable) + }) + + if wa.checkSetErr != nil { + return nil, wa.checkSetErr + } + for _, file := range pass.Files { - filename := pass.Fset.Position(file.Pos()).Filename + filename := getFilename(pass.Fset, file) if !strings.HasSuffix(filename, ".go") { continue } - processor := newProcessorWithConfig(file, pass.Fset, wa.config) - processor.parseAST() + // if the file is related to cgo the filename of the unadjusted position + // is a not a '.go' file. + unadjustedFilename := pass.Fset.PositionFor(file.Pos(), false).Filename - for pos, fix := range processor.result { + // if the file is related to cgo the filename of the unadjusted position + // is a not a '.go' file. + if !strings.HasSuffix(unadjustedFilename, ".go") { + continue + } + + // The file is skipped if the "unadjusted" file is a Go file, and it's a + // generated file (ex: "_test.go" file). The other non-Go files are + // skipped by the first 'if' with the adjusted position. + if !wa.config.IncludeGenerated && ast.IsGenerated(file) { + continue + } + + wsl := New(file, pass, wa.config) + wsl.Run() + + for pos, fix := range wsl.issues { textEdits := []analysis.TextEdit{} + for _, f := range fix.fixRanges { textEdits = append(textEdits, analysis.TextEdit{ Pos: f.fixRangeStart, @@ -96,7 +124,7 @@ func (wa *wslAnalyzer) run(pass *analysis.Pass) (interface{}, error) { pass.Report(analysis.Diagnostic{ Pos: pos, Category: "whitespace", - Message: fix.reason, + Message: fix.message, SuggestedFixes: []analysis.SuggestedFix{ { TextEdits: textEdits, @@ -117,10 +145,11 @@ type multiStringValue struct { slicePtr *[]string } -// Set implements the flag.Value interface and will overwrite the pointer to the +// 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{} + var s []string for _, v := range strings.Split(value, ",") { s = append(s, strings.TrimSpace(v)) @@ -131,7 +160,7 @@ func (m *multiStringValue) Set(value string) error { return nil } -// Set implements the flag.Value interface. +// String implements the flag.Value interface. func (m *multiStringValue) String() string { if m.slicePtr == nil { return "" @@ -139,3 +168,12 @@ func (m *multiStringValue) String() string { return strings.Join(*m.slicePtr, ", ") } + +func getFilename(fset *token.FileSet, file *ast.File) string { + filename := fset.PositionFor(file.Pos(), true).Filename + if !strings.HasSuffix(filename, ".go") { + return fset.PositionFor(file.Pos(), false).Filename + } + + return filename +} diff --git a/cmd/golangci-lint-migrate/README.md b/cmd/golangci-lint-migrate/README.md new file mode 100644 index 0000000..987cd9e --- /dev/null +++ b/cmd/golangci-lint-migrate/README.md @@ -0,0 +1,59 @@ +# Configuration migrator + +This is a tool that helps you migrate from an existing `golangci-lint` +configuration to the new `v5` configuration. + +The biggest change is that `v5` uses checks for everything that can be turned on +and off individually. + +## Usage + +Just run the code and point it to your `.golangci.yml` file. + +```sh +golangci-lint-migrate .golangci.yml + +2025/05/04 19:28:11 `allow-trailing-comment` is deprecated and always allowed in >= v5 +2025/05/04 19:28:11 `allow-separated-leading-comment` is deprecated and always allowed in >= v5 + +These settings are the closest you can get in the new version of `wsl` +Potential deprecations are logged above + +See https://github.com/bombsimon/wsl for more details + +linters: + settings: + wsl: + allow-first-in-block: true + allow-whole-block: false + branch-max-lines: 2 + case-max-lines: 1 + enable: + - assign-exclusive + - err + disable: + - append + - assign + - decl +``` + +## Info + +See [repo] and [checks] for details but in short, this is the change: + +- **strict-append** - Converted to a check called `append` +- **allow-assign-and-call** - Converted to check called `assign-expr` +- **allow-assign-and-anything** - Converted to a check called `assign` +- **allow-multiline-assign** - Deprecated, always allowed +- **force-case-trailing-whitespace** - Renamed to `case-max-lines` +- **allow-trailing-comment** - Deprecated, always allowed +- **allow-separated-leading-comment** - Deprecated, always allowed +- **allow-cuddle-declarations** - Converted to a check called `decl` +- **allow-cuddle-with-calls** - Deprecated and not needed +- **allow-cuddle-with-rhs** - Deprecated and not needed +- **force-err-cuddling** - Converted to a check called `err` +- **error-variable-names** - Deprecated, we allow everything impl. `error` +- **force-short-decl-cuddling** - Converted to a check called `assign-exclusive` + +[repo]: https://github.com/bombsimon/wsl/blob/main/README.md +[checks]: https://github.com/bombsimon/wsl/blob/main/CHECKS.md diff --git a/cmd/golangci-lint-migrate/main.go b/cmd/golangci-lint-migrate/main.go new file mode 100644 index 0000000..81cf1f9 --- /dev/null +++ b/cmd/golangci-lint-migrate/main.go @@ -0,0 +1,191 @@ +package main + +import ( + "bytes" + "fmt" + "log" + "os" + "slices" + + "github.com/bombsimon/wsl/v5" + "gopkg.in/yaml.v3" +) + +type Config struct { + Version string `yaml:"version"` +} + +type V1 struct { + Settings Settings `yaml:"linters-settings"` +} + +type V2 struct { + Linters Linters `yaml:"linters"` +} + +type Linters struct { + Settings Settings `yaml:"settings"` +} + +type Settings struct { + WSL *WSL `yaml:"wsl"` +} + +type NewConfig struct { + Linters struct { + Settings struct { + WSL WSLV5 `yaml:"wsl"` + } `yaml:"settings"` + } `yaml:"linters"` +} + +// StrictAppend is replaced with CheckAppend +// AllowAssignAndCall is replaced with CheckAssignExpr (inverse) +// AllowAssignAndAnything is replaced with CheckAssign +// AllowMultilineAssign is deprecated and always allowed in v5 +// AllowTrailingComment is deprecated and always allowed in v5 +// AllowCuddleDeclarations is replaced with CheckDecl +// AllowSeparatedLeadingComment is deprecated and always allowed in v5 +// ForceErrCuddling is replaced with CheckErr +// ForceShortDeclCuddling is replaced with CheckAssignExclusive +// ForceCaseTrailingWhitespace is deprecated and replaced with CaseMaxLines +// AllowCuddlingWithCalls is deprecated and not needed in v5 +// AllowCuddleWithRHS is deprecated and not needed in v5 +// ErrorVariableNames is deprecated and not needed in v5 +type WSL struct { + StrictAppend bool `yaml:"strict-append"` + AllowAssignAndCall bool `yaml:"allow-assign-and-call"` + AllowAssignAndAnything bool `yaml:"allow-assign-and-anything"` + AllowMultilineAssign bool `yaml:"allow-multiline-assign"` + AllowTrailingComment bool `yaml:"allow-trailing-comment"` + AllowCuddleDeclarations bool `yaml:"allow-cuddle-declarations"` + AllowSeparatedLeadingComment bool `yaml:"allow-separated-leading-comment"` + ForceErrCuddling bool `yaml:"force-err-cuddling"` + ForceShortDeclCuddling bool `yaml:"force-short-decl-cuddling"` + ForceCaseTrailingWhitespace int `yaml:"force-case-trailing-whitespace"` + + AllowCuddlingWithCalls []string `yaml:"allow-cuddle-with-calls"` + AllowCuddleWithRHS []string `yaml:"allow-cuddle-with-rhs"` + ErrorVariableNames []string `yaml:"error-variable-names"` +} + +type WSLV5 struct { + AllowFirstInBlock bool `yaml:"allow-first-in-block"` + AllowWholeBlock bool `yaml:"allow-whole-block"` + BranchMaxLines int `yaml:"branch-max-lines"` + CaseMaxLines int `yaml:"case-max-lines"` + Enable []string `yaml:"enable"` + Disable []string `yaml:"disable"` +} + +func main() { + if len(os.Args) != 2 { + log.Fatalf("expected exactly one argument which should be your `.golangci.yml` file") + } + + v1cfg := getWslConfig(os.Args[1]) + if v1cfg == nil { + log.Println("failed to get wsl configuration from config") + log.Println("ensure you passed a configuration with existing `wsl` configuration") + log.Fatalln("if you didn't use any configuration you can keep leaving it empty to use the defaults") + } + + v5cfg := WSLV5{ + AllowFirstInBlock: true, + AllowWholeBlock: false, + BranchMaxLines: 2, + CaseMaxLines: v1cfg.ForceCaseTrailingWhitespace, + } + + if !v1cfg.StrictAppend { + v5cfg.Disable = append(v5cfg.Disable, wsl.CheckAppend.String()) + } + + if v1cfg.AllowAssignAndAnything { + v5cfg.Disable = append(v5cfg.Disable, wsl.CheckAssign.String()) + } + + if v1cfg.AllowMultilineAssign { + log.Println("`allow-multiline-assign` is deprecated and always allowed in >= v5") + } + + if v1cfg.AllowTrailingComment { + log.Println("`allow-trailing-comment` is deprecated and always allowed in >= v5") + } + + if v1cfg.AllowSeparatedLeadingComment { + log.Println("`allow-separated-leading-comment` is deprecated and always allowed in >= v5") + } + + if v1cfg.AllowCuddleDeclarations { + v5cfg.Disable = append(v5cfg.Disable, wsl.CheckDecl.String()) + } + + if v1cfg.ForceErrCuddling { + v5cfg.Enable = append(v5cfg.Enable, wsl.CheckErr.String()) + } + + if v1cfg.ForceShortDeclCuddling { + v5cfg.Enable = append(v5cfg.Enable, wsl.CheckAssignExclusive.String()) + } + + if !v1cfg.AllowAssignAndCall { + v5cfg.Enable = append(v5cfg.Enable, wsl.CheckAssignExpr.String()) + } + + slices.Sort(v5cfg.Enable) + slices.Sort(v5cfg.Disable) + + cfg := NewConfig{} + cfg.Linters.Settings.WSL = v5cfg + + buf := bytes.NewBuffer([]byte{}) + e := yaml.NewEncoder(buf) + e.SetIndent(2) + + if err := e.Encode(&cfg); err != nil { + log.Fatalf("%v", err) + } + + fmt.Println("") + fmt.Println("These settings are the closest you can get in the new version of `wsl`") + fmt.Println("Potential deprecations are logged above") + fmt.Println("") + fmt.Println("See https://github.com/bombsimon/wsl for more details") + fmt.Println("") + + fmt.Println(buf.String()) +} + +func getWslConfig(filename string) *WSL { + yamlFile, err := os.ReadFile(filename) + if err != nil { + log.Fatalf("%v", err) + } + + var cfg Config + if err := yaml.Unmarshal(yamlFile, &cfg); err != nil { + log.Fatalf("%v", err) + } + + switch cfg.Version { + case "": + var cfg V1 + if err := yaml.Unmarshal(yamlFile, &cfg); err != nil { + log.Fatalf("%v", err) + } + + return cfg.Settings.WSL + case "2": + var cfg V2 + if err := yaml.Unmarshal(yamlFile, &cfg); err != nil { + log.Fatalf("%v", err) + } + + return cfg.Linters.Settings.WSL + default: + log.Fatalf("invalid version '%s'", cfg.Version) + } + + return nil +} diff --git a/cmd/golangci-lint-migrate/v1.yaml b/cmd/golangci-lint-migrate/v1.yaml new file mode 100644 index 0000000..537380f --- /dev/null +++ b/cmd/golangci-lint-migrate/v1.yaml @@ -0,0 +1,78 @@ +--- +linters: + enable: + - wsl + +linters-settings: + wsl: + # 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. + # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#strict-append + # Default: true + strict-append: false + + # 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. + # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#allow-assign-and-call + # Default: true + allow-assign-and-call: false + + # Allows assignments to be cuddled with anything. + # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#allow-assign-and-anything + # Default: false + allow-assign-and-anything: true + + # Allows cuddling to assignments even if they span over multiple lines. + # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#allow-multiline-assign + # Default: true + allow-multiline-assign: false + + # If the number of lines in a case block is equal to or lager than this number, + # the case *must* end white a newline. + # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#force-case-trailing-whitespace + # Default: 0 + force-case-trailing-whitespace: 1 + + # Allow blocks to end with comments. + # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#allow-trailing-comment + # Default: false + allow-trailing-comment: true + + # Allow multiple comments in the beginning of a block separated with newline. + # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#allow-separated-leading-comment + # Default: false + allow-separated-leading-comment: true + + # Allow multiple var/declaration statements to be cuddled. + # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#allow-cuddle-declarations + # Default: false + allow-cuddle-declarations: true + + # A list of call idents that everything can be cuddled with. + # Defaults: [ "Lock", "RLock" ] + allow-cuddle-with-calls: ["Foo", "Bar"] + + # AllowCuddleWithRHS is a list of right hand side variables that is allowed + # to be cuddled with anything. + # Defaults: [ "Unlock", "RUnlock" ] + allow-cuddle-with-rhs: ["Foo", "Bar"] + + # Causes an error when an If statement that checks an error variable doesn't + # cuddle with the assignment of that variable. + # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#force-err-cuddling + # Default: false + force-err-cuddling: true + + # When force-err-cuddling is enabled this is a list of names + # used for error variables to check for in the conditional. + # Default: [ "err" ] + error-variable-names: ["foo"] + + # Causes an error if a short declaration (:=) cuddles with anything other than + # another short declaration. + # This logic overrides force-err-cuddling among others. + # https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#force-short-decl-cuddling + # Default: false + force-short-decl-cuddling: true diff --git a/cmd/golangci-lint-migrate/v2.yaml b/cmd/golangci-lint-migrate/v2.yaml new file mode 100644 index 0000000..f7dc97f --- /dev/null +++ b/cmd/golangci-lint-migrate/v2.yaml @@ -0,0 +1,85 @@ +--- +version: "2" + +linters: + enable: + - wsl + settings: + wsl: + # 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. + # https://github.com/bombsimon/wsl/blob/HEAD/doc/configuration.md#strict-append + # Default: true + strict-append: false + + # 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. + # https://github.com/bombsimon/wsl/blob/HEAD/doc/configuration.md#allow-assign-and-call + # Default: true + allow-assign-and-call: false + + # Allows assignments to be cuddled with anything. + # https://github.com/bombsimon/wsl/blob/HEAD/doc/configuration.md#allow-assign-and-anything + # Default: false + allow-assign-and-anything: true + + # Allows cuddling to assignments even if they span over multiple lines. + # https://github.com/bombsimon/wsl/blob/HEAD/doc/configuration.md#allow-multiline-assign + # Default: true + allow-multiline-assign: false + + # If the number of lines in a case block is equal to or lager than this number, + # the case *must* end white a newline. + # https://github.com/bombsimon/wsl/blob/HEAD/doc/configuration.md#force-case-trailing-whitespace + # Default: 0 + force-case-trailing-whitespace: 1 + + # Allow blocks to end with comments. + # https://github.com/bombsimon/wsl/blob/HEAD/doc/configuration.md#allow-trailing-comment + # Default: false + allow-trailing-comment: true + + # Allow multiple comments in the beginning of a block separated with newline. + # https://github.com/bombsimon/wsl/blob/HEAD/doc/configuration.md#allow-separated-leading-comment + # Default: false + allow-separated-leading-comment: true + + # Allow multiple var/declaration statements to be cuddled. + # https://github.com/bombsimon/wsl/blob/HEAD/doc/configuration.md#allow-cuddle-declarations + # Default: false + allow-cuddle-declarations: true + + # A list of call idents that everything can be cuddled with. + # Defaults: [ "Lock", "RLock" ] + allow-cuddle-with-calls: ["Foo", "Bar"] + + # AllowCuddleWithRHS is a list of right hand side variables that is allowed + # to be cuddled with anything. + # Defaults: [ "Unlock", "RUnlock" ] + allow-cuddle-with-rhs: ["Foo", "Bar"] + + # Allow cuddling with any block as long as the variable is used somewhere in + # the block. + # https://github.com/bombsimon/wsl/blob/HEAD/doc/configuration.md#allow-cuddle-used-in-block + # Default: false + allow-cuddle-used-in-block: true + + # Causes an error when an If statement that checks an error variable doesn't + # cuddle with the assignment of that variable. + # https://github.com/bombsimon/wsl/blob/HEAD/doc/configuration.md#force-err-cuddling + # Default: false + force-err-cuddling: true + + # When force-err-cuddling is enabled this is a list of names + # used for error variables to check for in the conditional. + # Default: [ "err" ] + error-variable-names: ["foo"] + + # Causes an error if a short declaration (:=) cuddles with anything other than + # another short declaration. + # This logic overrides force-err-cuddling among others. + # https://github.com/bombsimon/wsl/blob/HEAD/doc/configuration.md#force-short-decl-cuddling + # Default: false + force-short-decl-cuddling: true diff --git a/cmd/wsl/main.go b/cmd/wsl/main.go index 92d1e8a..0139ea0 100644 --- a/cmd/wsl/main.go +++ b/cmd/wsl/main.go @@ -1,7 +1,7 @@ package main import ( - "github.com/bombsimon/wsl/v4" + "github.com/bombsimon/wsl/v5" "golang.org/x/tools/go/analysis/singlechecker" ) diff --git a/config.go b/config.go new file mode 100644 index 0000000..df5e650 --- /dev/null +++ b/config.go @@ -0,0 +1,279 @@ +package wsl + +import ( + "fmt" + "strings" +) + +// CheckSet is a set of checks to run. +type CheckSet map[CheckType]struct{} + +// CheckType is a type that represents a checker to run. +type CheckType int + +// Each checker is represented by a CheckType that is used to enable or disable +// the check. +// A check can either be of a specific built-in keyword or custom checks. +const ( + CheckInvalid CheckType = iota + CheckAssign + CheckBranch + CheckDecl + CheckDefer + CheckExpr + CheckFor + CheckGo + CheckIf + CheckIncDec + CheckLabel + CheckRange + CheckReturn + CheckSelect + CheckSend + CheckSwitch + CheckTypeSwitch + + // Check append only allows assignments of `append` to be cuddled with other + // assignments if it's a variable used in the append statement, e.g. + // + // a := 1 + // x = append(x, a) + // . + CheckAppend + // Assign exclusive only allows assignments of either new variables or + // re-assignment of existing ones, e.g. + // + // a := 1 + // b := 2 + // + // a = 1 + // b = 2 + // . + CheckAssignExclusive + // CheckAssignExpr will check so assignments are not cuddled with expression + // nodes, e.g. + // + // t1.Fn1() + // + // x := t1.Fn2() + // t1.Fn3() + // . + CheckAssignExpr + // Force error checking to follow immediately after an error variable is + // assigned, e.g. + // + // _, err := someFn() + // if err != nil { + // panic(err) + // } + // . + CheckErr + CheckLeadingWhitespace + CheckTrailingWhitespace + + // CheckTypes only used for reporting. + CheckCaseTrailingNewline +) + +func (c CheckType) String() string { + return [...]string{ + "invalid", + "assign", + "branch", + "decl", + "defer", + "expr", + "for", + "go", + "if", + "inc-dec", + "label", + "range", + "return", + "select", + "send", + "switch", + "type-switch", + // + "append", + "assign-exclusive", + "assign-expr", + "err", + "leading-whitespace", + "trailing-whitespace", + // + "case-trailing-newline", + }[c] +} + +type Configuration struct { + IncludeGenerated bool + AllowFirstInBlock bool + AllowWholeBlock bool + BranchMaxLines int + CaseMaxLines int + Checks CheckSet +} + +func NewConfig() *Configuration { + return &Configuration{ + IncludeGenerated: false, + AllowFirstInBlock: true, + AllowWholeBlock: false, + CaseMaxLines: 0, + BranchMaxLines: 2, + Checks: DefaultChecks(), + } +} + +func NewWithChecks( + defaultChecks string, + enable []string, + disable []string, +) (*Configuration, error) { + checks, err := NewCheckSet(defaultChecks, enable, disable) + if err != nil { + return nil, fmt.Errorf("failed to create config: %w", err) + } + + cfg := NewConfig() + cfg.Checks = checks + + return cfg, nil +} + +func NewCheckSet( + defaultChecks string, + enable []string, + disable []string, +) (CheckSet, error) { + var cs CheckSet + + switch strings.ToLower(defaultChecks) { + case "", "default": + cs = DefaultChecks() + case "all": + cs = AllChecks() + case "none": + cs = NoChecks() + default: + return nil, fmt.Errorf("invalid preset '%s', must be `all`, `none` or `` (empty)", defaultChecks) + } + + for _, s := range enable { + check, err := CheckFromString(s) + if err != nil { + return nil, fmt.Errorf("invalid check '%s'", s) + } + + cs.Add(check) + } + + for _, s := range disable { + check, err := CheckFromString(s) + if err != nil { + return nil, fmt.Errorf("invalid check '%s'", s) + } + + cs.Remove(check) + } + + return cs, nil +} + +func DefaultChecks() CheckSet { + return CheckSet{ + CheckAppend: {}, + CheckAssign: {}, + CheckBranch: {}, + CheckDecl: {}, + CheckDefer: {}, + CheckExpr: {}, + CheckFor: {}, + CheckGo: {}, + CheckIf: {}, + CheckIncDec: {}, + CheckLabel: {}, + CheckLeadingWhitespace: {}, + CheckTrailingWhitespace: {}, + CheckRange: {}, + CheckReturn: {}, + CheckSelect: {}, + CheckSend: {}, + CheckSwitch: {}, + CheckTypeSwitch: {}, + } +} + +func AllChecks() CheckSet { + c := DefaultChecks() + c.Add(CheckAssignExclusive) + c.Add(CheckAssignExpr) + c.Add(CheckErr) + + return c +} + +func NoChecks() CheckSet { + return CheckSet{} +} + +func (c CheckSet) Add(check CheckType) { + c[check] = struct{}{} +} + +func (c CheckSet) Remove(check CheckType) { + delete(c, check) +} + +func CheckFromString(s string) (CheckType, error) { + switch strings.ToLower(s) { + case "assign": + return CheckAssign, nil + case "branch": + return CheckBranch, nil + case "decl": + return CheckDecl, nil + case "defer": + return CheckDefer, nil + case "expr": + return CheckExpr, nil + case "for": + return CheckFor, nil + case "go": + return CheckGo, nil + case "if": + return CheckIf, nil + case "inc-dec": + return CheckIncDec, nil + case "label": + return CheckLabel, nil + case "range": + return CheckRange, nil + case "return": + return CheckReturn, nil + case "select": + return CheckSelect, nil + case "send": + return CheckSend, nil + case "switch": + return CheckSwitch, nil + case "type-switch": + return CheckTypeSwitch, nil + + case "append": + return CheckAppend, nil + case "assign-exclusive": + return CheckAssignExclusive, nil + case "assign-expr": + return CheckAssignExpr, nil + case "err": + return CheckErr, nil + case "leading-whitespace": + return CheckLeadingWhitespace, nil + case "trailing-whitespace": + return CheckTrailingWhitespace, nil + default: + return CheckInvalid, fmt.Errorf("invalid check '%s'", s) + } +} diff --git a/cursor.go b/cursor.go new file mode 100644 index 0000000..a4ad021 --- /dev/null +++ b/cursor.go @@ -0,0 +1,77 @@ +package wsl + +import ( + "go/ast" +) + +// Cursor holds a list of statements and a pointer to where in the list we are. +// Each block gets a new cursor and can be used to check previous or coming +// statements. +type Cursor struct { + currentIdx int + statements []ast.Stmt + checkType CheckType +} + +// NewCursor creates a new cursor with a given list of statements. +func NewCursor(statements []ast.Stmt) *Cursor { + return &Cursor{ + currentIdx: -1, + statements: statements, + } +} + +func (c *Cursor) SetChecker(ct CheckType) { + c.checkType = ct +} + +func (c *Cursor) Next() bool { + if c.currentIdx >= len(c.statements)-1 { + return false + } + + c.currentIdx++ + + return true +} + +func (c *Cursor) Previous() bool { + if c.currentIdx <= 0 { + return false + } + + c.currentIdx-- + + return true +} + +func (c *Cursor) PreviousNode() ast.Node { + defer c.Save()() + + var previousNode ast.Node + if c.Previous() { + previousNode = c.Stmt() + } + + return previousNode +} + +func (c *Cursor) Stmt() ast.Stmt { + return c.statements[c.currentIdx] +} + +func (c *Cursor) Save() func() { + idx := c.currentIdx + + return func() { + c.currentIdx = idx + } +} + +func (c *Cursor) Len() int { + return len(c.statements) +} + +func (c *Cursor) Nth(n int) ast.Stmt { + return c.statements[n] +} diff --git a/cursor_test.go b/cursor_test.go new file mode 100644 index 0000000..9f5539d --- /dev/null +++ b/cursor_test.go @@ -0,0 +1,49 @@ +package wsl + +import ( + "go/ast" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSavestate(t *testing.T) { + cursor := NewCursor([]ast.Stmt{ + &ast.AssignStmt{}, + &ast.AssignStmt{}, + &ast.AssignStmt{}, + &ast.AssignStmt{}, + &ast.AssignStmt{}, + &ast.AssignStmt{}, + &ast.AssignStmt{}, + }) + + reset := cursor.Save() + cursor.Next() + cursor.Next() + + func() { + reset := cursor.Save() + defer reset() + + cursor.Next() + cursor.Next() + + func() { + reset := cursor.Save() + defer reset() + + cursor.Next() + cursor.Next() + + assert.Equal(t, 5, cursor.currentIdx) + }() + + assert.Equal(t, 3, cursor.currentIdx) + }() + + assert.Equal(t, 1, cursor.currentIdx) + + reset() + assert.Equal(t, -1, cursor.currentIdx) +} diff --git a/doc/configuration.md b/doc/configuration.md deleted file mode 100644 index 099a973..0000000 --- a/doc/configuration.md +++ /dev/null @@ -1,329 +0,0 @@ -# Configuration - -The configuration should always have the same name for the flag in `wsl` as the -key name in the `golangci-lint` documentation. For example if you run local -installation you should be able to use `--allow-cuddle-declarations` and if -you're using `golangci-lint` the `golangci.yaml` configuration should look like -this: - -```yaml -linters-settings: - wsl: - allow-cuddle-declarations: true -``` - -## Available settings - -The following configuration is available. Click each header to jump to the rule -description. - -### [strict-append](rules.md#append-only-allowed-to-cuddle-with-appended-value) - -Controls if the checks for slice append should be "strict" in the sense that it -will only allow these assignments to be cuddled with variables being appended. - -> Default value: true - -Required when true: - -```go -x := []string{} -y := "not going in x" - -x = append(x, "not y") -x = append(x, y) -``` - -Supported when false: - -```go -x := []string{} -y := "not going in x" -x = append(x, "not y") -z := "string" -x = append(x, "i don't care") -``` - -### [allow-assign-and-call](rules.md#only-cuddled-expressions-if-assigning-variable-or-using-from-line-above) - -Controls if you may cuddle assignments and calls without needing an empty line -between them. - -> Default value: true - -Supported when true: - -```go -x := GetX() -x.Call() -x = AssignAgain() -x.CallAgain() -``` - -Required when false: - -```go -x := GetX() -x.Call() - -x = AssignAgain() -x.CallAgain() -``` - -### [allow-assign-and-anything](rules.md#assignments-should-only-be-cuddled-with-other-assignments) - -Controls if you may cuddle assignments and anything without needing an empty line -between them. - -> Default value: false - -Supported when true: - -```go -if x == 1 { - x = 0 -} -z := x + 2 - -fmt.Println("x") -y := "x" -``` - -Required when false: - -```go -if x == 1 { - x = 0 -} - -z := x + 2 - -fmt.Println("x") - -y := "x" -``` - -### [allow-multiline-assign](rules.md#only-cuddled-expressions-if-assigning-variable-or-using-from-line-above) - -Controls if you may cuddle assignments even if they span over multiple lines. - -> Default value: true - -Supported when true: - -```go -assignment := fmt.Sprintf( - "%s%s", - "I span over", "multiple lines" -) -assignmentTwo := "and yet I may be cuddled" -assignmentThree := "this is fine" -``` - -Required when false: - -```go -assignment := fmt.Sprintf( - "%s%s", - "I span over", "multiple lines" -) - -assignmentTwo := "so I cannot be cuddled" -assignmentThree := "this is fine" -``` - -### [allow-separated-leading-comment](rules.md#block-should-not-start-with-a-whitespace) - -This option allows whitespace after each comment group that begins a block. - -> Default value: false - -For example, - -```go -func example() string { - // comment - - return fmt.Sprintf("x") -} -``` - -and - -```go -func example() string { - // comment - - // comment - return fmt.Sprintf("x") -} -``` - -become legal, as the whitespace _after_ (or between) each comment block -doesn't count against whitespace before the first actual statement. - -### [force-case-trailing-whitespace](rules.md#case-block-should-end-with-newline-at-this-size) - -Can be set to force trailing newlines at the end of case blocks to improve -readability. If the number of lines (including comments) in a case block exceeds -this number a linter error will be yielded if the case does not end with a -newline. - -> Default value: 0 (never force) - -Supported when set to 0: - -```go -switch n { -case 1: - fmt.Println("newline") - -case 2: - fmt.Println("no newline") -case 3: - fmt.Println("Many") - fmt.Println("lines") - fmt.Println("without") - fmt.Println("newlinew") -case 4: - fmt.Println("done") -} -``` - -Required when set to 2: - -```go -switch n { -case 1: - fmt.Println("must have") - fmt.Println("empty whitespace") - -case 2: - fmt.Println("might have empty whitespace") - -case 3: - fmt.Println("might skip empty whitespace") -case 4: - fmt.Println("done"9) -} -``` - -### [allow-cuddle-declarations](rules.md#declarations-should-never-be-cuddled) - -Controls if you're allowed to cuddle multiple declarations. This is false by -default to encourage you to group them in one `var` block. One major benefit -with this is that if the variables are assigned the assignments will be -tabulated. - -> Default value: false - -Supported when true: - -```go -var foo = 1 -var fooBar = 2 -``` - -Required when false: - -```go -var ( - foo = 1 - fooBar = 2 -) -``` - -### [allow-trailing-comment](rules.md#block-should-not-end-with-a-whitespace-or-comment) - -Controls if blocks can end with comments. This is not encouraged sine it's -usually code smell but might be useful do improve understanding or learning -purposes. To be allowed there must be no whitespace between the comment and the -last statement or the comment and the closing brace. - -> Default value: false - -Supported when true - -```go -if true { - fmt.Println("x") - // See ya later! -} - -if false { - fmt.Println("x") - // - // Multiline OK -} - -if 1 == 1 { - fmt.Println("x") - /* - This is also fine! - So just comment awaaay! - */ -} -``` - -### [force-err-cuddling](rules.md#if-statements-that-check-an-error-must-be-cuddled-with-the-statement-that-assigned-the-error) - -Enforces that an `if` statement checking an error variable is cuddled with the line that assigned that error variable. - -> Default value: false - -Supported when false: - -```go -err := ErrorProducingFunc() - -if err != nil { - return err -} -``` - -Required when true: - -```go -err := ErrorProducingFunc() -if err != nil { - return err -} -``` - -### [force-short-decl-cuddling](rules.md#short-declaration-should-cuddle-only-with-other-short-declarations) - -Enforces that an assignment which is actually a short declaration (using `:=`) -is only allowed to cuddle with other short declarations, and not plain -assignments, blocks, etc. This rule helps make declarations stand out by -themselves, much the same as grouping `var` statements. - -> Default value: false - -Supported when false: - -```go -a := 1 -a = 2 - -err := ErrorProducingFunc() -if err != nil { - return err -} -``` - -Required when true: - -```go -a := 1 - -a = 2 - -err := ErrorProducingFunc() - -if err != nil { - return err -} -``` - -**Note**: this means the option _overrides_ the -[force-err-cuddling](#force-err-cuddling) option above, among others. diff --git a/doc/rules.md b/doc/rules.md deleted file mode 100644 index 059bb7a..0000000 --- a/doc/rules.md +++ /dev/null @@ -1,1075 +0,0 @@ -# Whitespace Linter (`wsl`) documentation - -This page describes checks supported by [wsl](https://github.com/bombsimon/wsl) -linter and how they should be resolved or configured to handle. - -## Checklist - -### Anonymous switch statements should never be cuddled - -Anonymous `switch` statements (mindless `switch`) should deserve its needed -attention that it does not need any assigned variables. Hence, it should not -cuddle with anything before it. One bad example is: - -```go -timeout := 10 -switch { -case s.Switches["bad timeout"]: - timeout = 100 -case s.Switches["zero timeout"]: - timeout = 0 -} -``` - -#### Recommended amendment - -Add an empty line before the `switch` statement: - -```go -timeout := 10 - -switch { -case s.Switches["bad timeout"]: - timeout = 100 -case s.Switches["zero timeout"]: - timeout = 0 -} -``` - ---- - -### Append only allowed to cuddle with appended value - -> Can be configured, see [configuration -documentation](configuration.md#strict-append) - -`append` is only allowed to cuddle with the appended value. Otherwise, they -deserve some distance. If the variables you're assigning isn't going to be -appended (or used in the append, e.g. a function call), try to separate it. - -```go -x := []string{} -x = append(x, "literal") -notUsed := "just assigning, don't mind me" -x = append(x, "not z..") -useMe := "right away" -alsoNotUsed := ":(" -x = append(x, "just noise" -x = append(x, useMe) -``` - -#### Recommended amendment - -Things not going to be appended should not be cuddled in the append path (or a -single append if only one). Variables being appended can be placed immediately -before an append statement. - -```go -notUsed := "just assigning, don't mind me" -alsoNotUsed := ":(" - -x := []string{} -x = append(x, "literal") -x = append(x, "not z..") -x = append(x, "just noise" -useMe := "to be used" -x = append(x, useMe)) -``` - ---- - -### Assignments should only be cuddled with other assignments - -> Can be configured, see [configuration -documentation](configuration.md#allow-assign-and-anything) - -Assignments should either be grouped together or have some space between whoever -else before it. One bad example is `z` and `y` in such case: - -```go -if x == 1 { - x = 0 -} -z := x + 2 - -fmt.Println("x") -y := "x" -``` - -#### Recommended amendment - -Group all assignments together when possible (`t` and `x`). Otherwise, leave -an empty line before the assignment (e.g. `z`). - -```go -if x == 1 { - x = 0 -} - -z := x + 2 -y := "x" - -fmt.Println("x") -``` - ---- - -### Block should not end with a whitespace (or comment) - -> Can be configured, see [configuration -documentation](configuration.md#allow-trailing-comment) - -Having an empty trailing whitespace is unnecessary and makes the block -definition looks never-ending long. You want to let reader know that the -code definitions end right after the last statement. Also, any trailing -comments should be on the top. One bad example: - -```go -func example() string { - return fmt.Sprintf("x") - // TODO: add mux function later. - -} -``` - -#### Recommended amendment - -Remove the unnecessary trailing whitespace line (after `return` statement). -Move the comment to the top. - -```go -func example(y int) string { - // TODO: add mux function later. - return fmt.Sprintf("x") -} -``` - ---- - -### Case block should end with newline at this size - -> Can be configured, see [configuration -documentation](configuration.md#force-case-trailing-whitespace) - -To improve readability WSL can force to add whitespaces based on a set limit. -See link to configuration for options. - -With `force-case-trailing-whitespace` set to 1 this yields an error. - -```go -switch n { -case 1: - fmt.Println("one") -case 2: - fmt.Println("two") -} -``` - -#### Recommended amendment - -```go -switch n { -case 1: - fmt.Println("one") - -case 2: - fmt.Println("two") -} -``` - ---- - -### Block should not start with a whitespace - -Having an empty leading whitespace is unnecessary and makes the block definition -looks disconnected and long. You want to let reader to know that the code -definitions start right after the block declaration. One bad example is: - -```go -func example() string { - - return fmt.Sprintf("x") -} -``` - -#### Recommended amendment - -Remove the unnecessary leading whitespace line (before `x` definition). - -```go -func example() string { - return fmt.Sprintf("x") -} -``` - -> However, this can be configured to allow white space after one -> or more initial comment groups, see -[configuration documentation](configuration.md#allow-separated-leading-comment) -> -> If that is done, then these examples are allowed: - -```go - func example() string { - // comment - - return fmt.Sprintf("x") -} -``` - -> and - -```go - func example() string { - // comment - - // comment - return fmt.Sprintf("x") -} -``` - ---- - -### Branch statements should not be cuddled if block has more than two lines - -Branch statements (`break`, `continue`, and `return`) should stand out clearly -when the block is having more than or equal to 2 lines. Hence, it deserves -some spacing. One bad example is: - -```go -for i := range make([]int, 5) { - if i > 2 { - sendToOne(i) - sendToSecond(i) - continue - } - - if statement == "is short" { - sendToOne(i) - break - } -} -``` - -#### Recommended amendment - -Add an empty line before the branch statements (`continue`) contained within -a more than or equal to 2 lines code block: - -```go -for i := range make([]int, 5) { - if i > 2 { - sendToOne(i) - sendToSecond(i) - - continue - } - - if statement == "is short" { - sendToOne(i) - break - } -} -``` - ---- - -### Declarations should never be cuddled - -> Can be configured, see -[configuration documentation](configuration.md#allow-cuddle-declarations) - -`var` declarations, in opinion, should never be cuddled. Instead, multiple -`var` patterns is encouraged to use the grouped `var` format. One case study is: - -```go -var eol string -var i int -``` - -#### Recommended amendment - -Since this hit is opinionated, there are 3 ways to deal with it: - -1) Use the grouped `var` pattern: - -```go -var ( - eol = "" - i = 0 -) -``` - -2) Allow it by configuration (`wsl` or `golangci-lint`) - -3) Use an empty line between them - -```go -var eol = "" - -var i = 0 -``` - ---- - -### Defer statements should only be cuddled with expressions on same variable - -`defer` statement should only cuddle with related expressions. Otherwise, it -deserves some distance from whatever it is. One bad example is: - -```go -x := 4 -defer notX() -``` - -#### Recommended amendment - -Add an empty line before `defer`: - -```go -x := 4 - -defer notX() - -thisIsX := func() {} -defer thisIsX() -``` - ---- - -### Expressions should not be cuddled with blocks - -Code expressions should not be cuddled with a block (e.g. `if` or `switch`). -There must be some clarity between the block and the new expression itself. -One bad example is: - -```go -t, err := someFunc() -if err != nil { - // handle error - return -} -fmt.Println(t) -``` - -#### Recommended amendment - -An empty line between the expression and block. - -```go -t, err := someFunc() -if err != nil { - // handle error - return -} - -fmt.Println(t) -``` - ---- - -### Expressions should not be cuddled with declarations or returns - -Any expressions should not cuddle with any declarations (`var`) or `return`. -They deserve some space for clarity. One bad example is (`run()`): - -```go -var i int -run() - -return i -run() -``` - -#### Recommended amendment - -Give an empty after the declaration (`var`) and an empty line before the -`return`: - -```go -var i int - -run() - -return i - -run() -``` - ---- - -### For statement without condition should never be cuddled - -`for` loop without conditions (infinity loop) should deserves its own -attention. Hence, it should not be cuddled with anyone. - -```go -x := "hey" -for { - fmt.Printf("x") -} -``` - -#### Recommended amendment - -Add an empty line before the `for` loop. - -```go -x := "hey" - -for { - fmt.Printf("x") -} -``` - ---- - -### For statements should only be cuddled with assignments used in the iteration - -`for` statement should only cuddle with related assignments. Otherwise, its -relationship status can be very complicated, making reader wondering what the -co-relation between the `for` block and whatever it is. One bad example is: - -```go -x := y + 2 -for i := 0; i < y+10; i++ { - fmt.Println(i) -} - -list := getList() -for i, v := range anotherList { - fmt.Println(i) -} -``` - -#### Recommended amendment - -Add an empty line before the `for` statement: - -```go -x := y + 2 - -for i := 0; i < y+10; i++ { - fmt.Printf("this is i:%v\n", i) -} - -step := 2 -for i := 0; i < y+10; i+=step { - fmt.Printf("this is i:%v\n", i) -} - -list := getList() -for i, v := range list { - fmt.Printf("%d: %s\n", i, v) -} -``` - ---- - -### Go statements can only invoke functions assigned on line above - -`go` statement deserves clarity from any nearby non-related executions. Hence, -it deserves an empty line separation before it. - -```go -thisIsFunc := func() {} -go thisIsNOTFunc() -``` - -#### Recommended amendment - -Add an empty before `go` statement. - -```go -thisIsFunc := func() {} -go thisIsFunc() - -thisIsNOTFunc := func() {} - -go callingAnotherFunc() -``` - ---- - -### If statements should only be cuddled with assignments - -`if` statement should only cuddle with one related assignment. Otherwise, it -should have a distance between `if` and whoever else is. - -```go -one := 1 -if 1 == 1 { - fmt.Println("duuh") -} -if 1 != one { - fmt.Println("i don't believe you!") -} -``` - -#### Recommended amendment - -Group that single related assignment together with the `if` block and give one -empty line before them. - -If environment is not allowed like mutex lock blocking -(e.g. `Intercept(...)`), add an empty line before the `if` block. - -```go -if 1 == 1 { - fmt.Println("duuh") -} - -one := 1 -if 1 != one { - fmt.Println("i don't believe you!") -} -``` - ---- - -### If statements should only be cuddled with assignments used in the if statement itself - -`if` statements should only cuddle with the associated assignment. Otherwise, -it deserves some space between itself and whoever before it. One bad example is -the `if` block that uses `x` cuddled with `z` assignment: - -```go -x := true -if true { - fmt.Println("didn't use cuddled variable") -} -``` - -#### Recommended amendment - -Shift the `if` block close to the assignment when possible (`if` with `x`). -Otherwise, leave an empty line before it (`if` uses `y`): - -```go -y := true - -if true { - fmt.Println("didn't use cuddled variable") -} - -x := false -if !x { - fmt.Println("proper usage of variable") -} -``` - ---- - -### Only cuddled expressions if assigning variable or using from line above - -> Can be configured, see -[configuration documentation](configuration.md#allow-assign-and-call) - -When an assignment is cuddling with an unrelated expression, they create -confusing relationship to one another. Therefore, they should keep their -distance. One bad example (all `fmt` printouts): - -```go -notInExpression := GetIt() -call.SomethingElse() -``` - -#### Recommended amendment - -Provide an empty line before the expression: - -```go -notInExpression := GetIt() - -call.SomethingElse() - -thisIsBetter := GetIt() -thisIsBetter.CallIt() -``` - ---- - -### Only one cuddle assignment allowed before defer statement - -`defer` statement should only be cuddled with 1 related assignment. If you have -more than 1 assignment(s), they should have a space between them for clarity -purposes. One bad example is: - -```go -assignmentOne := Something() -assignmentTwo := AnotherThing() -defer assignmentTwo() -``` - -> **EXCEPTION**: It is allowed to use the following: -> -> 1) The `defer` after `error` check as reported in [Issue #31](https://github.com/bombsimon/wsl/issues/31) -> -> ```go -> f1, err := os.Open("/path/to/f1.txt") -> if err != nil { -> // handle error -> return -1 -> } -> defer f1.Close() -> ``` -> -> OR -> -> 2) The conventional mutex `Lock` and `Unlock`. -> -> ```go -> m.Lock() -> defer m.Unlock() -> ``` - -#### Recommended amendment - -Add an empty line before `defer`: - -```go -assignmentOne := Something() - -assignmentTwo := AnotherThing() -defer assignmentTwo() -``` - ---- - -### Only one cuddle assignment allowed before for statement - -`for` block should only be cuddled with 1 related assignment. If you have more -than 1 assignment(s), they should have a space between them for clarity -purposes. One bad example is: - -```go -i := 0 -a := 0 -for i = 0; i < 5; i++ { - fmt.Println("x") -} -``` - -#### Recommended amendment - -An empty line between the last assignment and the `for` block. - -```go -i := 0 -a := 0 - -for i = 0; i < 5; i++ { - fmt.Println("x") -} - -j := 0 -for i = 0; j < 5; i++ { - fmt.Println("x") -} -``` - ---- - -### Only one cuddle assignment allowed before go statement - -`go` block should only be cuddled with 1 related assignment. If you have more -than 1 assignment(s), they should have a space between them for clarity -purposes. One bad example is: - -```go -assignOne := SomeOne() -assignTwo := SomeTwo() -go assignTwo() -``` - -#### Recommended amendment - -An empty line between the last assignment and the `go` block. - -```go -assignOne := SomeOne() - -assignTwo := SomeTwo() -go assignTwo() -``` - ---- - -### Only one cuddle assignment allowed before if statement - -If block should only be cuddled with 1 related assignment. If you have more -than 1 assignment(s), they should have more space between them for clarity -purposes. One bad example is: - -```go -lengthA := len(sliceA) -lengthB := len(sliceB) -if lengthA != lengthB { - fmt.Println("not equal") -} -``` - -#### Recommended amendment - -An empty line between the last assignment and the `if` block. - -```go -lengthA := len(sliceA) -lengthB := len(sliceB) - -if lengthA != lengthB { - fmt.Println("not equal") -} - -lengthC := len(sliceC) -if lengthC > 1 { - fmt.Println("only one assignment is ok") -} -``` - ---- - -### Only one cuddle assignment allowed before range statement - -`range` block should only be cuddled with 1 related assignment. If you have more -than 1 assignment(s), they should have more space between them for clarity -purposes. One bad example is: - -```go -listA := GetA() -listB := GetB() -for _, v := range listB { - fmt.Println(v) -} -``` - -#### Recommended amendment - -Give an empty line before `range` statement: - -```go -listA := GetA() -listB := GetB() - -for _, v := range listB { - fmt.Println(v) -} - -listC := GetC() -for _, v := range listC { - fmt.Println(v) -} -``` - ---- - -### Only one cuddle assignment allowed before switch statement - -`switch` block should only be cuddled with 1 related assignment. If you have -more than 1 assignment(s), they should have more space between them for clarity -purposes. One bad example is: - -```go -assignOne := SomeOone() -assignTwo := SomeTwo() -switch assignTwo { -case 1: - fmt.Println("one") -default: - fmt.Println("NOT one") -} -``` - -#### Recommended amendment - -An empty line between the last assignment and the `switch` block: - -```go -assignOne := SomeOone() -assignTwo := SomeTwo() - -switch assignTwo { -case 1: - fmt.Println("one") -default: - fmt.Println("NOT one") -} - - -assignThree := SomeThree() -switch assignTwo { -// cases -} -``` - ---- - -### Only one cuddle assignment allowed before type switch statement - -`type` `switch` block should only be cuddled with 1 related assignment. If you -have more than 1 assignment(s), they should have more space between them for -clarity purposes. - -```go -someT := GetT() -anotherT := GetT() -switch v := anotherT.(type) { -case int: - fmt.Println("was int") -default: - fmt.Println("was not int") -} -``` - -#### Recommended amendment - -An empty line between the last assignment and the `switch` block. - -```go -someT := GetT() -anotherT := GetT() - -switch v := anotherT.(type) { -case int: - fmt.Println("was int") -default: - fmt.Println("was not int") -} - -thirdT := GetT() -switch v := thirdT.(type) -// cases -} -``` - ---- - -### Ranges should only be cuddled with assignments used in the iteration - -`range` statements should only cuddle with assignments related to it. Otherwise, -it creates unrelated relationship perception that sends the reader to wonder -why are they closely together. One bad example is: - -```go -y := []string{"a", "b", "c"} - -x := 5 -for _, v := range y { - fmt.Println(v) -} -``` - -#### Recommended amendment - -Either group the related assignment together with the `range` block and -add an empty line before them (first `range`) OR an empty line before the -`range` block (second `range`): - -```go -y := []string{"a", "b", "c"} -for _, v := range y { - fmt.Println(v) -} - -// May also cuddle if used first in block -t2 := []string{} -for _, v := range y { - t2 = append(t2, v) -} -``` - ---- - -### Return statements should not be cuddled if block has more than two lines - -`return` statement should not be cuddled if the function block is not a -2-lines block. Otherwise, there should be a clarity with `return` line. If -the function block is single/double lines, the `return` statement can be -cuddled. - -```go -func F1(x int) (s string) { - switch x { - case 1: - s = "one" - case 2: - s = "two" - case 3: - s = "three" - } - return s -} -``` - -#### Recommended amendment - -An empty line between `return` and multi-line block or no empty line between -`return` and single-line block. - -```go -func F1(x int) (s string) { - switch x { - case 1: - s = "one" - case 2: - s = "two" - case 3: - s = "three" - } - - return s -} - -func PlusFifteenAsString(y int) string { - y += 15 - return fmt.Sprintf("%s", y) -} - -func IsNotZero(i int) bool { - return i != 0 -} -``` - ---- - -### Switch statements should only be cuddled with variables switched - -`switch` statements with associated switching variable should not cuddle with -non-associated switching entity. This will set the reader wondering why are -they grouped together at the first place. One bad example is: - -```go -notInSwitch := "" -switch someThingElse { -case 1: - fmt.Println("one") -case 2: - fmt.Println("one") -case 3: - fmt.Println("three") -} -``` - -#### Recommended amendment - -Group related assignment together and add an empty line before them OR add an -empty line before the `switch`: - -```go -notInSwitch := "" - -switch someThingElse { -case 1: - fmt.Println("one") -case 2: - fmt.Println("one") -case 3: - fmt.Println("three") -} - -inSwitch := 2 -switch inSwitch { -case 1: - fmt.Println("better") -} -``` - ---- - -### Type switch statements should only be cuddled with variables switched - -`type` `switch` statements should only cuddle with its switching variable. -Otherwise, it makes unclear relationship between the `switch` block and whatever -before it. Here is a bad example: - -```go -notInSwitch := GetSomeType() -switch someThingElse.(type) { -case int: - fmt.Println("int") -default: - fmt.Println("not int") -} -``` - -#### Recommended amendment - -Give an empty line before the `switch` statement: - -```go -notInSwitch := GetSomeType() - -switch someThingElse.(type) { -case int: - fmt.Println("int") -default: - fmt.Println("not int") -} - -inSwitch := GetSomeType() -switch inSwitch.(type) { -// cases -} -``` - ---- - -### If statements that check an error must be cuddled with the statement that assigned the error - -> Can be configured, see [configuration -documentation](configuration.md#force-err-cuddling) - -When an `if` statement checks a variable named `err`, which was assigned on the line above, it should be cuddled with that assignment. This makes clear the relationship between the error check and the call that may have resulted in an error. - -```go -err := ErrorProducingFunc() - -if err != nil { - return err -} -``` - -#### Recommended amendment - -Remove the empty line between the assignment and the error check. - -```go -err := ErrorProducingFunc() -if err != nil { - return err -} -``` - ---- - -### Short declaration should cuddle only with other short declarations - -> Can be configured, see [configuration -documentation](configuration.md#force-short-decl-cuddling) - -A short declaration (an "assignment" using `:=`) must not be cuddled with anything other than another short declaration. - -```go -a := 1 -err := ErrorProducingFunc() -if err != nil { - return err -} -``` - -#### Recommended amendment - -Add an empty line between the short declaration and the error check. - -```go -a := 1 -err := ErrorProducingFunc() - -if err != nil { - return err -} -``` - -**Note**: this is the opposite of the case above forcing an `err` variable -to be assigned together with its check; it also overrides some other rules -which about assignments by separating short declarations from "plain" -assignment statements using `=`. diff --git a/go.mod b/go.mod index def9fb2..d25148b 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,18 @@ -module github.com/bombsimon/wsl/v4 +module github.com/bombsimon/wsl/v5 -go 1.19 +go 1.23.0 -require golang.org/x/tools v0.6.0 +toolchain go1.24.1 require ( - golang.org/x/mod v0.8.0 // indirect - golang.org/x/sys v0.5.0 // indirect + github.com/stretchr/testify v1.9.0 + golang.org/x/tools v0.32.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/sync v0.13.0 // indirect ) diff --git a/go.sum b/go.sum index 44d387e..b2e79ca 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,18 @@ -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/testdata/src/default_config/assign/assign.go b/testdata/src/default_config/assign/assign.go new file mode 100644 index 0000000..e3edd6d --- /dev/null +++ b/testdata/src/default_config/assign/assign.go @@ -0,0 +1,72 @@ +package testpkg + +import ( + "bytes" + "encoding/json" +) + +type T struct { + I int +} + +func NewT() *T { + return &T{} +} + +func (*T) Fn() int { + return 1 +} + +func FnC(_ string, fn func() error) error { + fn() + return nil +} + +func strictAppend() { + s := []string{} + s = append(s, "a") + s = append(s, "b") + x := "c" + s = append(s, x) + y := "e" + s = append(s, "d") // want `missing whitespace above this line \(no shared variables above assign\)` + s = append(s, y) +} + +func incDec() { + x := 1 + x++ + x-- + y := x + + _ = y +} + +func assignAndCall() { + t1 := NewT() + t2 := NewT() + + t1.Fn() + x := t1.Fn() + t1.Fn() +} + +func closureInCall() { + buf := &bytes.Buffer{} + err := Fnc("buf", func() error { + return json.NewEncoder(buf).Encode("x") + }) +} + +func assignAfterBlock() { + x := 1 + if x > 0 { + return + } + x = 2 // want `missing whitespace above this line \(invalid statement above assign\)` +} + +func decl() { + var x string + y := "" // want `missing whitespace above this line \(invalid statement above assign\)` +} diff --git a/testdata/src/default_config/assign/assign.go.golden b/testdata/src/default_config/assign/assign.go.golden new file mode 100644 index 0000000..1dad1b5 --- /dev/null +++ b/testdata/src/default_config/assign/assign.go.golden @@ -0,0 +1,75 @@ +package testpkg + +import ( + "bytes" + "encoding/json" +) + +type T struct { + I int +} + +func NewT() *T { + return &T{} +} + +func (*T) Fn() int { + return 1 +} + +func FnC(_ string, fn func() error) error { + fn() + return nil +} + +func strictAppend() { + s := []string{} + s = append(s, "a") + s = append(s, "b") + x := "c" + s = append(s, x) + y := "e" + + s = append(s, "d") // want `missing whitespace above this line \(no shared variables above assign\)` + s = append(s, y) +} + +func incDec() { + x := 1 + x++ + x-- + y := x + + _ = y +} + +func assignAndCall() { + t1 := NewT() + t2 := NewT() + + t1.Fn() + x := t1.Fn() + t1.Fn() +} + +func closureInCall() { + buf := &bytes.Buffer{} + err := Fnc("buf", func() error { + return json.NewEncoder(buf).Encode("x") + }) +} + +func assignAfterBlock() { + x := 1 + if x > 0 { + return + } + + x = 2 // want `missing whitespace above this line \(invalid statement above assign\)` +} + +func decl() { + var x string + + y := "" // want `missing whitespace above this line \(invalid statement above assign\)` +} diff --git a/testdata/src/default_config/branch/branch.go b/testdata/src/default_config/branch/branch.go new file mode 100644 index 0000000..a16c923 --- /dev/null +++ b/testdata/src/default_config/branch/branch.go @@ -0,0 +1,97 @@ +package testpkg + +import "fmt" + +func fn1() { + for range []int{} { + if true { + fmt.Println("") + + continue + } + + if true { + fmt.Println("") + break + } + + if true { + fmt.Println("") + continue + } + + if true { + fmt.Println("") + fallthrough + } + + if true { + fmt.Println("") + goto START + } + + if false { + fmt.Println("") + fmt.Println("") + fmt.Println("") + + break + } + + if false { + fmt.Println("") + fmt.Println("") + fmt.Println("") + + continue + } + + if false { + fmt.Println("") + fmt.Println("") + fmt.Println("") + + fallthrough + } + + if false { + fmt.Println("") + fmt.Println("") + fmt.Println("") + + goto START + } + } +} + +func fn2() { + for range []int{} { + if true { + fmt.Println("") + fmt.Println("") + fmt.Println("") + break // want `missing whitespace above this line \(too many lines above branch\)` + } + + if true { + fmt.Println("") + fmt.Println("") + fmt.Println("") + continue // want `missing whitespace above this line \(too many lines above branch\)` + } + + if true { + fmt.Println("") + fmt.Println("") + fmt.Println("") + fallthrough // want `missing whitespace above this line \(too many lines above branch\)` + } + + if true { + fmt.Println("") + fmt.Println("") + fmt.Println("") + goto START // want `missing whitespace above this line \(too many lines above branch\)` + } + } +} diff --git a/testdata/src/default_config/branch/branch.go.golden b/testdata/src/default_config/branch/branch.go.golden new file mode 100644 index 0000000..de9c6bb --- /dev/null +++ b/testdata/src/default_config/branch/branch.go.golden @@ -0,0 +1,101 @@ +package testpkg + +import "fmt" + +func fn1() { + for range []int{} { + if true { + fmt.Println("") + + continue + } + + if true { + fmt.Println("") + break + } + + if true { + fmt.Println("") + continue + } + + if true { + fmt.Println("") + fallthrough + } + + if true { + fmt.Println("") + goto START + } + + if false { + fmt.Println("") + fmt.Println("") + fmt.Println("") + + break + } + + if false { + fmt.Println("") + fmt.Println("") + fmt.Println("") + + continue + } + + if false { + fmt.Println("") + fmt.Println("") + fmt.Println("") + + fallthrough + } + + if false { + fmt.Println("") + fmt.Println("") + fmt.Println("") + + goto START + } + } +} + +func fn2() { + for range []int{} { + if true { + fmt.Println("") + fmt.Println("") + fmt.Println("") + + break // want `missing whitespace above this line \(too many lines above branch\)` + } + + if true { + fmt.Println("") + fmt.Println("") + fmt.Println("") + + continue // want `missing whitespace above this line \(too many lines above branch\)` + } + + if true { + fmt.Println("") + fmt.Println("") + fmt.Println("") + + fallthrough // want `missing whitespace above this line \(too many lines above branch\)` + } + + if true { + fmt.Println("") + fmt.Println("") + fmt.Println("") + + goto START // want `missing whitespace above this line \(too many lines above branch\)` + } + } +} diff --git a/testdata/src/default_config/decl/decl.go b/testdata/src/default_config/decl/decl.go new file mode 100644 index 0000000..80c5ead --- /dev/null +++ b/testdata/src/default_config/decl/decl.go @@ -0,0 +1,30 @@ +package testpkg + +func fn1() { + var a = 1 + var b = 2 // want `missing whitespace above this line \(never cuddle decl\)` + + const c = 3 + const d = 4 // want `missing whitespace above this line \(never cuddle decl\)` + + e := 5 + var f = 6 // want `missing whitespace above this line \(never cuddle decl\)` + g := 7 // want `missing whitespace above this line \(invalid statement above assign\)` +} + +func fn2() { + var a = 1 + var b = a // want `missing whitespace above this line \(never cuddle decl\)` +} + +func fn3() { + a := 1 + var b = a // want `missing whitespace above this line \(never cuddle decl\)` +} + +func fn4() { + var x = func() { // want +1 `unnecessary whitespace \(leading-whitespace\)` + + return 1 + }() +} diff --git a/testdata/src/default_config/decl/decl.go.golden b/testdata/src/default_config/decl/decl.go.golden new file mode 100644 index 0000000..c6601d6 --- /dev/null +++ b/testdata/src/default_config/decl/decl.go.golden @@ -0,0 +1,35 @@ +package testpkg + +func fn1() { + var a = 1 + + var b = 2 // want `missing whitespace above this line \(never cuddle decl\)` + + const c = 3 + + const d = 4 // want `missing whitespace above this line \(never cuddle decl\)` + + e := 5 + + var f = 6 // want `missing whitespace above this line \(never cuddle decl\)` + + g := 7 // want `missing whitespace above this line \(invalid statement above assign\)` +} + +func fn2() { + var a = 1 + + var b = a // want `missing whitespace above this line \(never cuddle decl\)` +} + +func fn3() { + a := 1 + + var b = a // want `missing whitespace above this line \(never cuddle decl\)` +} + +func fn4() { + var x = func() { // want +1 `unnecessary whitespace \(leading-whitespace\)` + return 1 + }() +} diff --git a/testdata/src/default_config/defer.go b/testdata/src/default_config/defer.go deleted file mode 100644 index 27f424d..0000000 --- a/testdata/src/default_config/defer.go +++ /dev/null @@ -1,58 +0,0 @@ -package testpkg - -import "fmt" - -func fn1() { - undoMaxProcs, err := maxprocsSet() - if err != nil { - return fmt.Errorf("failed to set GOMAXPROCS, err: %w", err) - } - defer undoMaxProcs() - - callback, x := getCb() - if x != b { - return - } - defer callback() - - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - db, err := OpenDB() - requireNoError(t, err) - defer db.Close() - - tx := BeginTx(db) - defer func() { - EndTx(tx, err) - }() - - thingOne := getOne() - thingTwo := getTwo() - defer thingOne.Close() -} - -func fn2() { - thingOne := getOne() - thingTwo := getTwo() - defer thingTwo.Close() // want "only one cuddle assignment allowed before defer statement" - - someF, err := getF() - if err != nil { - return err - } - someF() // want "expressions should not be cuddled with block" - - thingOne := getOne() - defer thingOne.Close() - thingTwo := getTwo() // want "assignments should only be cuddled with other assignments" - defer thingTwo.Close() // want "only one cuddle assignment allowed before defer statement" - - thingOne := getOne() - thingTwo := getTwo() - defer thingOne.Close() // want "only one cuddle assignment allowed before defer statement" - defer thingTwo.Close() -} diff --git a/testdata/src/default_config/defer.go.golden b/testdata/src/default_config/defer.go.golden deleted file mode 100644 index aff6e96..0000000 --- a/testdata/src/default_config/defer.go.golden +++ /dev/null @@ -1,62 +0,0 @@ -package testpkg - -import "fmt" - -func fn1() { - undoMaxProcs, err := maxprocsSet() - if err != nil { - return fmt.Errorf("failed to set GOMAXPROCS, err: %w", err) - } - defer undoMaxProcs() - - callback, x := getCb() - if x != b { - return - } - defer callback() - - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - db, err := OpenDB() - requireNoError(t, err) - defer db.Close() - - tx := BeginTx(db) - defer func() { - EndTx(tx, err) - }() - - thingOne := getOne() - thingTwo := getTwo() - defer thingOne.Close() -} - -func fn2() { - thingOne := getOne() - - thingTwo := getTwo() - defer thingTwo.Close() // want "only one cuddle assignment allowed before defer statement" - - someF, err := getF() - if err != nil { - return err - } - - someF() // want "expressions should not be cuddled with block" - - thingOne := getOne() - defer thingOne.Close() - - thingTwo := getTwo() // want "assignments should only be cuddled with other assignments" - defer thingTwo.Close() // want "only one cuddle assignment allowed before defer statement" - - thingOne := getOne() - thingTwo := getTwo() - - defer thingOne.Close() // want "only one cuddle assignment allowed before defer statement" - defer thingTwo.Close() -} diff --git a/testdata/src/default_config/defer/defer.go b/testdata/src/default_config/defer/defer.go new file mode 100644 index 0000000..cdae75a --- /dev/null +++ b/testdata/src/default_config/defer/defer.go @@ -0,0 +1,69 @@ +package testpkg + +import ( + "fmt" + "os" + "sync" +) + +type T int + +func Fn() T { + return T(0) +} + +func (*T) Close() {} + +func Fn() {} + +func fn() { + a := Fn() + b := Fn() + + defer a.Close() + defer b.Close() + + a = Fn() + defer a.Close() + + b = Fn() + defer b.Close() + + a = Fn() + defer a.Close() + b = Fn() // want `missing whitespace above this line \(invalid statement above assign\)` + defer b.Close() + + a = Fn() + b = Fn() + defer a.Close() // want `missing whitespace above this line \(no shared variables above defer\)` + defer b.Close() + + m := sync.Mutex{} + + m.Lock() + defer m.Unlock() + + c := true + defer func(b bool) { // want `missing whitespace above this line \(no shared variables above defer\)` + fmt.Printf("%v", b) + }() + + _ = c +} + +func fn2() { + a := 1 + b := Fn() // want `missing whitespace above this line \(too many statements above defer\)` + defer b.Close() + + _ = a +} + +func fn3() { + f, err := os.Open("x") + if err != nil { + panic(err) + } + defer f.Close() +} diff --git a/testdata/src/default_config/defer/defer.go.golden b/testdata/src/default_config/defer/defer.go.golden new file mode 100644 index 0000000..d042030 --- /dev/null +++ b/testdata/src/default_config/defer/defer.go.golden @@ -0,0 +1,73 @@ +package testpkg + +import ( + "fmt" + "os" + "sync" +) + +type T int + +func Fn() T { + return T(0) +} + +func (*T) Close() {} + +func Fn() {} + +func fn() { + a := Fn() + b := Fn() + + defer a.Close() + defer b.Close() + + a = Fn() + defer a.Close() + + b = Fn() + defer b.Close() + + a = Fn() + defer a.Close() + + b = Fn() // want `missing whitespace above this line \(invalid statement above assign\)` + defer b.Close() + + a = Fn() + b = Fn() + + defer a.Close() // want `missing whitespace above this line \(no shared variables above defer\)` + defer b.Close() + + m := sync.Mutex{} + + m.Lock() + defer m.Unlock() + + c := true + + defer func(b bool) { // want `missing whitespace above this line \(no shared variables above defer\)` + fmt.Printf("%v", b) + }() + + _ = c +} + +func fn2() { + a := 1 + + b := Fn() // want `missing whitespace above this line \(too many statements above defer\)` + defer b.Close() + + _ = a +} + +func fn3() { + f, err := os.Open("x") + if err != nil { + panic(err) + } + defer f.Close() +} diff --git a/testdata/src/default_config/doc_examples.go b/testdata/src/default_config/doc_examples.go deleted file mode 100644 index 023a7c0..0000000 --- a/testdata/src/default_config/doc_examples.go +++ /dev/null @@ -1,290 +0,0 @@ -package pkg - -import ( - "fmt" - "os" -) - -func AnonyMousStatement() { - timeout := 10 - switch { // want "anonymous switch statements should never be cuddled" - case true: - timeout = 100 - case false: - timeout = 0 - } -} - -func Append() { - x := []string{} - x = append(x, "literal") - notUsed := "just assigning, don't mind me" - x = append(x, "not z..") // want "append only allowed to cuddle with appended value" - useMe := "right away" - alsoNotUsed := ":(" - x = append(x, "just noise") // want "append only allowed to cuddle with appended value" - x = append(x, useMe) -} - -func Assignment() { - if x == 1 { - x = 0 - } - z := x + 2 // want "assignments should only be cuddled with other assignments" - - fmt.Println("x") - y := "x" // want "assignments should only be cuddled with other assignments" -} - -func TrailingWhitespaceOrComment() { - return fmt.Sprintf("x") - // REMEMBER: add mux function later. - -} // want "block should not end with a whitespace" - -// Default is for this turned off so no diagnostic. -func CaseTrailingWhitespace() { - switch n { - case 1: - fmt.Println("one") - case 2: - fmt.Println("two") - } -} - -func StartingWhitespace() { // want "block should not start with a whitespace" - - - return -} - -func BranchStatement() { - for i := range make([]int, 5) { - if i > 2 { - sendToOne(i) - sendToSecond(i) - continue // want "branch statements should not be cuddled if block has more than two lines" - } - - if statement == "is short" { - sendToOne(i) - break - } - } -} - -func Declarations() { - var eol string - var i int // want "declarations should never be cuddled" -} - -func Defer() { - x := 4 - defer notX() // want "defer statements should only be cuddled with expressions on same variable" - - thisIsX := func() {} - defer thisIsX() -} - -func ExpressionAndBlocks() { - t, err := someFunc() - if err != nil { - // handle error - return - } - fmt.Println(t) // want "expressions should not be cuddled with blocks" -} - -func ExpressionAndDeclarationOrReturn() { - var i int - run() // want "expressions should not be cuddled with declarations or returns" - - return i - run() // want "expressions should not be cuddled with declarations or returns" -} - -func ForWithoutCondition() { - x := "hey" - for { // want "for statement without condition should never be cuddled" - fmt.Printf("x") - } -} - -func For() { - x := y + 2 - for i := 0; i < y+10; i++ { // want "for statements should only be cuddled with assignments used in the iteration" - fmt.Println(i) - } - - list := getList() - for i, v := range anotherList { // want "ranges should only be cuddled with assignments used in the iteration" - fmt.Println(i) - } -} - -func Go() { - thisIsFunc := func() {} - go thisIsNOTFunc() // want "go statements can only invoke functions assigned on line above" - - thisIsFunc := func() {} - go thisIsFunc() -} - -func IfAssign() { - one := 1 - if 1 == 1 { // want "if statements should only be cuddled with assignments used in the if statement itself" - fmt.Println("duuh") - } - if 1 != one { // want "if statements should only be cuddled with assignments" - fmt.Println("i don't believe you!") - } - - two := 2 - if two != one { - fmt.Println("i don't believe you!") - } -} - -func ExpressionAndAssignment() { - notInExpression := GetIt() - call.SomethingElse() // want "only cuddled expressions if assigning variable or using from line above" - - thisIsBetter := GetIt() - thisIsBetter.CallIt() -} - -func OnlyOneDefer() { - assignmentOne := Something() - assignmentTwo := AnotherThing() - defer assignmentTwo() // want "only one cuddle assignment allowed before defer statement" - - f1, err := os.Open("/path/to/f1.txt") - if err != nil { - return -1 - } - defer f1.Close() - - m.Lock() - defer m.Unlock() -} - -func OnlyOneFor() { - i := 0 - a := 0 - for i = 0; i < 5; i++ { // want "only one cuddle assignment allowed before for statement" - fmt.Println("x") - } - - a := 0 - i := 0 - for i = 0; i < 5; i++ { // want "only one cuddle assignment allowed before for statement" - fmt.Println("x") - } -} - -func OnlyOneGo() { - assignOne := SomeOne() - assignTwo := SomeTwo() - go assignTwo() // want "only one cuddle assignment allowed before go statement" -} - -func OnlyOneIf() { - lengthA := len(sliceA) - lengthB := len(sliceB) - if lengthA != lengthB { // want "only one cuddle assignment allowed before if statement" - fmt.Println("not equal") - } -} - -func OnlyOneRange() { - listA := GetA() - listB := GetB() - for _, v := range listB { // want "only one cuddle assignment allowed before range statement" - fmt.Println(v) - } -} - -func OnlyOneSwitch() { - assignOne := SomeOone() - assignTwo := SomeTwo() - switch assignTwo { // want "only one cuddle assignment allowed before switch statement" - case 1: - fmt.Println("one") - default: - fmt.Println("NOT one") - } -} - -func OnlyOneTypeSwitch() { - someT := GetT() - anotherT := GetT() - switch v := anotherT.(type) { // want "only one cuddle assignment allowed before type switch statement" - case int: - fmt.Println("was int") - default: - fmt.Println("was not int") - } -} - -func RangeWithAssignment() { - y := []string{"a", "b", "c"} - - x := 5 - for _, v := range y { // want "ranges should only be cuddled with assignments used in the iteration" - fmt.Println(v) - } - - x := 5 - - y := []string{"a", "b", "c"} - for _, v := range y { - fmt.Println(v) - } -} - -func Return() { - _ = func() { - y += 15 - y += 15 - return fmt.Sprintf("%s", y) // want "return statements should not be cuddled if block has more than two lines" - } - - _ = func() { - y += 15 - return fmt.Sprintf("%s", y) - } -} - -func Switch() { - notInSwitch := "" - switch someThingElse { // want "switch statements should only be cuddled with variables switched" - case 1: - fmt.Println("one") - case 2: - fmt.Println("one") - case 3: - fmt.Println("three") - } - - inSwitch := 2 - switch inSwitch { - case 1: - fmt.Println("better") - } -} - -func TypeSwitch() { - notInSwitch := GetSomeType() - switch someThingElse.(type) { // want "type switch statements should only be cuddled with variables switched" - case int: - fmt.Println("int") - default: - fmt.Println("not int") - } - - inSwitch := GetSomeType() - switch inSwitch.(type) { - case int: - fmt.Println("better") - } -} diff --git a/testdata/src/default_config/doc_examples.go.golden b/testdata/src/default_config/doc_examples.go.golden deleted file mode 100644 index 7426bbb..0000000 --- a/testdata/src/default_config/doc_examples.go.golden +++ /dev/null @@ -1,317 +0,0 @@ -package pkg - -import ( - "fmt" - "os" -) - -func AnonyMousStatement() { - timeout := 10 - - switch { // want "anonymous switch statements should never be cuddled" - case true: - timeout = 100 - case false: - timeout = 0 - } -} - -func Append() { - x := []string{} - x = append(x, "literal") - notUsed := "just assigning, don't mind me" - - x = append(x, "not z..") // want "append only allowed to cuddle with appended value" - useMe := "right away" - alsoNotUsed := ":(" - - x = append(x, "just noise") // want "append only allowed to cuddle with appended value" - x = append(x, useMe) -} - -func Assignment() { - if x == 1 { - x = 0 - } - - z := x + 2 // want "assignments should only be cuddled with other assignments" - - fmt.Println("x") - - y := "x" // want "assignments should only be cuddled with other assignments" -} - -func TrailingWhitespaceOrComment() { - return fmt.Sprintf("x") - // REMEMBER: add mux function later. -} // want "block should not end with a whitespace" - -// Default is for this turned off so no diagnostic. -func CaseTrailingWhitespace() { - switch n { - case 1: - fmt.Println("one") - case 2: - fmt.Println("two") - } -} - -func StartingWhitespace() { // want "block should not start with a whitespace" - return -} - -func BranchStatement() { - for i := range make([]int, 5) { - if i > 2 { - sendToOne(i) - sendToSecond(i) - - continue // want "branch statements should not be cuddled if block has more than two lines" - } - - if statement == "is short" { - sendToOne(i) - break - } - } -} - -func Declarations() { - var eol string - - var i int // want "declarations should never be cuddled" -} - -func Defer() { - x := 4 - - defer notX() // want "defer statements should only be cuddled with expressions on same variable" - - thisIsX := func() {} - defer thisIsX() -} - -func ExpressionAndBlocks() { - t, err := someFunc() - if err != nil { - // handle error - return - } - - fmt.Println(t) // want "expressions should not be cuddled with blocks" -} - -func ExpressionAndDeclarationOrReturn() { - var i int - - run() // want "expressions should not be cuddled with declarations or returns" - - return i - - run() // want "expressions should not be cuddled with declarations or returns" -} - -func ForWithoutCondition() { - x := "hey" - - for { // want "for statement without condition should never be cuddled" - fmt.Printf("x") - } -} - -func For() { - x := y + 2 - - for i := 0; i < y+10; i++ { // want "for statements should only be cuddled with assignments used in the iteration" - fmt.Println(i) - } - - list := getList() - - for i, v := range anotherList { // want "ranges should only be cuddled with assignments used in the iteration" - fmt.Println(i) - } -} - -func Go() { - thisIsFunc := func() {} - - go thisIsNOTFunc() // want "go statements can only invoke functions assigned on line above" - - thisIsFunc := func() {} - go thisIsFunc() -} - -func IfAssign() { - one := 1 - - if 1 == 1 { // want "if statements should only be cuddled with assignments used in the if statement itself" - fmt.Println("duuh") - } - - if 1 != one { // want "if statements should only be cuddled with assignments" - fmt.Println("i don't believe you!") - } - - two := 2 - if two != one { - fmt.Println("i don't believe you!") - } -} - -func ExpressionAndAssignment() { - notInExpression := GetIt() - - call.SomethingElse() // want "only cuddled expressions if assigning variable or using from line above" - - thisIsBetter := GetIt() - thisIsBetter.CallIt() -} - -func OnlyOneDefer() { - assignmentOne := Something() - - assignmentTwo := AnotherThing() - defer assignmentTwo() // want "only one cuddle assignment allowed before defer statement" - - f1, err := os.Open("/path/to/f1.txt") - if err != nil { - return -1 - } - defer f1.Close() - - m.Lock() - defer m.Unlock() -} - -func OnlyOneFor() { - i := 0 - a := 0 - - for i = 0; i < 5; i++ { // want "only one cuddle assignment allowed before for statement" - fmt.Println("x") - } - - a := 0 - - i := 0 - for i = 0; i < 5; i++ { // want "only one cuddle assignment allowed before for statement" - fmt.Println("x") - } -} - -func OnlyOneGo() { - assignOne := SomeOne() - - assignTwo := SomeTwo() - go assignTwo() // want "only one cuddle assignment allowed before go statement" -} - -func OnlyOneIf() { - lengthA := len(sliceA) - lengthB := len(sliceB) - - if lengthA != lengthB { // want "only one cuddle assignment allowed before if statement" - fmt.Println("not equal") - } -} - -func OnlyOneRange() { - listA := GetA() - - listB := GetB() - for _, v := range listB { // want "only one cuddle assignment allowed before range statement" - fmt.Println(v) - } -} - -func OnlyOneSwitch() { - assignOne := SomeOone() - - assignTwo := SomeTwo() - switch assignTwo { // want "only one cuddle assignment allowed before switch statement" - case 1: - fmt.Println("one") - default: - fmt.Println("NOT one") - } -} - -func OnlyOneTypeSwitch() { - someT := GetT() - - anotherT := GetT() - switch v := anotherT.(type) { // want "only one cuddle assignment allowed before type switch statement" - case int: - fmt.Println("was int") - default: - fmt.Println("was not int") - } -} - -func RangeWithAssignment() { - y := []string{"a", "b", "c"} - - x := 5 - - for _, v := range y { // want "ranges should only be cuddled with assignments used in the iteration" - fmt.Println(v) - } - - x := 5 - - y := []string{"a", "b", "c"} - for _, v := range y { - fmt.Println(v) - } -} - -func Return() { - _ = func() { - y += 15 - y += 15 - - return fmt.Sprintf("%s", y) // want "return statements should not be cuddled if block has more than two lines" - } - - _ = func() { - y += 15 - return fmt.Sprintf("%s", y) - } -} - -func Switch() { - notInSwitch := "" - - switch someThingElse { // want "switch statements should only be cuddled with variables switched" - case 1: - fmt.Println("one") - case 2: - fmt.Println("one") - case 3: - fmt.Println("three") - } - - inSwitch := 2 - switch inSwitch { - case 1: - fmt.Println("better") - } -} - -func TypeSwitch() { - notInSwitch := GetSomeType() - - switch someThingElse.(type) { // want "type switch statements should only be cuddled with variables switched" - case int: - fmt.Println("int") - default: - fmt.Println("not int") - } - - inSwitch := GetSomeType() - switch inSwitch.(type) { - case int: - fmt.Println("better") - } -} diff --git a/testdata/src/default_config/else_if.go b/testdata/src/default_config/else_if.go deleted file mode 100644 index 7b10487..0000000 --- a/testdata/src/default_config/else_if.go +++ /dev/null @@ -1,20 +0,0 @@ -func fn() { - if true { // want "block should not start with a whitespace" - - fmt.Println("a") - } - - if true { - fmt.Println("a") - } else if false { // want "block should not start with a whitespace" - - fmt.Println("b") - } else { // want "block should not start with a whitespace" - - fmt.Println("c") - fmt.Println("c") - fmt.Println("c") - return // want "return statements should not be cuddled if block has more than two lines" - - } // want "block should not end with a whitespace" -} diff --git a/testdata/src/default_config/else_if.go.golden b/testdata/src/default_config/else_if.go.golden deleted file mode 100644 index 44af882..0000000 --- a/testdata/src/default_config/else_if.go.golden +++ /dev/null @@ -1,16 +0,0 @@ -func fn() { - if true { // want "block should not start with a whitespace" - fmt.Println("a") - } - - if true { - fmt.Println("a") - } else if false { // want "block should not start with a whitespace" - fmt.Println("b") - } else { // want "block should not start with a whitespace" - fmt.Println("c") - fmt.Println("c") - fmt.Println("c") - return // want "return statements should not be cuddled if block has more than two lines" - } // want "block should not end with a whitespace" -} diff --git a/testdata/src/default_config/expr/expr.go b/testdata/src/default_config/expr/expr.go new file mode 100644 index 0000000..8b7af20 --- /dev/null +++ b/testdata/src/default_config/expr/expr.go @@ -0,0 +1,56 @@ +package testpkg + +import "fmt" + +func fn() { + a := 1 + b := 2 + c := 3 + a = 4 + b = 3 + + _ = a + _ = b + _ = c +} + +func fn2() { + a := 1 + b := 2 + fmt.Println("") // want `missing whitespace above this line \(no shared variables above expr\)` + c := 3 // want `missing whitespace above this line \(invalid statement above assign\)` + d := 4 + + _ = a + _ = b + _ = c + _ = d +} + +func fn3() { + a := 1 + b := 2 + fmt.Println(b) + c := 3 // want `missing whitespace above this line \(invalid statement above assign\)` + d := 4 + + _ = a + _ = b + _ = c + _ = d +} + +func fn4() { + b := 2 + <-ch // want `missing whitespace above this line \(no shared variables above expr\)` +} + +func fn5() { + s := []string{ + func() string { // want +1 `unnecessary whitespace \(leading-whitespace\)` + + "a" + }, + "b", + } +} diff --git a/testdata/src/default_config/expr/expr.go.golden b/testdata/src/default_config/expr/expr.go.golden new file mode 100644 index 0000000..3c52043 --- /dev/null +++ b/testdata/src/default_config/expr/expr.go.golden @@ -0,0 +1,59 @@ +package testpkg + +import "fmt" + +func fn() { + a := 1 + b := 2 + c := 3 + a = 4 + b = 3 + + _ = a + _ = b + _ = c +} + +func fn2() { + a := 1 + b := 2 + + fmt.Println("") // want `missing whitespace above this line \(no shared variables above expr\)` + + c := 3 // want `missing whitespace above this line \(invalid statement above assign\)` + d := 4 + + _ = a + _ = b + _ = c + _ = d +} + +func fn3() { + a := 1 + b := 2 + fmt.Println(b) + + c := 3 // want `missing whitespace above this line \(invalid statement above assign\)` + d := 4 + + _ = a + _ = b + _ = c + _ = d +} + +func fn4() { + b := 2 + + <-ch // want `missing whitespace above this line \(no shared variables above expr\)` +} + +func fn5() { + s := []string{ + func() string { // want +1 `unnecessary whitespace \(leading-whitespace\)` + "a" + }, + "b", + } +} diff --git a/testdata/src/default_config/fix_advanced.go b/testdata/src/default_config/fix_advanced.go deleted file mode 100644 index 4ba14c8..0000000 --- a/testdata/src/default_config/fix_advanced.go +++ /dev/null @@ -1,22 +0,0 @@ -package testpkg - -func Advanced() { - var foo = 1 - var bar = 2 // want "declarations should never be cuddled" - var biz int // want "declarations should never be cuddled" - - x := []string{} - x = append(x, "literal") - notUsed := "just assigning, don't mind me" - x = append(x, "not z..") // want "append only allowed to cuddle with appended value" - useMe := "right away" - alsoNotUsed := ":(" - x = append(x, "just noise") // want "append only allowed to cuddle with appended value" - x = append(x, useMe) - - _ = foo - _ = bar - _ = biz - _ = notUsed - _ = alsoNotUsed -} diff --git a/testdata/src/default_config/fix_advanced.go.golden b/testdata/src/default_config/fix_advanced.go.golden deleted file mode 100644 index eae2712..0000000 --- a/testdata/src/default_config/fix_advanced.go.golden +++ /dev/null @@ -1,26 +0,0 @@ -package testpkg - -func Advanced() { - var foo = 1 - - var bar = 2 // want "declarations should never be cuddled" - - var biz int // want "declarations should never be cuddled" - - x := []string{} - x = append(x, "literal") - notUsed := "just assigning, don't mind me" - - x = append(x, "not z..") // want "append only allowed to cuddle with appended value" - useMe := "right away" - alsoNotUsed := ":(" - - x = append(x, "just noise") // want "append only allowed to cuddle with appended value" - x = append(x, useMe) - - _ = foo - _ = bar - _ = biz - _ = notUsed - _ = alsoNotUsed -} diff --git a/testdata/src/default_config/fix_cuddle_blocks.go b/testdata/src/default_config/fix_cuddle_blocks.go deleted file mode 100644 index 0c7f1a4..0000000 --- a/testdata/src/default_config/fix_cuddle_blocks.go +++ /dev/null @@ -1,62 +0,0 @@ -package testpkg - -import "fmt" - -func FailBlocks() { - if false { - return - } - _, err := maybeErr() // want "assignments should only be cuddled with other assignments" - if err != nil { // want "only one cuddle assignment allowed before if statement" - return - } - - if true { - return - } - _, x := maybeErr() // want "assignments should only be cuddled with other assignments" - if err != nil { // want "only one cuddle assignment allowed before if statement" - return - } - - x = nil - _, err = maybeErr() - if err != nil { // want "only one cuddle assignment allowed before if statement" - panic(err) - } - - if x != nil { - } - switch 1 { // want "anonymous switch statements should never be cuddled" - case 1: - fmt.Println("one") - case 2: - fmt.Println("two") - case 3: - fmt.Println("three") - } - - foo := 1 - bar := 2 - baz := 3 - if 1 == 1 { // want "only one cuddle assignment allowed before if statement" - baz = 4 - } - - _ = foo - _ = bar - _ = baz - - a := 1 - b := 2 - for b < bar { // want "only one cuddle assignment allowed before for statement" - b-- - } - - _ = a - _ = b -} - -func maybeErr() (int, error) { - return 0, nil -} diff --git a/testdata/src/default_config/fix_cuddle_blocks.go.golden b/testdata/src/default_config/fix_cuddle_blocks.go.golden deleted file mode 100644 index 7b91d40..0000000 --- a/testdata/src/default_config/fix_cuddle_blocks.go.golden +++ /dev/null @@ -1,69 +0,0 @@ -package testpkg - -import "fmt" - -func FailBlocks() { - if false { - return - } - - _, err := maybeErr() // want "assignments should only be cuddled with other assignments" - if err != nil { // want "only one cuddle assignment allowed before if statement" - return - } - - if true { - return - } - - _, x := maybeErr() // want "assignments should only be cuddled with other assignments" - - if err != nil { // want "only one cuddle assignment allowed before if statement" - return - } - - x = nil - - _, err = maybeErr() - if err != nil { // want "only one cuddle assignment allowed before if statement" - panic(err) - } - - if x != nil { - } - - switch 1 { // want "anonymous switch statements should never be cuddled" - case 1: - fmt.Println("one") - case 2: - fmt.Println("two") - case 3: - fmt.Println("three") - } - - foo := 1 - bar := 2 - - baz := 3 - if 1 == 1 { // want "only one cuddle assignment allowed before if statement" - baz = 4 - } - - _ = foo - _ = bar - _ = baz - - a := 1 - - b := 2 - for b < bar { // want "only one cuddle assignment allowed before for statement" - b-- - } - - _ = a - _ = b -} - -func maybeErr() (int, error) { - return 0, nil -} diff --git a/testdata/src/default_config/for/for.go b/testdata/src/default_config/for/for.go new file mode 100644 index 0000000..c5ff1dd --- /dev/null +++ b/testdata/src/default_config/for/for.go @@ -0,0 +1,32 @@ +package testpkg + +func fn1() { + a := 1 + b := 2 // want `missing whitespace above this line \(too many statements above for\)` + for i := 0; i < b; i++ { + panic(1) + } + + _ = a + _ = b +} + +func fn2() { + b := 2 + a := 1 + for i := 0; i < b; i++ { // want `missing whitespace above this line \(no shared variables above for\)` + panic(1) + } + + _ = a + _ = b +} + +func fn3() { + for i := 0; i < 1; i++ { + panic("") + } + for i := 0; i < 1; i++ { // want `missing whitespace above this line \(invalid statement above for\)` + panic("") + } +} diff --git a/testdata/src/default_config/for/for.go.golden b/testdata/src/default_config/for/for.go.golden new file mode 100644 index 0000000..0059a77 --- /dev/null +++ b/testdata/src/default_config/for/for.go.golden @@ -0,0 +1,35 @@ +package testpkg + +func fn1() { + a := 1 + + b := 2 // want `missing whitespace above this line \(too many statements above for\)` + for i := 0; i < b; i++ { + panic(1) + } + + _ = a + _ = b +} + +func fn2() { + b := 2 + a := 1 + + for i := 0; i < b; i++ { // want `missing whitespace above this line \(no shared variables above for\)` + panic(1) + } + + _ = a + _ = b +} + +func fn3() { + for i := 0; i < 1; i++ { + panic("") + } + + for i := 0; i < 1; i++ { // want `missing whitespace above this line \(invalid statement above for\)` + panic("") + } +} diff --git a/testdata/src/default_config/generic_handling.go b/testdata/src/default_config/generic_handling.go deleted file mode 100644 index 418523f..0000000 --- a/testdata/src/default_config/generic_handling.go +++ /dev/null @@ -1,594 +0,0 @@ -package pkg - -import ( - "encoding/json" - "fmt" - "strconv" - "sync" - "time" -) - -func OnlyComments() { - // Also signel comments are ignored - // foo := false - // if true { - // - // foo = true - // - // } -} - -func NoBlock(r string) (x uintptr) - -func CommentOnFnLine() { // This is just a comment - fmt.Println("hello") -} - -func ReturnOnSameLine() error { - s := func() error { return nil } - _ = s -} - -func LabeledBlocks() { - goto end -end: -} - -func If() { - v, err := strconv.Atoi("1") - if err != nil { - fmt.Println(v) - } - - a := true - if a { - fmt.Println("I'm OK") - } - - a, b := true, false - if !a { - fmt.Println("I'm OK") - } - - a, b = true, false - if err != nil { // want "if statements should only be cuddled with assignments used in the if statement itself" - fmt.Println("I'm not OK") - } - - if true { - fmt.Println("I'm OK") - } - if false { // want "if statements should only be cuddled with assignments" - fmt.Println("I'm OK") - } - - c := true - fooBar := "string" - a, b := true, false - if a && b || !c && len(fooBar) > 2 { // want "only one cuddle assignment allowed before if statement" - return false - } - a, b, c := someFunc() // want "assignments should only be cuddled with other assignments" - if z && len(y) > 0 || 3 == 4 { // want "only one cuddle assignment allowed before if statement" - return true - } - - a, b := true, false - if c && b || !c && len(fooBar) > 2 { - return false - } - - r := map[string]interface{}{} - if err := json.NewDecoder(someReader).Decode(&r); err != nil { - return "this should be OK" - } -} - -func Return() { - if true { - if false { - // - } - return // want "return statements should not be cuddled if block has more than two lines" - } - - if false { - fmt.Println("this is less than two lines") - return - } - return // want "return statements should not be cuddled if block has more than two lines" -} - -func AssignmentAndDeclaration() { - if true { - fmt.Println("I'm not OK") - } - foo := true // want "assignments should only be cuddled with other assignments" - - bar := foo - baz := true - biz := false - - var a bool - fmt.Println(a) // want "expressions should not be cuddled with declarations or returns" - - b := true - fmt.Println(b) -} - -func Range() { - anotherList := make([]string, 5) - - myList := make([]int, 10) - for i := range anotherList { // want "ranges should only be cuddled with assignments used in the iteration" - fmt.Println(i) - } - - myList = make([]string, 5) - - anotherList = make([]int, 10) - for i := range anotherList { - fmt.Println(i) - } - - someList, anotherList := GetTwoListsOfStrings() - for i := range append(someList, anotherList...) { - fmt.Println(i) - } - - aThirdList := GetList() - for i := range append(someList, aThirdList...) { - fmt.Println(i) - } -} - -func FirstInBlock() { - idx := i - if i > 0 { - idx = i - 1 - } - - vals := map[int]struct{}{} - for i := range make([]int, 5) { - vals[i] = struct{}{} - } - - x := []int{} - - vals := map[int]struct{}{} - for i := range make([]int, 5) { // want "ranges should only be cuddled with assignments used in the iteration" - x = append(x, i) - } -} - -func OnlyCuddleOneAssignment() { - foo := true - bar := false - - biz := true || false - if biz { - return true - } - - foo := true - bar := false - biz := true || false - - if biz { - return true - } - - foo := true - bar := false - biz := true || false - if biz { // want "only one cuddle assignment allowed before if statement" - return false - } -} - -func IdentifiersWithIndices() { - runes := []rune{'+', '-'} - if runes[0] == '+' || runes[0] == '-' { - return string(runes[1:]) - } - - listTwo := []string{} - - listOne := []string{"one"} - if listOne[0] == "two" { - return "not allowed" - } - - for i := range make([]int, 10) { - key := GetKey() - if val, ok := someMap[key]; ok { - fmt.Println("ok!") - } - - someOtherMap := GetMap() - if val, ok := someOtherMap[key]; ok { - fmt.Println("ok") - } - - someIndex := 3 - if val := someSlice[someIndex]; val != nil { - retunr - } - } -} - -func Defer() { - thingOne := getOne() - thingTwo := getTwo() - - defer thingOne.Close() - defer thingTwo.Close() - - thingOne := getOne() - defer thingOne.Close() - - thingTwo := getTwo() - defer thingTwo.Close() - - thingOne := getOne() - defer thingOne.Close() - thingTwo := getTwo() // want "assignments should only be cuddled with other assignments" - defer thingTwo.Close() // want "only one cuddle assignment allowed before defer statement" - - thingOne := getOne() - thingTwo := getTwo() - defer thingOne.Close() // want "only one cuddle assignment allowed before defer statement" - defer thingTwo.Close() - - m := sync.Mutex{} - - m.Lock() - defer m.Unlock() - - foo := true - defer func(b bool) { // want "defer statements should only be cuddled with expressions on same variable" - fmt.Printf("%v", b) - }() -} - -func For() { - bool := true - for { // want "for statement without condition should never be cuddled" - fmt.Println("should not be allowed") - - if bool { - break - } - } -} - -func Go() { - go func() { - panic("is this real life?") - }() - - fooFunc := func() {} - go fooFunc() - - barFunc := func() {} - go fooFunc() // want "go statements can only invoke functions assigned on line above" - - go func() { - fmt.Println("hey") - }() - - cuddled := true - go func() { // want "go statements can only invoke functions assigned on line above" - fmt.Println("hey") - }() - - argToGo := 1 - go takesArg(argToGo) - - notArgToGo := 1 - go takesArg(argToGo) // want "go statements can only invoke functions assigned on line above" - - t1 := NewT() - t2 := NewT() - t3 := NewT() - - go t1() - go t2() - go t3() - - multiCuddle1 := NewT() - multiCuddle2 := NewT() - go multiCuddle2() // want "only one cuddle assignment allowed before go" - - t4 := NewT() - t5 := NewT() - go t5() // want "only one cuddle assignment allowed before go" - go t4() -} - -func Switch() { - var b bool - switch b { - case true: - return "a" - case false: - return "b" - } - - t := time.Now() - - switch { - case t.Hour() < 12: - fmt.Println("It's before noon") - default: - fmt.Println("It's after noon") - } - - var b bool - switch anotherBool { // want "switch statements should only be cuddled with variables switched" - case true: - return "a" - case false: - return "b" - } - - t := time.Now() - switch { // want "anonymous switch statements should never be cuddled" - case t.Hour() < 12: - fmt.Println("It's before noon") - default: - fmt.Println("It's after noon") - } -} - -func TypeSwitch() { - x := GetSome() - switch v := x.(type) { - case int: - return "got int" - default: - return "got other" - } - - var id string - switch i := objectID.(type) { - case int: - id = strconv.Itoa(i) - case uint32: - id = strconv.Itoa(int(i)) - case string: - id = i - } - - var b bool - switch AnotherVal.(type) { // want "type switch statements should only be cuddled with variables switched" - case int: - return "a" - case string: - return "b" - } -} - -func Append() { - someList := []string{} - - bar := "baz" - someList = append(someList, bar) - - bar := "baz" - someList = append(someList, "notBar") // want "append only allowed to cuddle with appended value" - - bar := "baz" - someList = append(someList, fmt.Sprintf("%s", bar)) - - bar := "baz" - whatever := appendFn(bar) - - s := []string{} - - s = append(s, "one") - s = append(s, "two") - s = append(s, "three") - - p.defs = append(p.defs, x) - def.parseFrom(p) - p.defs = append(p.defs, def) - def.parseFrom(p) - def.parseFrom(p) - p.defs = append(p.defs, x) -} - -func ExpressionAssignment() { - foo := true - someFunc(foo) - - foo := true - someFunc(false) // want "only cuddled expressions if assigning variable or using from line above" -} - -func Channel() { - timeoutCh := time.After(timeout) - - for range make([]int, 10) { - select { - case <-timeoutCh: - return true - case <-time.After(10 * time.Millisecond): - return false - } - } - - select { - case <-time.After(1 * time.Second): - return "1s" - default: - return "are we there yet?" - } -} - -func Branch() { - for { - if true { - singleLine := true - break - } - - if true && false { - multiLine := true - maybeVar := "var" - continue // want "branch statements should not be cuddled if block has more than two lines" - } - - if false { - multiLine := true - maybeVar := "var" - - break - } - } -} - -func Locks() { - hashFileCache.Lock() - out, ok := hashFileCache.m[file] - hashFileCache.Unlock() - - mu := &sync.Mutex{} - mu.X(y).Z.RLock() - x, y := someMap[someKey] - mu.RUnlock() -} - -func SpliceAndSlice() { - start := 0 - if v := aSlice[start:3]; v { - fmt.Println("") - } - - someKey := "str" - if v, ok := someMap[obtain(someKey)+"str"]; ok { - fmt.Println("Hey there") - } - - end := 10 - if v := arr[3:notEnd]; !v { // want "if statements should only be cuddled with assignments used in the if statement itself" - // Error - } - - notKey := "str" - if v, ok := someMap[someKey]; ok { // want "if statements should only be cuddled with assignments used in the if statement itself" - // Error - } -} - -func Block() { - var foo string - - x := func() { - var err error - foo = "1" // want "assignments should only be cuddled with other assignments" - }() - - x := func() { - var err error - foo = "1" // want "assignments should only be cuddled with other assignments" - } - - func() { - var err error - foo = "1" // want "assignments should only be cuddled with other assignments" - }() - - func() { - var err error - foo = "1" // want "assignments should only be cuddled with other assignments" - } - - func() { - func() { - return func() { - var err error - foo = "1" // want "assignments should only be cuddled with other assignments" - } - }() - } - - var x error - foo, err := func() { return "", nil } // want "assignments should only be cuddled with other assignments" - - defer func() { - var err error - foo = "1" // want "assignments should only be cuddled with other assignments" - }() - - go func() { - var err error - foo = "1" // want "assignments should only be cuddled with other assignments" - }() -} - -func CommentInLast() { - switch nextStatement.(type) { - case *someType, *anotherType: - default: - // Foo - return - } -} - -func OnlyCheckTwoLinesAboveIfAssignment() { - if err != nil { - return - } - a, err := someFn() // want "assignments should only be cuddled with other assignments" - if err != nil { // want "only one cuddle assignment allowed before if statement" - return result, err - } - b := someFn() // want "assignments should only be cuddled with other assignments" - if err := fn(b); err != nil { // want "only one cuddle assignment allowed before if statement" - return - } - - if true { - return - } - c, err := someFn() // want "assignments should only be cuddled with other assignments" - if err != nil { // want "only one cuddle assignment allowed before if statement" - return result, err - } -} - -func IncrementDecrement() { - a := 1 - a++ - a-- - b := 2 - - if true { - b-- - } - b++ // want "assignments should only be cuddled with other assignments" - - go func() {}() - b++ // want "assignments should only be cuddled with other assignments" - go func() {}() // want "only one cuddle assignment allowed before go statement" -} - -// Issue #09 -func AnonymousFunc() { - fmt.Println(func() string { - _ = 1 - _ = 2 - _ = 3 - return "string" // want "return statements should not be cuddled if block has more than two lines" - }) - - fmt.Println(func() error { - foo := "foo" - fmt.Println("fmt.Println") // want "only cuddled expressions if assigning variable or using from line above" - if foo == "bar" { // want "if statements should only be cuddled with assignments" - return fmt.Errorf("bar") - } - return nil // want "return statements should not be cuddled if block has more than two lines" - }()) -} diff --git a/testdata/src/default_config/generic_handling.go.golden b/testdata/src/default_config/generic_handling.go.golden deleted file mode 100644 index 67bc61f..0000000 --- a/testdata/src/default_config/generic_handling.go.golden +++ /dev/null @@ -1,641 +0,0 @@ -package pkg - -import ( - "encoding/json" - "fmt" - "strconv" - "sync" - "time" -) - -func OnlyComments() { - // Also signel comments are ignored - // foo := false - // if true { - // - // foo = true - // - // } -} - -func NoBlock(r string) (x uintptr) - -func CommentOnFnLine() { // This is just a comment - fmt.Println("hello") -} - -func ReturnOnSameLine() error { - s := func() error { return nil } - _ = s -} - -func LabeledBlocks() { - goto end -end: -} - -func If() { - v, err := strconv.Atoi("1") - if err != nil { - fmt.Println(v) - } - - a := true - if a { - fmt.Println("I'm OK") - } - - a, b := true, false - if !a { - fmt.Println("I'm OK") - } - - a, b = true, false - - if err != nil { // want "if statements should only be cuddled with assignments used in the if statement itself" - fmt.Println("I'm not OK") - } - - if true { - fmt.Println("I'm OK") - } - - if false { // want "if statements should only be cuddled with assignments" - fmt.Println("I'm OK") - } - - c := true - fooBar := "string" - a, b := true, false - - if a && b || !c && len(fooBar) > 2 { // want "only one cuddle assignment allowed before if statement" - return false - } - - a, b, c := someFunc() // want "assignments should only be cuddled with other assignments" - - if z && len(y) > 0 || 3 == 4 { // want "only one cuddle assignment allowed before if statement" - return true - } - - a, b := true, false - if c && b || !c && len(fooBar) > 2 { - return false - } - - r := map[string]interface{}{} - if err := json.NewDecoder(someReader).Decode(&r); err != nil { - return "this should be OK" - } -} - -func Return() { - if true { - if false { - // - } - - return // want "return statements should not be cuddled if block has more than two lines" - } - - if false { - fmt.Println("this is less than two lines") - return - } - - return // want "return statements should not be cuddled if block has more than two lines" -} - -func AssignmentAndDeclaration() { - if true { - fmt.Println("I'm not OK") - } - - foo := true // want "assignments should only be cuddled with other assignments" - - bar := foo - baz := true - biz := false - - var a bool - - fmt.Println(a) // want "expressions should not be cuddled with declarations or returns" - - b := true - fmt.Println(b) -} - -func Range() { - anotherList := make([]string, 5) - - myList := make([]int, 10) - - for i := range anotherList { // want "ranges should only be cuddled with assignments used in the iteration" - fmt.Println(i) - } - - myList = make([]string, 5) - - anotherList = make([]int, 10) - for i := range anotherList { - fmt.Println(i) - } - - someList, anotherList := GetTwoListsOfStrings() - for i := range append(someList, anotherList...) { - fmt.Println(i) - } - - aThirdList := GetList() - for i := range append(someList, aThirdList...) { - fmt.Println(i) - } -} - -func FirstInBlock() { - idx := i - if i > 0 { - idx = i - 1 - } - - vals := map[int]struct{}{} - for i := range make([]int, 5) { - vals[i] = struct{}{} - } - - x := []int{} - - vals := map[int]struct{}{} - - for i := range make([]int, 5) { // want "ranges should only be cuddled with assignments used in the iteration" - x = append(x, i) - } -} - -func OnlyCuddleOneAssignment() { - foo := true - bar := false - - biz := true || false - if biz { - return true - } - - foo := true - bar := false - biz := true || false - - if biz { - return true - } - - foo := true - bar := false - - biz := true || false - if biz { // want "only one cuddle assignment allowed before if statement" - return false - } -} - -func IdentifiersWithIndices() { - runes := []rune{'+', '-'} - if runes[0] == '+' || runes[0] == '-' { - return string(runes[1:]) - } - - listTwo := []string{} - - listOne := []string{"one"} - if listOne[0] == "two" { - return "not allowed" - } - - for i := range make([]int, 10) { - key := GetKey() - if val, ok := someMap[key]; ok { - fmt.Println("ok!") - } - - someOtherMap := GetMap() - if val, ok := someOtherMap[key]; ok { - fmt.Println("ok") - } - - someIndex := 3 - if val := someSlice[someIndex]; val != nil { - retunr - } - } -} - -func Defer() { - thingOne := getOne() - thingTwo := getTwo() - - defer thingOne.Close() - defer thingTwo.Close() - - thingOne := getOne() - defer thingOne.Close() - - thingTwo := getTwo() - defer thingTwo.Close() - - thingOne := getOne() - defer thingOne.Close() - - thingTwo := getTwo() // want "assignments should only be cuddled with other assignments" - defer thingTwo.Close() // want "only one cuddle assignment allowed before defer statement" - - thingOne := getOne() - thingTwo := getTwo() - - defer thingOne.Close() // want "only one cuddle assignment allowed before defer statement" - defer thingTwo.Close() - - m := sync.Mutex{} - - m.Lock() - defer m.Unlock() - - foo := true - - defer func(b bool) { // want "defer statements should only be cuddled with expressions on same variable" - fmt.Printf("%v", b) - }() -} - -func For() { - bool := true - - for { // want "for statement without condition should never be cuddled" - fmt.Println("should not be allowed") - - if bool { - break - } - } -} - -func Go() { - go func() { - panic("is this real life?") - }() - - fooFunc := func() {} - go fooFunc() - - barFunc := func() {} - - go fooFunc() // want "go statements can only invoke functions assigned on line above" - - go func() { - fmt.Println("hey") - }() - - cuddled := true - - go func() { // want "go statements can only invoke functions assigned on line above" - fmt.Println("hey") - }() - - argToGo := 1 - go takesArg(argToGo) - - notArgToGo := 1 - - go takesArg(argToGo) // want "go statements can only invoke functions assigned on line above" - - t1 := NewT() - t2 := NewT() - t3 := NewT() - - go t1() - go t2() - go t3() - - multiCuddle1 := NewT() - - multiCuddle2 := NewT() - go multiCuddle2() // want "only one cuddle assignment allowed before go" - - t4 := NewT() - - t5 := NewT() - go t5() // want "only one cuddle assignment allowed before go" - go t4() -} - -func Switch() { - var b bool - switch b { - case true: - return "a" - case false: - return "b" - } - - t := time.Now() - - switch { - case t.Hour() < 12: - fmt.Println("It's before noon") - default: - fmt.Println("It's after noon") - } - - var b bool - - switch anotherBool { // want "switch statements should only be cuddled with variables switched" - case true: - return "a" - case false: - return "b" - } - - t := time.Now() - - switch { // want "anonymous switch statements should never be cuddled" - case t.Hour() < 12: - fmt.Println("It's before noon") - default: - fmt.Println("It's after noon") - } -} - -func TypeSwitch() { - x := GetSome() - switch v := x.(type) { - case int: - return "got int" - default: - return "got other" - } - - var id string - switch i := objectID.(type) { - case int: - id = strconv.Itoa(i) - case uint32: - id = strconv.Itoa(int(i)) - case string: - id = i - } - - var b bool - - switch AnotherVal.(type) { // want "type switch statements should only be cuddled with variables switched" - case int: - return "a" - case string: - return "b" - } -} - -func Append() { - someList := []string{} - - bar := "baz" - someList = append(someList, bar) - - bar := "baz" - - someList = append(someList, "notBar") // want "append only allowed to cuddle with appended value" - - bar := "baz" - someList = append(someList, fmt.Sprintf("%s", bar)) - - bar := "baz" - whatever := appendFn(bar) - - s := []string{} - - s = append(s, "one") - s = append(s, "two") - s = append(s, "three") - - p.defs = append(p.defs, x) - def.parseFrom(p) - p.defs = append(p.defs, def) - def.parseFrom(p) - def.parseFrom(p) - p.defs = append(p.defs, x) -} - -func ExpressionAssignment() { - foo := true - someFunc(foo) - - foo := true - - someFunc(false) // want "only cuddled expressions if assigning variable or using from line above" -} - -func Channel() { - timeoutCh := time.After(timeout) - - for range make([]int, 10) { - select { - case <-timeoutCh: - return true - case <-time.After(10 * time.Millisecond): - return false - } - } - - select { - case <-time.After(1 * time.Second): - return "1s" - default: - return "are we there yet?" - } -} - -func Branch() { - for { - if true { - singleLine := true - break - } - - if true && false { - multiLine := true - maybeVar := "var" - - continue // want "branch statements should not be cuddled if block has more than two lines" - } - - if false { - multiLine := true - maybeVar := "var" - - break - } - } -} - -func Locks() { - hashFileCache.Lock() - out, ok := hashFileCache.m[file] - hashFileCache.Unlock() - - mu := &sync.Mutex{} - mu.X(y).Z.RLock() - x, y := someMap[someKey] - mu.RUnlock() -} - -func SpliceAndSlice() { - start := 0 - if v := aSlice[start:3]; v { - fmt.Println("") - } - - someKey := "str" - if v, ok := someMap[obtain(someKey)+"str"]; ok { - fmt.Println("Hey there") - } - - end := 10 - - if v := arr[3:notEnd]; !v { // want "if statements should only be cuddled with assignments used in the if statement itself" - // Error - } - - notKey := "str" - - if v, ok := someMap[someKey]; ok { // want "if statements should only be cuddled with assignments used in the if statement itself" - // Error - } -} - -func Block() { - var foo string - - x := func() { - var err error - - foo = "1" // want "assignments should only be cuddled with other assignments" - }() - - x := func() { - var err error - - foo = "1" // want "assignments should only be cuddled with other assignments" - } - - func() { - var err error - - foo = "1" // want "assignments should only be cuddled with other assignments" - }() - - func() { - var err error - - foo = "1" // want "assignments should only be cuddled with other assignments" - } - - func() { - func() { - return func() { - var err error - - foo = "1" // want "assignments should only be cuddled with other assignments" - } - }() - } - - var x error - - foo, err := func() { return "", nil } // want "assignments should only be cuddled with other assignments" - - defer func() { - var err error - - foo = "1" // want "assignments should only be cuddled with other assignments" - }() - - go func() { - var err error - - foo = "1" // want "assignments should only be cuddled with other assignments" - }() -} - -func CommentInLast() { - switch nextStatement.(type) { - case *someType, *anotherType: - default: - // Foo - return - } -} - -func OnlyCheckTwoLinesAboveIfAssignment() { - if err != nil { - return - } - - a, err := someFn() // want "assignments should only be cuddled with other assignments" - if err != nil { // want "only one cuddle assignment allowed before if statement" - return result, err - } - - b := someFn() // want "assignments should only be cuddled with other assignments" - if err := fn(b); err != nil { // want "only one cuddle assignment allowed before if statement" - return - } - - if true { - return - } - - c, err := someFn() // want "assignments should only be cuddled with other assignments" - if err != nil { // want "only one cuddle assignment allowed before if statement" - return result, err - } -} - -func IncrementDecrement() { - a := 1 - a++ - a-- - b := 2 - - if true { - b-- - } - - b++ // want "assignments should only be cuddled with other assignments" - - go func() {}() - - b++ // want "assignments should only be cuddled with other assignments" - - go func() {}() // want "only one cuddle assignment allowed before go statement" -} - -// Issue #09 -func AnonymousFunc() { - fmt.Println(func() string { - _ = 1 - _ = 2 - _ = 3 - - return "string" // want "return statements should not be cuddled if block has more than two lines" - }) - - fmt.Println(func() error { - foo := "foo" - - fmt.Println("fmt.Println") // want "only cuddled expressions if assigning variable or using from line above" - - if foo == "bar" { // want "if statements should only be cuddled with assignments" - return fmt.Errorf("bar") - } - - return nil // want "return statements should not be cuddled if block has more than two lines" - }()) -} diff --git a/testdata/src/default_config/go/go.go b/testdata/src/default_config/go/go.go new file mode 100644 index 0000000..ef58324 --- /dev/null +++ b/testdata/src/default_config/go/go.go @@ -0,0 +1,63 @@ +package testpkg + +import "fmt" + +func NewT() func() { + return func() {} +} + +func Fn(_ int) {} + +func Go() { + fooFunc := func() {} + go fooFunc() + + barFunc := func() {} + go fooFunc() // want `missing whitespace above this line \(no shared variables above go\)` + + _ = barFunc + + go func() { + fmt.Println("hey") + }() + + cuddled := true + go func() { // want `missing whitespace above this line \(no shared variables above go\)` + fmt.Println("hey") + }() + + _ = cuddled + + argToGo := 1 + go Fn(argToGo) + + notArgToGo := 1 + go Fn(argToGo) // want `missing whitespace above this line \(no shared variables above go\)` + + _ = notArgToGo + + t1 := NewT() + t2 := NewT() + t3 := NewT() + + go t1() + go t2() + go t3() + + multiCuddle1 := NewT() + multiCuddle2 := NewT() // want `missing whitespace above this line \(too many statements above go\)` + go multiCuddle2() + + t4 := NewT() + t5 := NewT() // want `missing whitespace above this line \(too many statements above go\)` + go t5() + go t4() + + _ = t1 + _ = t2 + _ = t3 + _ = t4 + _ = t5 + _ = multiCuddle1 + _ = multiCuddle2 +} diff --git a/testdata/src/default_config/go/go.go.golden b/testdata/src/default_config/go/go.go.golden new file mode 100644 index 0000000..6b88425 --- /dev/null +++ b/testdata/src/default_config/go/go.go.golden @@ -0,0 +1,69 @@ + +package testpkg + +import "fmt" + +func NewT() func() { + return func() {} +} + +func Fn(_ int) {} + +func Go() { + fooFunc := func() {} + go fooFunc() + + barFunc := func() {} + + go fooFunc() // want `missing whitespace above this line \(no shared variables above go\)` + + _ = barFunc + + go func() { + fmt.Println("hey") + }() + + cuddled := true + + go func() { // want `missing whitespace above this line \(no shared variables above go\)` + fmt.Println("hey") + }() + + _ = cuddled + + argToGo := 1 + go Fn(argToGo) + + notArgToGo := 1 + + go Fn(argToGo) // want `missing whitespace above this line \(no shared variables above go\)` + + _ = notArgToGo + + t1 := NewT() + t2 := NewT() + t3 := NewT() + + go t1() + go t2() + go t3() + + multiCuddle1 := NewT() + + multiCuddle2 := NewT() // want `missing whitespace above this line \(too many statements above go\)` + go multiCuddle2() + + t4 := NewT() + + t5 := NewT() // want `missing whitespace above this line \(too many statements above go\)` + go t5() + go t4() + + _ = t1 + _ = t2 + _ = t3 + _ = t4 + _ = t5 + _ = multiCuddle1 + _ = multiCuddle2 +} diff --git a/testdata/src/default_config/if/if.go b/testdata/src/default_config/if/if.go new file mode 100644 index 0000000..1d797bc --- /dev/null +++ b/testdata/src/default_config/if/if.go @@ -0,0 +1,67 @@ +package testpkg + +import "errors" + +func fn1() { + a := 1 + b := 2 // want `missing whitespace above this line \(too many statements above if\)` + if b == 2 { + panic(1) + } + + _ = a + _ = b +} + +func fn2() { + b := 2 + a := 1 + if b == 2 { // want `missing whitespace above this line \(no shared variables above if\)` + panic(1) + } + + _ = a + _ = b +} + +func fn3() { + err := errors.New("error") + + if err != nil { + panic(err) + } +} + +func fn4() { + if a := 1; a != 2 { + panic(a) + } + if a := 2; a != 2 { // want `missing whitespace above this line \(invalid statement above if\)` + panic(a) + } +} + +func fn5(m any, k string) string { + v := m.(map[string]string) + if r, ok := m[k]; ok { + return r + } + + return k +} + +func fn6() { + if true { + _ = 1 + _ = 2 + return // want `missing whitespace above this line \(too many lines above return\)` + } else if false { + _ = 1 + _ = 2 + return // want `missing whitespace above this line \(too many lines above return\)` + } else { + _ = 1 + _ = 2 + return // want `missing whitespace above this line \(too many lines above return\)` + } +} diff --git a/testdata/src/default_config/if/if.go.golden b/testdata/src/default_config/if/if.go.golden new file mode 100644 index 0000000..f61ba5b --- /dev/null +++ b/testdata/src/default_config/if/if.go.golden @@ -0,0 +1,73 @@ +package testpkg + +import "errors" + +func fn1() { + a := 1 + + b := 2 // want `missing whitespace above this line \(too many statements above if\)` + if b == 2 { + panic(1) + } + + _ = a + _ = b +} + +func fn2() { + b := 2 + a := 1 + + if b == 2 { // want `missing whitespace above this line \(no shared variables above if\)` + panic(1) + } + + _ = a + _ = b +} + +func fn3() { + err := errors.New("error") + + if err != nil { + panic(err) + } +} + +func fn4() { + if a := 1; a != 2 { + panic(a) + } + + if a := 2; a != 2 { // want `missing whitespace above this line \(invalid statement above if\)` + panic(a) + } +} + +func fn5(m any, k string) string { + v := m.(map[string]string) + if r, ok := m[k]; ok { + return r + } + + return k +} + +func fn6() { + if true { + _ = 1 + _ = 2 + + return // want `missing whitespace above this line \(too many lines above return\)` + } else if false { + _ = 1 + _ = 2 + + return // want `missing whitespace above this line \(too many lines above return\)` + } else { + _ = 1 + _ = 2 + + return // want `missing whitespace above this line \(too many lines above return\)` + } +} diff --git a/testdata/src/default_config/label/label.go b/testdata/src/default_config/label/label.go new file mode 100644 index 0000000..9381472 --- /dev/null +++ b/testdata/src/default_config/label/label.go @@ -0,0 +1,50 @@ +package testpkg + +import "fmt" + +func fn1() { + for i := range make([]int, 2) { + if i == 1 { + goto END + } + + END: + _ = 1 + } +} + +func fn2() { + for i := range make([]int, 2) { + if i == 1 { + goto END + } + + END: + } +} + +func fn3() { +L1: + if true { + _ = 1 + } +L2: // want `missing whitespace above this line \(never cuddle label\)` + if true { + _ = 1 + } + + _ = 1 +L3: // want `missing whitespace above this line \(never cuddle label\)` + _ = 1 +} + +func fn() { +LABEL: + if true { + fmt.Println("") + fmt.Println("") + fmt.Println("") + fmt.Println("") + break // want `missing whitespace above this line \(too many lines above branch\)` + } +} diff --git a/testdata/src/default_config/label/label.go.golden b/testdata/src/default_config/label/label.go.golden new file mode 100644 index 0000000..9ef54e6 --- /dev/null +++ b/testdata/src/default_config/label/label.go.golden @@ -0,0 +1,53 @@ +package testpkg + +import "fmt" + +func fn1() { + for i := range make([]int, 2) { + if i == 1 { + goto END + } + + END: + _ = 1 + } +} + +func fn2() { + for i := range make([]int, 2) { + if i == 1 { + goto END + } + + END: + } +} + +func fn3() { +L1: + if true { + _ = 1 + } + +L2: // want `missing whitespace above this line \(never cuddle label\)` + if true { + _ = 1 + } + + _ = 1 + +L3: // want `missing whitespace above this line \(never cuddle label\)` + _ = 1 +} + +func fn() { +LABEL: + if true { + fmt.Println("") + fmt.Println("") + fmt.Println("") + fmt.Println("") + + break // want `missing whitespace above this line \(too many lines above branch\)` + } +} diff --git a/testdata/src/default_config/multiline_case.go b/testdata/src/default_config/multiline_case.go deleted file mode 100644 index af193c7..0000000 --- a/testdata/src/default_config/multiline_case.go +++ /dev/null @@ -1,32 +0,0 @@ -package pkg - -func Fn() { - switch { - case true, false: - fmt.Println("ok") - case true || false: - fmt.Println("ok") - case true, false: - fmt.Println("ok") - case true || false: - fmt.Println("ok") - case true, - false: // want "block should not start with a whitespace" - - fmt.Println("starting whitespace multiline case") - case true || - false: // want "block should not start with a whitespace" - - fmt.Println("starting whitespace multiline case") - case true, - false: - fmt.Println("ending whitespace multiline case") - - case true || - false: - fmt.Println("ending whitespace multiline case") - - case true, false: - fmt.Println("all") - } -} diff --git a/testdata/src/default_config/multiline_case.go.golden b/testdata/src/default_config/multiline_case.go.golden deleted file mode 100644 index 9837d6e..0000000 --- a/testdata/src/default_config/multiline_case.go.golden +++ /dev/null @@ -1,30 +0,0 @@ -package pkg - -func Fn() { - switch { - case true, false: - fmt.Println("ok") - case true || false: - fmt.Println("ok") - case true, false: - fmt.Println("ok") - case true || false: - fmt.Println("ok") - case true, - false: // want "block should not start with a whitespace" - fmt.Println("starting whitespace multiline case") - case true || - false: // want "block should not start with a whitespace" - fmt.Println("starting whitespace multiline case") - case true, - false: - fmt.Println("ending whitespace multiline case") - - case true || - false: - fmt.Println("ending whitespace multiline case") - - case true, false: - fmt.Println("all") - } -} diff --git a/testdata/src/default_config/range/range.go b/testdata/src/default_config/range/range.go new file mode 100644 index 0000000..0bee03d --- /dev/null +++ b/testdata/src/default_config/range/range.go @@ -0,0 +1,32 @@ +package testpkg + +func fn1() { + a := []int{} + b := []int{} // want `missing whitespace above this line \(too many statements above range\)` + for range b { + panic(1) + } + + _ = a + _ = b +} + +func fn2() { + b := []int{} + a := []int{} + for range b { // want `missing whitespace above this line \(no shared variables above range\)` + panic(1) + } + + _ = a + _ = b +} + +func fn3() { + for range make([]int, 0) { + panic("") + } + for range make([]int, 0) { // want `missing whitespace above this line \(invalid statement above range\)` + panic("") + } +} diff --git a/testdata/src/default_config/range/range.go.golden b/testdata/src/default_config/range/range.go.golden new file mode 100644 index 0000000..316d4ca --- /dev/null +++ b/testdata/src/default_config/range/range.go.golden @@ -0,0 +1,35 @@ +package testpkg + +func fn1() { + a := []int{} + + b := []int{} // want `missing whitespace above this line \(too many statements above range\)` + for range b { + panic(1) + } + + _ = a + _ = b +} + +func fn2() { + b := []int{} + a := []int{} + + for range b { // want `missing whitespace above this line \(no shared variables above range\)` + panic(1) + } + + _ = a + _ = b +} + +func fn3() { + for range make([]int, 0) { + panic("") + } + + for range make([]int, 0) { // want `missing whitespace above this line \(invalid statement above range\)` + panic("") + } +} diff --git a/testdata/src/default_config/remove_whitespace.go b/testdata/src/default_config/remove_whitespace.go deleted file mode 100644 index faf4eca..0000000 --- a/testdata/src/default_config/remove_whitespace.go +++ /dev/null @@ -1,83 +0,0 @@ -package testpkg - -import "fmt" - -func RemoveWhitespaceNoComments() { // want "block should not start with a whitespace" - - a := 1 - if a < 2 { // want "block should not start with a whitespace" - - a = 2 - - } // want "block should not end with a whitespace" - - switch { // want "block should not start with a whitespace" - - case true: - fmt.Println("true") - - default: - fmt.Println("false") - - } // want "block should not end with a whitespace" - - _ = a - -} // want "block should not end with a whitespace" - -// RemoveWhiteSpaceWithhComments keeps comments even when removing newlines. -func RemoveWhiteSpaceWithhComments() { // want "block should not start with a whitespace" - // This comment should be kept - - a := 1 - if a < 2 { // want "block should not start with a whitespace" - - // This comment should be kept - a = 2 - // This comment should be kept - - } // want "block should not end with a whitespace" - - if a > 2 { // want "block should not start with a whitespace" - // This comment should be kept - - a = 2 - - // This comment should be kept - } // want "block should not end with a whitespace" - - if a > 3 { // want "block should not start with a whitespace" - - // This comment should be kept - - a = 2 - - // This comment should be kept - - } // want "block should not end with a whitespace" - - _ = a - - // This comment should be kept as well -} // want "block should not end with a whitespace" - -func MultipleCommentsAreSorted() { - switch 1 { - case 1: // want "block should not start with a whitespace" - // Comment - - // Comment - fmt.Println("1") - case 2: // want "block should not start with a whitespace" - // Comment - - // Comment - fmt.Println("2") - } -} - -func ExampleT() { - fmt.Println("output") - - // Output: output -} diff --git a/testdata/src/default_config/remove_whitespace.go.golden b/testdata/src/default_config/remove_whitespace.go.golden deleted file mode 100644 index f1c0994..0000000 --- a/testdata/src/default_config/remove_whitespace.go.golden +++ /dev/null @@ -1,65 +0,0 @@ -package testpkg - -import "fmt" - -func RemoveWhitespaceNoComments() { // want "block should not start with a whitespace" - a := 1 - if a < 2 { // want "block should not start with a whitespace" - a = 2 - } // want "block should not end with a whitespace" - - switch { // want "block should not start with a whitespace" - case true: - fmt.Println("true") - - default: - fmt.Println("false") - } // want "block should not end with a whitespace" - - _ = a -} // want "block should not end with a whitespace" - -// RemoveWhiteSpaceWithhComments keeps comments even when removing newlines. -func RemoveWhiteSpaceWithhComments() { // want "block should not start with a whitespace" - // This comment should be kept - a := 1 - if a < 2 { // want "block should not start with a whitespace" - // This comment should be kept - a = 2 - // This comment should be kept - } // want "block should not end with a whitespace" - - if a > 2 { // want "block should not start with a whitespace" - // This comment should be kept - a = 2 - // This comment should be kept - } // want "block should not end with a whitespace" - - if a > 3 { // want "block should not start with a whitespace" - // This comment should be kept - a = 2 - // This comment should be kept - } // want "block should not end with a whitespace" - - _ = a - // This comment should be kept as well -} // want "block should not end with a whitespace" - -func MultipleCommentsAreSorted() { - switch 1 { - case 1: // want "block should not start with a whitespace" - // Comment - // Comment - fmt.Println("1") - case 2: // want "block should not start with a whitespace" - // Comment - // Comment - fmt.Println("2") - } -} - -func ExampleT() { - fmt.Println("output") - - // Output: output -} diff --git a/testdata/src/default_config/return/return.go b/testdata/src/default_config/return/return.go new file mode 100644 index 0000000..5e9a7c7 --- /dev/null +++ b/testdata/src/default_config/return/return.go @@ -0,0 +1,40 @@ +package testpkg + +func fn1() int { + return 1 +} + +func fn2() int { + _ = 1 + return 1 +} + +func fn3() int { + _ = 1 + _ = 2 + return 1 // want `missing whitespace above this line \(too many lines above return\)` +} + +func fn4() int { + if true { + _ = 1 + } + return 1 // want `missing whitespace above this line \(too many lines above return\)` +} + +func fn5() int { + if true { + _ = 1 + _ = 2 + return 1 // want `missing whitespace above this line \(too many lines above return\)` + } + return 1 // want `missing whitespace above this line \(too many lines above return\)` +} + +func fn6() func() { + _ = 1 + _ = 2 + return func() { // want `missing whitespace above this line \(too many lines above return\)` + _ = 1 + } +} diff --git a/testdata/src/default_config/return/return.go.golden b/testdata/src/default_config/return/return.go.golden new file mode 100644 index 0000000..1a925d2 --- /dev/null +++ b/testdata/src/default_config/return/return.go.golden @@ -0,0 +1,46 @@ +package testpkg + +func fn1() int { + return 1 +} + +func fn2() int { + _ = 1 + return 1 +} + +func fn3() int { + _ = 1 + _ = 2 + + return 1 // want `missing whitespace above this line \(too many lines above return\)` +} + +func fn4() int { + if true { + _ = 1 + } + + return 1 // want `missing whitespace above this line \(too many lines above return\)` +} + +func fn5() int { + if true { + _ = 1 + _ = 2 + + return 1 // want `missing whitespace above this line \(too many lines above return\)` + } + + return 1 // want `missing whitespace above this line \(too many lines above return\)` +} + +func fn6() func() { + _ = 1 + _ = 2 + + return func() { // want `missing whitespace above this line \(too many lines above return\)` + _ = 1 + } +} + diff --git a/testdata/src/default_config/select/select.go b/testdata/src/default_config/select/select.go new file mode 100644 index 0000000..c8d8844 --- /dev/null +++ b/testdata/src/default_config/select/select.go @@ -0,0 +1,28 @@ +package testpkg + +import "context" + +func fn1(ctx context.Context, ch1 chan struct{}) { + select { + case ctx.Done(): + _ = 1 + case <-ch1: + _ = 1 + default: + _ = 1 + } +} + +func fn1(ctx context.Context, ch1 chan struct{}) { + x := 1 + select { // want `missing whitespace above this line \(no shared variables above select\)` + case ctx.Done(): + _ = 1 + case <-ch1: + _ = 1 + default: + _ = 1 + } + + _ = x +} diff --git a/testdata/src/default_config/select/select.go.golden b/testdata/src/default_config/select/select.go.golden new file mode 100644 index 0000000..6090756 --- /dev/null +++ b/testdata/src/default_config/select/select.go.golden @@ -0,0 +1,29 @@ +package testpkg + +import "context" + +func fn1(ctx context.Context, ch1 chan struct{}) { + select { + case ctx.Done(): + _ = 1 + case <-ch1: + _ = 1 + default: + _ = 1 + } +} + +func fn1(ctx context.Context, ch1 chan struct{}) { + x := 1 + + select { // want `missing whitespace above this line \(no shared variables above select\)` + case ctx.Done(): + _ = 1 + case <-ch1: + _ = 1 + default: + _ = 1 + } + + _ = x +} diff --git a/testdata/src/default_config/send/send.go b/testdata/src/default_config/send/send.go new file mode 100644 index 0000000..123d1b3 --- /dev/null +++ b/testdata/src/default_config/send/send.go @@ -0,0 +1,22 @@ +package testpkg + +func fn1(ch chan int) { + a := 1 + ch <- a + + b := 1 + ch <- someFn(b) + + c := 1 + ch <- 1 // want `missing whitespace above this line \(no shared variables above send\)` + + d := 2 + ch <- func() int { // want `missing whitespace above this line \(no shared variables above send\)` + return c + }() + + e := 2 + ch <- func() int { + return e + }() +} diff --git a/testdata/src/default_config/send/send.go.golden b/testdata/src/default_config/send/send.go.golden new file mode 100644 index 0000000..c538093 --- /dev/null +++ b/testdata/src/default_config/send/send.go.golden @@ -0,0 +1,24 @@ +package testpkg + +func fn1(ch chan int) { + a := 1 + ch <- a + + b := 1 + ch <- someFn(b) + + c := 1 + + ch <- 1 // want `missing whitespace above this line \(no shared variables above send\)` + + d := 2 + + ch <- func() int { // want `missing whitespace above this line \(no shared variables above send\)` + return c + }() + + e := 2 + ch <- func() int { + return e + }() +} diff --git a/testdata/src/default_config/switch/switch.go b/testdata/src/default_config/switch/switch.go new file mode 100644 index 0000000..3ae5dc3 --- /dev/null +++ b/testdata/src/default_config/switch/switch.go @@ -0,0 +1,64 @@ +package testpkg + +func fn0() { + a := 1 + switch a { + case 1: + case 2: + panic(a) + } +} + +func fn1() { + a := 1 + b := 2 // want `missing whitespace above this line \(too many statements above switch\)` + switch b { + case 1: + case 2: + case 3: + panic(a) + } +} + +func fn2() { + b := 2 + a := 1 + switch b { // want `missing whitespace above this line \(no shared variables above switch\)` + case 1: + case 2: + case 3: + panic(a) + } +} + +func fn3() { + switch true { + case true: + case false: + } + switch true { // want `missing whitespace above this line \(invalid statement above switch\)` + case true: + case false: + panic("") + } +} + +func fn4() { + timeout := 10 + switch timeout { + case 5: + timeout = 100 + case 10: + timeout = 0 + } +} + +func fn5() { + timeout := 10 + switch { // want `missing whitespace above this line \(no shared variables above switch\)` + case timeout > 5: + timeout = 100 + case timeout == 10: + timeout = 0 + } +} diff --git a/testdata/src/default_config/switch/switch.go.golden b/testdata/src/default_config/switch/switch.go.golden new file mode 100644 index 0000000..e84d6d8 --- /dev/null +++ b/testdata/src/default_config/switch/switch.go.golden @@ -0,0 +1,68 @@ +package testpkg + +func fn0() { + a := 1 + switch a { + case 1: + case 2: + panic(a) + } +} + +func fn1() { + a := 1 + + b := 2 // want `missing whitespace above this line \(too many statements above switch\)` + switch b { + case 1: + case 2: + case 3: + panic(a) + } +} + +func fn2() { + b := 2 + a := 1 + + switch b { // want `missing whitespace above this line \(no shared variables above switch\)` + case 1: + case 2: + case 3: + panic(a) + } +} + +func fn3() { + switch true { + case true: + case false: + } + + switch true { // want `missing whitespace above this line \(invalid statement above switch\)` + case true: + case false: + panic("") + } +} + +func fn4() { + timeout := 10 + switch timeout { + case 5: + timeout = 100 + case 10: + timeout = 0 + } +} + +func fn5() { + timeout := 10 + + switch { // want `missing whitespace above this line \(no shared variables above switch\)` + case timeout > 5: + timeout = 100 + case timeout == 10: + timeout = 0 + } +} diff --git a/testdata/src/default_config/type_switch/type_switch.go b/testdata/src/default_config/type_switch/type_switch.go new file mode 100644 index 0000000..8e832b0 --- /dev/null +++ b/testdata/src/default_config/type_switch/type_switch.go @@ -0,0 +1,51 @@ +package testpkg + +func Fn() any { + return nil +} + +func fn1() { + var v any + + v = 1 + switch v.(type) { + case int: + case string: + } +} + +func fn2() { + var a any = 1 + + b := 1 + switch a.(type) { // want `missing whitespace above this line \(no shared variables above type-switch\)` + case int: + case string: + } + + _ = b +} + +func fn3(in any) { + if true { + return + } + + switch a := in.(type) { + case string: + return + default: + _ = a + } +} + +func fn4(in any) { + a := Fn() + b := Fn() // want `missing whitespace above this line \(too many statements above type-switch\)` + switch b.(type) { + case string: + return + default: + _ = a + } +} diff --git a/testdata/src/default_config/type_switch/type_switch.go.golden b/testdata/src/default_config/type_switch/type_switch.go.golden new file mode 100644 index 0000000..3a4b54c --- /dev/null +++ b/testdata/src/default_config/type_switch/type_switch.go.golden @@ -0,0 +1,54 @@ +package testpkg + +func Fn() any { + return nil +} + +func fn1() { + var v any + + v = 1 + switch v.(type) { + case int: + case string: + } +} + +func fn2() { + var a any = 1 + + b := 1 + + switch a.(type) { // want `missing whitespace above this line \(no shared variables above type-switch\)` + case int: + case string: + } + + _ = b +} + +func fn3(in any) { + if true { + return + } + + switch a := in.(type) { + case string: + return + default: + _ = a + } +} + +func fn4(in any) { + a := Fn() + + b := Fn() // want `missing whitespace above this line \(too many statements above type-switch\)` + switch b.(type) { + case string: + return + default: + _ = a + } +} + diff --git a/testdata/src/default_config/whitespace/leading_whitespace.go b/testdata/src/default_config/whitespace/leading_whitespace.go new file mode 100644 index 0000000..7385b14 --- /dev/null +++ b/testdata/src/default_config/whitespace/leading_whitespace.go @@ -0,0 +1,157 @@ +package testpkg + +import "fmt" + +func Call(fn func()) func() { + return fn +} + +func fn0() { fmt.Println("Hello, World") } + +func fn1() { // want +1 `unnecessary whitespace \(leading-whitespace\)` + + fmt.Println("Hello, World") +} + +func fn2() { // want +1 `unnecessary whitespace \(leading-whitespace\)` + + // Space before comment. + fmt.Println("Hello, World") +} + +func fn3() { // want +2 `unnecessary whitespace \(leading-whitespace\)` + // Space after comment + + fmt.Println("Hello, World") +} + +func fn4() { + fmt.Println("Hello, World") +} + +func fn5() { + // Comment without space before or after. + fmt.Println("Hello, World") +} + +func fn51() { + // Comment with + + // a newline between + _ = 1 +} + +func fn6() { + if true { // want +1 `unnecessary whitespace \(leading-whitespace\)` + + _ = 1 + } +} + +func fn7() { + if true { + _ = 1 + } else if true { // want +1 `unnecessary whitespace \(leading-whitespace\)` + + _ = 1 + } else { // want +1 `unnecessary whitespace \(leading-whitespace\)` + + _ = 1 + } +} + +func fn8(a string, b any, s []string) { // want +1 `unnecessary whitespace \(leading-whitespace\)` + + if true { // want +1 `unnecessary whitespace \(leading-whitespace\)` + + _ = 1 + } else if true { // want +1 `unnecessary whitespace \(leading-whitespace\)` + + _ = 1 + } + + for i := 0; i < 1; i++ { // want +1 `unnecessary whitespace \(leading-whitespace\)` + + _ = 1 + } + + for n := range []int{} { // want +1 `unnecessary whitespace \(leading-whitespace\)` + + _ = n + } + + for range s { // want +1 `unnecessary whitespace \(leading-whitespace\)` + + _ = 1 + } + + switch a { // want +1 `unnecessary whitespace \(leading-whitespace\)` + + case "a": + } + + switch b.(type) { // want +1 `unnecessary whitespace \(leading-whitespace\)` + + case int: + } + + f := func() { // want +1 `unnecessary whitespace \(leading-whitespace\)` + + _ = 1 + } + + f2 := Call(func() { // want +1 `unnecessary whitespace \(leading-whitespace\)` + + _ = 1 + }) + + _ = f + _ = f2 +} + +func fn9() { + switch { + case 1: // want +1 `unnecessary whitespace \(leading-whitespace\)` + + _ = 1 + + case 2: // want +2 `unnecessary whitespace \(leading-whitespace\)` + // This is a comment + + _ = 2 + + default: // want +1 `unnecessary whitespace \(leading-whitespace\)` + + _ = 3 + + } +} + +func fn10() { + switch { + case true || false: + fmt.Println("ok") + case true, false: + fmt.Println("ok") + case true || false: + fmt.Println("ok") + case true, + false: // want +1 `unnecessary whitespace \(leading-whitespace\)` + + fmt.Println("nok") + case true || + false: // want +1 `unnecessary whitespace \(leading-whitespace\)` + + fmt.Println("nok") + case true, + false: + fmt.Println("ok") + + case true || + false: + fmt.Println("ok") + + case true, false: + fmt.Println("all") + } +} diff --git a/testdata/src/default_config/whitespace/leading_whitespace.go.golden b/testdata/src/default_config/whitespace/leading_whitespace.go.golden new file mode 100644 index 0000000..91a81d4 --- /dev/null +++ b/testdata/src/default_config/whitespace/leading_whitespace.go.golden @@ -0,0 +1,136 @@ +package testpkg + +import "fmt" + +func Call(fn func()) func() { + return fn +} + +func fn0() { fmt.Println("Hello, World") } + +func fn1() { // want +1 `unnecessary whitespace \(leading-whitespace\)` + fmt.Println("Hello, World") +} + +func fn2() { // want +1 `unnecessary whitespace \(leading-whitespace\)` + // Space before comment. + fmt.Println("Hello, World") +} + +func fn3() { // want +2 `unnecessary whitespace \(leading-whitespace\)` + // Space after comment + fmt.Println("Hello, World") +} + +func fn4() { + fmt.Println("Hello, World") +} + +func fn5() { + // Comment without space before or after. + fmt.Println("Hello, World") +} + +func fn51() { + // Comment with + + // a newline between + _ = 1 +} + +func fn6() { + if true { // want +1 `unnecessary whitespace \(leading-whitespace\)` + _ = 1 + } +} + +func fn7() { + if true { + _ = 1 + } else if true { // want +1 `unnecessary whitespace \(leading-whitespace\)` + _ = 1 + } else { // want +1 `unnecessary whitespace \(leading-whitespace\)` + _ = 1 + } +} + +func fn8(a string, b any, s []string) { // want +1 `unnecessary whitespace \(leading-whitespace\)` + if true { // want +1 `unnecessary whitespace \(leading-whitespace\)` + _ = 1 + } else if true { // want +1 `unnecessary whitespace \(leading-whitespace\)` + _ = 1 + } + + for i := 0; i < 1; i++ { // want +1 `unnecessary whitespace \(leading-whitespace\)` + _ = 1 + } + + for n := range []int{} { // want +1 `unnecessary whitespace \(leading-whitespace\)` + _ = n + } + + for range s { // want +1 `unnecessary whitespace \(leading-whitespace\)` + _ = 1 + } + + switch a { // want +1 `unnecessary whitespace \(leading-whitespace\)` + case "a": + } + + switch b.(type) { // want +1 `unnecessary whitespace \(leading-whitespace\)` + case int: + } + + f := func() { // want +1 `unnecessary whitespace \(leading-whitespace\)` + _ = 1 + } + + f2 := Call(func() { // want +1 `unnecessary whitespace \(leading-whitespace\)` + _ = 1 + }) + + _ = f + _ = f2 +} + +func fn9() { + switch { + case 1: // want +1 `unnecessary whitespace \(leading-whitespace\)` + _ = 1 + + case 2: // want +2 `unnecessary whitespace \(leading-whitespace\)` + // This is a comment + _ = 2 + + default: // want +1 `unnecessary whitespace \(leading-whitespace\)` + _ = 3 + + } +} + +func fn10() { + switch { + case true || false: + fmt.Println("ok") + case true, false: + fmt.Println("ok") + case true || false: + fmt.Println("ok") + case true, + false: // want +1 `unnecessary whitespace \(leading-whitespace\)` + fmt.Println("nok") + case true || + false: // want +1 `unnecessary whitespace \(leading-whitespace\)` + fmt.Println("nok") + case true, + false: + fmt.Println("ok") + + case true || + false: + fmt.Println("ok") + + case true, false: + fmt.Println("all") + } +} diff --git a/testdata/src/default_config/whitespace/trailing_whitespace.go b/testdata/src/default_config/whitespace/trailing_whitespace.go new file mode 100644 index 0000000..03560da --- /dev/null +++ b/testdata/src/default_config/whitespace/trailing_whitespace.go @@ -0,0 +1,98 @@ +package testpkg + +import "fmt" + +func Call(fn func()) func() { + return fn +} + +func fn1() { + fmt.Println("Hello, World") // want +1 `unnecessary whitespace \(trailing-whitespace\)` + +} + +func fn2() { + fmt.Println("Hello, World") // want +2 `unnecessary whitespace \(trailing-whitespace\)` + // Comment with wihtespace + +} + +func fn3() { + fmt.Println("Hello, World") + // Comment without space before or after. +} + +func fn4() { + _ = 1 + // Comment with + + // a newline between +} + +func fn5() { + if true { + _ = 1 // want +1 `unnecessary whitespace \(trailing-whitespace\)` + + } +} + +func fn6() { + if true { + _ = 1 // want +1 `unnecessary whitespace \(trailing-whitespace\)` + + } else if true { + _ = 1 // want +1 `unnecessary whitespace \(trailing-whitespace\)` + + } else { + _ = 1 // want +1 `unnecessary whitespace \(trailing-whitespace\)` + + } +} + +func fn8(a string, b any, s []string) { + if true { + _ = 1 // want +1 `unnecessary whitespace \(trailing-whitespace\)` + + } else if true { + _ = 1 // want +1 `unnecessary whitespace \(trailing-whitespace\)` + + } + + for i := 0; i < 1; i++ { + _ = 1 // want +1 `unnecessary whitespace \(trailing-whitespace\)` + + } + + for n := range []int{} { + _ = n // want +1 `unnecessary whitespace \(trailing-whitespace\)` + + } + + for range s { + _ = 1 // want +1 `unnecessary whitespace \(trailing-whitespace\)` + + } + + switch a { + case "a": + + } + + switch b.(type) { + case int: + + } + + f := func() { + _ = 1 // want +1 `unnecessary whitespace \(trailing-whitespace\)` + + } + + f2 := Call(func() { + _ = 1 // want +1 `unnecessary whitespace \(trailing-whitespace\)` + + }) + + _ = f + _ = f2 +} diff --git a/testdata/src/default_config/whitespace/trailing_whitespace.go.golden b/testdata/src/default_config/whitespace/trailing_whitespace.go.golden new file mode 100644 index 0000000..12d664f --- /dev/null +++ b/testdata/src/default_config/whitespace/trailing_whitespace.go.golden @@ -0,0 +1,85 @@ +package testpkg + +import "fmt" + +func Call(fn func()) func() { + return fn +} + +func fn1() { + fmt.Println("Hello, World") // want +1 `unnecessary whitespace \(trailing-whitespace\)` +} + +func fn2() { + fmt.Println("Hello, World") // want +2 `unnecessary whitespace \(trailing-whitespace\)` + // Comment with wihtespace +} + +func fn3() { + fmt.Println("Hello, World") + // Comment without space before or after. +} + +func fn4() { + _ = 1 + // Comment with + + // a newline between +} + +func fn5() { + if true { + _ = 1 // want +1 `unnecessary whitespace \(trailing-whitespace\)` + } +} + +func fn6() { + if true { + _ = 1 // want +1 `unnecessary whitespace \(trailing-whitespace\)` + } else if true { + _ = 1 // want +1 `unnecessary whitespace \(trailing-whitespace\)` + } else { + _ = 1 // want +1 `unnecessary whitespace \(trailing-whitespace\)` + } +} + +func fn8(a string, b any, s []string) { + if true { + _ = 1 // want +1 `unnecessary whitespace \(trailing-whitespace\)` + } else if true { + _ = 1 // want +1 `unnecessary whitespace \(trailing-whitespace\)` + } + + for i := 0; i < 1; i++ { + _ = 1 // want +1 `unnecessary whitespace \(trailing-whitespace\)` + } + + for n := range []int{} { + _ = n // want +1 `unnecessary whitespace \(trailing-whitespace\)` + } + + for range s { + _ = 1 // want +1 `unnecessary whitespace \(trailing-whitespace\)` + } + + switch a { + case "a": + + } + + switch b.(type) { + case int: + + } + + f := func() { + _ = 1 // want +1 `unnecessary whitespace \(trailing-whitespace\)` + } + + f2 := Call(func() { + _ = 1 // want +1 `unnecessary whitespace \(trailing-whitespace\)` + }) + + _ = f + _ = f2 +} diff --git a/testdata/src/wip/wip.go b/testdata/src/wip/wip.go deleted file mode 100644 index a21e3cf..0000000 --- a/testdata/src/wip/wip.go +++ /dev/null @@ -1 +0,0 @@ -package testpkg diff --git a/testdata/src/wip/wip.go.golden b/testdata/src/wip/wip.go.golden deleted file mode 100644 index a21e3cf..0000000 --- a/testdata/src/wip/wip.go.golden +++ /dev/null @@ -1 +0,0 @@ -package testpkg diff --git a/testdata/src/with_config/assign_and_anything/code.go b/testdata/src/with_config/assign_and_anything/code.go deleted file mode 100644 index 344f838..0000000 --- a/testdata/src/with_config/assign_and_anything/code.go +++ /dev/null @@ -1,13 +0,0 @@ -package pkg - -import "fmt" - -func Fn() { - if x == 1 { - x = 0 - } - z := x + 2 - - fmt.Println("x") - y := "x" -} diff --git a/testdata/src/with_config/assign_and_anything/code.go.golden b/testdata/src/with_config/assign_and_anything/code.go.golden deleted file mode 100644 index 64fe3f0..0000000 --- a/testdata/src/with_config/assign_and_anything/code.go.golden +++ /dev/null @@ -1,14 +0,0 @@ -package pkg - -import "fmt" - -func Fn() { - if x == 1 { - x = 0 - } - z := x + 2 - - fmt.Println("x") - y := "x" -} - diff --git a/testdata/src/with_config/assign_and_call/code.go b/testdata/src/with_config/assign_and_call/code.go deleted file mode 100644 index d34737a..0000000 --- a/testdata/src/with_config/assign_and_call/code.go +++ /dev/null @@ -1,8 +0,0 @@ -package pkg - -func AllowAssignAndCallCuddle() { - p.token(':') - d.StartBit = p.uint() // want "assignments should only be cuddled with other assignments" - p.token('|') // want "only cuddled expressions if assigning variable or using from line above" - d.Size = p.uint() // want "assignments should only be cuddled with other assignments" -} diff --git a/testdata/src/with_config/assign_and_call/code.go.golden b/testdata/src/with_config/assign_and_call/code.go.golden deleted file mode 100644 index b245cab..0000000 --- a/testdata/src/with_config/assign_and_call/code.go.golden +++ /dev/null @@ -1,11 +0,0 @@ -package pkg - -func AllowAssignAndCallCuddle() { - p.token(':') - - d.StartBit = p.uint() // want "assignments should only be cuddled with other assignments" - - p.token('|') // want "only cuddled expressions if assigning variable or using from line above" - - d.Size = p.uint() // want "assignments should only be cuddled with other assignments" -} diff --git a/testdata/src/with_config/assign_expr/assign_expr.go b/testdata/src/with_config/assign_expr/assign_expr.go new file mode 100644 index 0000000..92cd61b --- /dev/null +++ b/testdata/src/with_config/assign_expr/assign_expr.go @@ -0,0 +1,7 @@ +package testpkg + +func assignAndCall(t1 SomeT) { + t1.Fn() + x := t1.Fn() // want `missing whitespace above this line \(invalid statement above assign\)` + t1.Fn() +} diff --git a/testdata/src/with_config/assign_expr/assign_expr.go.golden b/testdata/src/with_config/assign_expr/assign_expr.go.golden new file mode 100644 index 0000000..84a7146 --- /dev/null +++ b/testdata/src/with_config/assign_expr/assign_expr.go.golden @@ -0,0 +1,9 @@ +package testpkg + +func assignAndCall(t1 SomeT) { + t1.Fn() + + x := t1.Fn() // want `missing whitespace above this line \(invalid statement above assign\)` + t1.Fn() +} + diff --git a/testdata/src/with_config/branch_max_lines/branch.go b/testdata/src/with_config/branch_max_lines/branch.go new file mode 100644 index 0000000..710dbd4 --- /dev/null +++ b/testdata/src/with_config/branch_max_lines/branch.go @@ -0,0 +1,18 @@ +package testpkg + +func fn1() int { + _ = 1 + _ = 2 + _ = 3 + _ = 4 + return 1 +} + +func fn2() int { + _ = 1 + _ = 2 + _ = 3 + _ = 4 + _ = 5 + return 1 // want `missing whitespace above this line \(too many lines above return\)` +} diff --git a/testdata/src/with_config/branch_max_lines/branch.go.golden b/testdata/src/with_config/branch_max_lines/branch.go.golden new file mode 100644 index 0000000..ccc96e3 --- /dev/null +++ b/testdata/src/with_config/branch_max_lines/branch.go.golden @@ -0,0 +1,20 @@ +package testpkg + +func fn1() int { + _ = 1 + _ = 2 + _ = 3 + _ = 4 + return 1 +} + +func fn2() int { + _ = 1 + _ = 2 + _ = 3 + _ = 4 + _ = 5 + + return 1 // want `missing whitespace above this line \(too many lines above return\)` +} + diff --git a/testdata/src/with_config/case_blocks/code.go b/testdata/src/with_config/case_blocks/code.go deleted file mode 100644 index 347c88a..0000000 --- a/testdata/src/with_config/case_blocks/code.go +++ /dev/null @@ -1,48 +0,0 @@ -package testpkg - -import "fmt" - -func Fn() { - switch 1 { - case 1: // want "block should not start with a whitespace" - - // Comment - - // Comment - - fmt.Println("a") - - // Comment - fmt.Println("a") // want "case block should end with newline at this size" - case 2: // want "block should not start with a whitespace" - - fmt.Println("a") - case 3: - fmt.Println("a") - case 4: - fmt.Println("a") - fmt.Println("a") - fmt.Println("a") - fmt.Println("a") // want "case block should end with newline at this size" - case 5: - fmt.Println("a") - fmt.Println("a") - fmt.Println("a") - fmt.Println("a") - // Error // want "case block should end with newline at this size" - case 6: - // Comment - // Comment - case 7: - fmt.Println("a") - /* - - Comment - - */ - - // Error // want "case block should end with newline at this size" - case 7: - fmt.Println("a") - } -} diff --git a/testdata/src/with_config/case_blocks/code.go.golden b/testdata/src/with_config/case_blocks/code.go.golden deleted file mode 100644 index f345c75..0000000 --- a/testdata/src/with_config/case_blocks/code.go.golden +++ /dev/null @@ -1,48 +0,0 @@ -package testpkg - -import "fmt" - -func Fn() { - switch 1 { - case 1: // want "block should not start with a whitespace" - // Comment - // Comment - fmt.Println("a") - - // Comment - fmt.Println("a") // want "case block should end with newline at this size" - - case 2: // want "block should not start with a whitespace" - fmt.Println("a") - case 3: - fmt.Println("a") - case 4: - fmt.Println("a") - fmt.Println("a") - fmt.Println("a") - fmt.Println("a") // want "case block should end with newline at this size" - - case 5: - fmt.Println("a") - fmt.Println("a") - fmt.Println("a") - fmt.Println("a") - // Error // want "case block should end with newline at this size" - - case 6: - // Comment - // Comment - case 7: - fmt.Println("a") - /* - - Comment - - */ - - // Error // want "case block should end with newline at this size" - - case 7: - fmt.Println("a") - } -} diff --git a/testdata/src/with_config/case_max_lines/case.go b/testdata/src/with_config/case_max_lines/case.go new file mode 100644 index 0000000..f07becd --- /dev/null +++ b/testdata/src/with_config/case_max_lines/case.go @@ -0,0 +1,41 @@ +package testpkg + +import "context" + +func fn1(n int) { + switch n { + case 1: + n++ + case 2: + n++ + n++ + case 3: + n++ + n++ + n++ // want `missing whitespace below this line \(case-trailing-newline\)` + case 4: + n++ + n++ + n++ + n++ + // Just a comment + default: + n++ + n++ + n++ + n++ + } +} + +func fn2(ctx context.Context, ch1 chan struct{}) { + select { + case ctx.Done(): + _ = 1 + _ = 1 + _ = 1 // want `missing whitespace below this line \(case-trailing-newline\)` + case <-ch1: + _ = 1 + default: + _ = 1 + } +} diff --git a/testdata/src/with_config/case_max_lines/case.go.golden b/testdata/src/with_config/case_max_lines/case.go.golden new file mode 100644 index 0000000..fa6e278 --- /dev/null +++ b/testdata/src/with_config/case_max_lines/case.go.golden @@ -0,0 +1,43 @@ +package testpkg + +import "context" + +func fn1(n int) { + switch n { + case 1: + n++ + case 2: + n++ + n++ + case 3: + n++ + n++ + n++ // want `missing whitespace below this line \(case-trailing-newline\)` + + case 4: + n++ + n++ + n++ + n++ + // Just a comment + default: + n++ + n++ + n++ + n++ + } +} + +func fn2(ctx context.Context, ch1 chan struct{}) { + select { + case ctx.Done(): + _ = 1 + _ = 1 + _ = 1 // want `missing whitespace below this line \(case-trailing-newline\)` + + case <-ch1: + _ = 1 + default: + _ = 1 + } +} diff --git a/testdata/src/with_config/decl/code.go b/testdata/src/with_config/decl/code.go deleted file mode 100644 index 0837e4e..0000000 --- a/testdata/src/with_config/decl/code.go +++ /dev/null @@ -1,7 +0,0 @@ -package pkg - -func Fn() { - var a = 1 - var b = 2 - var c = 3 -} diff --git a/testdata/src/with_config/decl/code.go.golden b/testdata/src/with_config/decl/code.go.golden deleted file mode 100644 index 70e8eb5..0000000 --- a/testdata/src/with_config/decl/code.go.golden +++ /dev/null @@ -1,8 +0,0 @@ -package pkg - -func Fn() { - var a = 1 - var b = 2 - var c = 3 -} - diff --git a/testdata/src/with_config/disable_all/disable.go b/testdata/src/with_config/disable_all/disable.go new file mode 100644 index 0000000..a467eb3 --- /dev/null +++ b/testdata/src/with_config/disable_all/disable.go @@ -0,0 +1,174 @@ +package testpkg + +func assign() { + var a = 1 + b := 1 + + _ = a + _ = b +} + +func branch() { + for i := range(make(int[], 2)) { + _ = 1 + _ = 1 + _ = 1 + continue + } + + for i := range(make(int[], 2)) { + _ = 1 + _ = 1 + _ = 1 + break + } +} + +func decl() { + var a = 1 + var b = 1 + var c = b + + _ = a + _ = b + _ = c +} + +func defer() { + d := 1 + defer func() {}() +} + +func expr() { + a := 1 + b := 2 + fmt.Println("") + c := 3 + d := 4 +} + +func for() { + a := 1 + b := 2 + for i := 0; i < b; i++ { + panic(1) + } + + for i := 0; i < 1; i++ { + panic("") + } + for i := 0; i < 1; i++ { + panic("") + } + + _ = a + _ = b +} + +func go() { + f1 := func() {} + + f2 := func() {} + go f1() + + f3 := func() {} + go Fn(f1) +} + +func if() { + a := 1 + b := 2 + if b == 2 { + panic(1) + } + + b = 2 + a = 1 + if b == 2 { + panic(1) + } + + _ = a + _ = b +} + +func label() { +L1: + if true { + _ = 1 + } +L2: + if true { + _ = 1 + } +} + +func range() { + a := []int{} + b := []int{} + for range b { + panic(1) + } + + _ = a + _ = b +} + +func return() { + if true { + _ = 1 + _ = 2 + return 1 + } + return 1 +} + +func select(ctx context.Context, ch1 chan struct{}) { + x := 1 + select { + case ctx.Done(): + _ = 1 + case <-ch1: + _ = 1 + } + + _ = x +} + +func send(ch chan int) { + a := 1 + ch <- 1 +} + +func switch() { + a := 1 + b := 2 + switch b { + case 1: + case 2: + case 3: + panic(a) + } +} + +func typeSwitch() { + var a any = 1 + + b := 1 + switch a.(type) { + case int: + case string: + } + + _ = b +} + +func whitespace() { + + if true { + + _ = 1 + + } + +} diff --git a/testdata/src/with_config/error_check/code.go b/testdata/src/with_config/error_check/code.go deleted file mode 100644 index 67ac6b4..0000000 --- a/testdata/src/with_config/error_check/code.go +++ /dev/null @@ -1,9 +0,0 @@ -package pkg - -func Fn() { - err := ErrorProducingFunc() - - if err != nil { // want "if statements that check an error must be cuddled with the statement that assigned the error" - return err - } -} diff --git a/testdata/src/with_config/error_check/code.go.golden b/testdata/src/with_config/error_check/code.go.golden deleted file mode 100644 index 15fce78..0000000 --- a/testdata/src/with_config/error_check/code.go.golden +++ /dev/null @@ -1,8 +0,0 @@ -package pkg - -func Fn() { - err := ErrorProducingFunc() - if err != nil { // want "if statements that check an error must be cuddled with the statement that assigned the error" - return err - } -} diff --git a/testdata/src/with_config/exclusive_short_decl/code.go b/testdata/src/with_config/exclusive_short_decl/code.go new file mode 100644 index 0000000..d150535 --- /dev/null +++ b/testdata/src/with_config/exclusive_short_decl/code.go @@ -0,0 +1,15 @@ +package testpkg + +func fn() { + a := 1 + b := 2 + c := 3 + + a = 1 + b = 1 + c = 1 + + d := 1 + a = 3 // want `missing whitespace above this line \(invalid statement above assign\)` + e := 4 // want `missing whitespace above this line \(invalid statement above assign\)` +} diff --git a/testdata/src/with_config/exclusive_short_decl/code.go.golden b/testdata/src/with_config/exclusive_short_decl/code.go.golden new file mode 100644 index 0000000..68320ed --- /dev/null +++ b/testdata/src/with_config/exclusive_short_decl/code.go.golden @@ -0,0 +1,17 @@ +package testpkg + +func fn() { + a := 1 + b := 2 + c := 3 + + a = 1 + b = 1 + c = 1 + + d := 1 + + a = 3 // want `missing whitespace above this line \(invalid statement above assign\)` + + e := 4 // want `missing whitespace above this line \(invalid statement above assign\)` +} diff --git a/testdata/src/with_config/first_in_block_n1/block.go b/testdata/src/with_config/first_in_block_n1/block.go new file mode 100644 index 0000000..e218e01 --- /dev/null +++ b/testdata/src/with_config/first_in_block_n1/block.go @@ -0,0 +1,31 @@ +package testpkg + +func Fn(_ int) {} + +func fn1() { + one := 1 + two := 2 + three := 3 + + a := 1 + if true { + Fn(a) + } + + b := 1 + if true { // want `missing whitespace above this line \(no shared variables above if\)` + Fn(one) + + if false { + Fn(two) + + if true { + Fn(three) + + if true { + Fn(b) + } + } + } + } +} diff --git a/testdata/src/with_config/first_in_block_n1/block.go.golden b/testdata/src/with_config/first_in_block_n1/block.go.golden new file mode 100644 index 0000000..97e063a --- /dev/null +++ b/testdata/src/with_config/first_in_block_n1/block.go.golden @@ -0,0 +1,33 @@ +package testpkg + +func Fn(_ int) {} + +func fn1() { + one := 1 + two := 2 + three := 3 + + a := 1 + if true { + Fn(a) + } + + b := 1 + + if true { // want `missing whitespace above this line \(no shared variables above if\)` + Fn(one) + + if false { + Fn(two) + + if true { + Fn(three) + + if true { + Fn(b) + } + } + } + } +} + diff --git a/testdata/src/with_config/if_errcheck/if_errcheck.go b/testdata/src/with_config/if_errcheck/if_errcheck.go new file mode 100644 index 0000000..5732f48 --- /dev/null +++ b/testdata/src/with_config/if_errcheck/if_errcheck.go @@ -0,0 +1,77 @@ +package testpkg + +import "errors" + +func fn1() { + err := errors.New("x") // want +1 `unnecessary whitespace \(err\)` + + if err != nil { + panic(err) + } +} + +func fn11() { // want +2 `unnecessary whitespace \(err\)` + err := errors.New("x") + + if err != nil { + panic(err) + } +} + +func fn12() { + err := errors.New("x") + // Some comment + if err != nil { + panic(err) + } +} + +func fn13() { + err := errors.New("x") + + // Some comment + if err != nil { + panic(err) + } +} + +func fn2() { + a := 1 + err := errors.New("x") // want +1 `unnecessary whitespace \(err\)` + + if err != nil { + panic(err) + } + + _ = a +} + +func fn21() { + a := 1 // want +2 `unnecessary whitespace \(err\)` + err := errors.New("x") + + if err != nil { + panic(err) + } + + _ = a +} + +func fn3() { + a := 1 + err := errors.New("x") // want `missing whitespace above this line \(too many statements above if\)` + if err != nil { + panic(err) + } + + _ = a +} + +func fn4() { + msg := "not an error" + err := &msg + + if err != nil { + panic(err) + } +} diff --git a/testdata/src/with_config/if_errcheck/if_errcheck.go.golden b/testdata/src/with_config/if_errcheck/if_errcheck.go.golden new file mode 100644 index 0000000..c7b6353 --- /dev/null +++ b/testdata/src/with_config/if_errcheck/if_errcheck.go.golden @@ -0,0 +1,77 @@ + +package testpkg + +import "errors" + +func fn1() { + err := errors.New("x") // want +1 `unnecessary whitespace \(err\)` + if err != nil { + panic(err) + } +} + +func fn11() { // want +2 `unnecessary whitespace \(err\)` + err := errors.New("x") + if err != nil { + panic(err) + } +} + +func fn12() { + err := errors.New("x") + // Some comment + if err != nil { + panic(err) + } +} + +func fn13() { + err := errors.New("x") + + // Some comment + if err != nil { + panic(err) + } +} + +func fn2() { + a := 1 + + err := errors.New("x") // want +1 `unnecessary whitespace \(err\)` + if err != nil { + panic(err) + } + + _ = a +} + +func fn21() { + a := 1 // want +2 `unnecessary whitespace \(err\)` + + err := errors.New("x") + if err != nil { + panic(err) + } + + _ = a +} + +func fn3() { + a := 1 + + err := errors.New("x") // want `missing whitespace above this line \(too many statements above if\)` + if err != nil { + panic(err) + } + + _ = a +} + +func fn4() { + msg := "not an error" + err := &msg + + if err != nil { + panic(err) + } +} diff --git a/testdata/src/with_config/multi_line_assign/code.go b/testdata/src/with_config/multi_line_assign/code.go deleted file mode 100644 index 5f8a372..0000000 --- a/testdata/src/with_config/multi_line_assign/code.go +++ /dev/null @@ -1,13 +0,0 @@ -package pkg - -import "fmt" - -func AllowMultiLineAssignCuddle() { - x := SomeFunc( - "taking multiple values", - "suddendly not a single line", - ) - if x != nil { // want "if statements should only be cuddled with assignments" - fmt.Println("denied") - } -} diff --git a/testdata/src/with_config/multi_line_assign/code.go.golden b/testdata/src/with_config/multi_line_assign/code.go.golden deleted file mode 100644 index 044d0aa..0000000 --- a/testdata/src/with_config/multi_line_assign/code.go.golden +++ /dev/null @@ -1,14 +0,0 @@ -package pkg - -import "fmt" - -func AllowMultiLineAssignCuddle() { - x := SomeFunc( - "taking multiple values", - "suddendly not a single line", - ) - - if x != nil { // want "if statements should only be cuddled with assignments" - fmt.Println("denied") - } -} diff --git a/testdata/src/with_config/no_check_decl/no_check_decl.go b/testdata/src/with_config/no_check_decl/no_check_decl.go new file mode 100644 index 0000000..2036697 --- /dev/null +++ b/testdata/src/with_config/no_check_decl/no_check_decl.go @@ -0,0 +1,20 @@ +package testpkg + +func fn1() { + var a = 1 + var b = 2 + const c = 3 + const d = 4 + + e := 5 + var f = 6 + g := 7 + + _ = a + _ = b + _ = c + _ = d + _ = e + _ = f + _ = g +} diff --git a/testdata/src/with_config/separate_leading_whitespace/code.go b/testdata/src/with_config/separate_leading_whitespace/code.go deleted file mode 100644 index 277b797..0000000 --- a/testdata/src/with_config/separate_leading_whitespace/code.go +++ /dev/null @@ -1,18 +0,0 @@ -package pkg - -import "fmt" - -func Fn() { - // Comment one - - // Comment two - fmt.Println("a") -} - -func Fn2() { // want "block should not start with a whitespace" - // Comment one - - // Comment two - - fmt.Println("a") -} diff --git a/testdata/src/with_config/separate_leading_whitespace/code.go.golden b/testdata/src/with_config/separate_leading_whitespace/code.go.golden deleted file mode 100644 index dd6bda4..0000000 --- a/testdata/src/with_config/separate_leading_whitespace/code.go.golden +++ /dev/null @@ -1,17 +0,0 @@ -package pkg - -import "fmt" - -func Fn() { - // Comment one - - // Comment two - fmt.Println("a") -} - -func Fn2() { // want "block should not start with a whitespace" - // Comment one - - // Comment two - fmt.Println("a") -} diff --git a/testdata/src/with_config/short_decl/code.go b/testdata/src/with_config/short_decl/code.go deleted file mode 100644 index 142a972..0000000 --- a/testdata/src/with_config/short_decl/code.go +++ /dev/null @@ -1,19 +0,0 @@ -package pkg - -func Fn() { - a = "a" - b = "b" - c = "c" - - a := "a" - b := "b" - c := "c" - - a := "a" // want "short declaration should cuddle only with other short declarations" - b = "b" - c := "c" // want "short declaration should cuddle only with other short declarations" - - a := "a" // want "short declaration should cuddle only with other short declarations" - b = "b" // Hey - c := "c" // want "short declaration should cuddle only with other short declarations" -} diff --git a/testdata/src/with_config/short_decl/code.go.golden b/testdata/src/with_config/short_decl/code.go.golden deleted file mode 100644 index 835051d..0000000 --- a/testdata/src/with_config/short_decl/code.go.golden +++ /dev/null @@ -1,23 +0,0 @@ -package pkg - -func Fn() { - a = "a" - b = "b" - c = "c" - - a := "a" - b := "b" - c := "c" - - a := "a" // want "short declaration should cuddle only with other short declarations" - - b = "b" - - c := "c" // want "short declaration should cuddle only with other short declarations" - - a := "a" // want "short declaration should cuddle only with other short declarations" - - b = "b" // Hey - - c := "c" // want "short declaration should cuddle only with other short declarations" -} diff --git a/testdata/src/with_config/strict_append/code.go b/testdata/src/with_config/strict_append/code.go deleted file mode 100644 index 1a64a57..0000000 --- a/testdata/src/with_config/strict_append/code.go +++ /dev/null @@ -1,9 +0,0 @@ -package pkg - -func Fn() { - x := []string{} - y := "not going in x" - x = append(x, "not y") - z := "string" - x = append(x, "i don't care") -} diff --git a/testdata/src/with_config/strict_append/code.go.golden b/testdata/src/with_config/strict_append/code.go.golden deleted file mode 100644 index 961a4a3..0000000 --- a/testdata/src/with_config/strict_append/code.go.golden +++ /dev/null @@ -1,10 +0,0 @@ -package pkg - -func Fn() { - x := []string{} - y := "not going in x" - x = append(x, "not y") - z := "string" - x = append(x, "i don't care") -} - diff --git a/testdata/src/with_config/trailing_comments/code.go b/testdata/src/with_config/trailing_comments/code.go deleted file mode 100644 index 448d668..0000000 --- a/testdata/src/with_config/trailing_comments/code.go +++ /dev/null @@ -1,17 +0,0 @@ -package pkg - -import "fmt" - -func Fn() { - if true { - fmt.Println("a") - // Comment - } - - if true { - fmt.Println("a") - // Comment - - // Comment - } // want "block should not end with a whitespace" -} diff --git a/testdata/src/with_config/trailing_comments/code.go.golden b/testdata/src/with_config/trailing_comments/code.go.golden deleted file mode 100644 index 3f60deb..0000000 --- a/testdata/src/with_config/trailing_comments/code.go.golden +++ /dev/null @@ -1,16 +0,0 @@ -package pkg - -import "fmt" - -func Fn() { - if true { - fmt.Println("a") - // Comment - } - - if true { - fmt.Println("a") - // Comment - // Comment - } // want "block should not end with a whitespace" -} diff --git a/testdata/src/with_config/whole_block/block.go b/testdata/src/with_config/whole_block/block.go new file mode 100644 index 0000000..e7665e9 --- /dev/null +++ b/testdata/src/with_config/whole_block/block.go @@ -0,0 +1,140 @@ +package testpkg + +func Fn(_ int) {} + +func fn1() { + x := 1 + if true { + y, z := 1, 2 + if false { + z++ + if true { // want `missing whitespace above this line \(no shared variables above if\)` + y++ + x++ + } + } + } +} + +func fn2() { + a := 1 + b := 1 + c := 1 + + a = 1 + if true { + a++ + } else if false { + b++ + } else { + c++ + } + + b = 1 + if true { + a++ + } else if false { + b++ + } else { + c++ + } + + c = 1 + if true { + a++ + } else if false { + b++ + } else { + c++ + } +} + +func fn3(a, b, c itn) { + a = 1 + for range make([]int, 10) { + if true { + c++ + + Fn(a) + } + } + + b = 1 + for range make([]int, 10) { // want `missing whitespace above this line \(no shared variables above range\)` + if true { + c++ + + Fn(a) + } + } + + a = 1 + for i := 0; i < 3; i++ { + if true { + c++ + + Fn(a) + } + } + + b = 1 + for i := 0; i < 3; i++ { // want `missing whitespace above this line \(no shared variables above for\)` + if true { + c++ + + Fn(a) + } + } +} + +func fn4(a, b, c int) { + a = 1 + switch { + case true: + c++ + a++ + case false: + c++ + a-- + } + + b = 1 + switch { // want `missing whitespace above this line \(no shared variables above switch\)` + case true: + c++ + a++ + case false: + c++ + a-- + } +} + +func fn5(a, b, c int) { + a = 1 + go func() { + c++ + a++ + }() + + b = 1 + go func() { // want `missing whitespace above this line \(no shared variables above go\)` + c++ + a++ + }() +} + +func fn6(a, b, c int) { + a = 1 + defer func() { + if true { + c++ + a++ + } + }() + + b = 1 + defer func() { // want `missing whitespace above this line \(no shared variables above defer\)` + c++ + a++ + }() +} diff --git a/testdata/src/with_config/whole_block/block.go.golden b/testdata/src/with_config/whole_block/block.go.golden new file mode 100644 index 0000000..576ca62 --- /dev/null +++ b/testdata/src/with_config/whole_block/block.go.golden @@ -0,0 +1,146 @@ +package testpkg + +func Fn(_ int) {} + +func fn1() { + x := 1 + if true { + y, z := 1, 2 + if false { + z++ + + if true { // want `missing whitespace above this line \(no shared variables above if\)` + y++ + x++ + } + } + } +} + +func fn2() { + a := 1 + b := 1 + c := 1 + + a = 1 + if true { + a++ + } else if false { + b++ + } else { + c++ + } + + b = 1 + if true { + a++ + } else if false { + b++ + } else { + c++ + } + + c = 1 + if true { + a++ + } else if false { + b++ + } else { + c++ + } +} + +func fn3(a, b, c itn) { + a = 1 + for range make([]int, 10) { + if true { + c++ + + Fn(a) + } + } + + b = 1 + + for range make([]int, 10) { // want `missing whitespace above this line \(no shared variables above range\)` + if true { + c++ + + Fn(a) + } + } + + a = 1 + for i := 0; i < 3; i++ { + if true { + c++ + + Fn(a) + } + } + + b = 1 + + for i := 0; i < 3; i++ { // want `missing whitespace above this line \(no shared variables above for\)` + if true { + c++ + + Fn(a) + } + } +} + +func fn4(a, b, c int) { + a = 1 + switch { + case true: + c++ + a++ + case false: + c++ + a-- + } + + b = 1 + + switch { // want `missing whitespace above this line \(no shared variables above switch\)` + case true: + c++ + a++ + case false: + c++ + a-- + } +} + +func fn5(a, b, c int) { + a = 1 + go func() { + c++ + a++ + }() + + b = 1 + + go func() { // want `missing whitespace above this line \(no shared variables above go\)` + c++ + a++ + }() +} + +func fn6(a, b, c int) { + a = 1 + defer func() { + if true { + c++ + a++ + } + }() + + b = 1 + + defer func() { // want `missing whitespace above this line \(no shared variables above defer\)` + c++ + a++ + }() +} diff --git a/wsl.go b/wsl.go index 6e4bf8a..cd60f14 100644 --- a/wsl.go +++ b/wsl.go @@ -4,1408 +4,1084 @@ import ( "fmt" "go/ast" "go/token" - "reflect" - "sort" - "strings" -) + "go/types" + "slices" -// 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" + "golang.org/x/tools/go/analysis" ) -// 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" + messageMissingWhitespaceAbove = "missing whitespace above this line" + messageMissingWhitespaceBelow = "missing whitespace below this line" + messageRemoveWhitespace = "unnecessary whitespace" ) -// Configuration represents configurable settingds 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 { +type fixRange struct { fixRangeStart token.Pos fixRangeEnd token.Pos } -// result represents the result of one error. -type result struct { - fixRanges []fix - reason string +type issue struct { + message string + // We can report multiple fixes at the same position. This happens e.g. when + // we force error cuddling but the error assignment is already cuddled. + // See `checkError` for examples. + fixRanges []fixRange } -// 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 +type WSL struct { file *ast.File - fileSet *token.FileSet - result map[token.Pos]result - warnings []string + fset *token.FileSet + typeInfo *types.Info + issues map[token.Pos]issue + config *Configuration } -// 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), +func New(file *ast.File, pass *analysis.Pass, cfg *Configuration) *WSL { + return &WSL{ + fset: pass.Fset, + file: file, + typeInfo: pass.TypesInfo, + issues: make(map[token.Pos]issue), + config: cfg, } } -// 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) +// Run will run analysis on the file and pass passed to the constructor. It's +// typically only supposed to be used by [analysis.Analyzer]. +func (w *WSL) Run() { + for _, decl := range w.file.Decls { + if funcDecl, ok := decl.(*ast.FuncDecl); ok { + w.checkFunc(funcDecl) } } } -// 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 +func (w *WSL) checkStmt(stmt ast.Stmt, cursor *Cursor) { + //nolint:gocritic // This is not commented out code, it's examples + switch s := stmt.(type) { + // if a {} else if b {} else {} + case *ast.IfStmt: + w.checkIf(s, cursor, false) + // for {} / for a; b; c {} + case *ast.ForStmt: + w.checkFor(s, cursor) + // for _, _ = range a {} + case *ast.RangeStmt: + w.checkRange(s, cursor) + // switch {} // switch a {} + case *ast.SwitchStmt: + w.checkSwitch(s, cursor) + // switch a.(type) {} + case *ast.TypeSwitchStmt: + w.checkTypeSwitch(s, cursor) + // return a + case *ast.ReturnStmt: + w.checkReturn(s, cursor) + // continue / break + case *ast.BranchStmt: + w.checkBranch(s, cursor) + // var a + case *ast.DeclStmt: + w.checkDeclStmt(s, cursor) + // a := a + case *ast.AssignStmt: + w.checkAssign(s, cursor) + // a++ / a-- + case *ast.IncDecStmt: + w.checkIncDec(s, cursor) + // defer func() {} + case *ast.DeferStmt: + w.checkDefer(s, cursor) + // go func() {} + case *ast.GoStmt: + w.checkGo(s, cursor) + // e.g. someFn() + case *ast.ExprStmt: + w.checkExprStmt(s, cursor) + // case: + case *ast.CaseClause: + w.checkCaseClause(s, cursor) + // case: + case *ast.CommClause: + w.checkCommClause(s, cursor) + // { } + case *ast.BlockStmt: + w.checkBlock(s) + // select { } + case *ast.SelectStmt: + w.checkSelect(s, cursor) + // ch <- ... + case *ast.SendStmt: + w.checkSend(s, cursor) + // LABEL: + case *ast.LabeledStmt: + w.checkLabel(s, cursor) + case *ast.EmptyStmt: + default: } +} - // Start by finding leading and trailing whitespaces. - p.findLeadingAndTrailingWhitespaces(ident, block, nil) +//nolint:unparam // False positive on `cursor` +func (w *WSL) checkExpr(expr ast.Expr, cursor *Cursor) { + switch s := expr.(type) { + // func() {} + case *ast.FuncLit: + w.checkBlock(s.Body) + // Call(args...) + case *ast.CallExpr: + w.checkExpr(s.Fun, cursor) - // Parse the block body contents. - p.parseBlockStatements(block.List) + for _, e := range s.Args { + w.checkExpr(e, cursor) + } + case *ast.StarExpr: + w.checkExpr(s.X, cursor) + case *ast.CompositeLit: + for _, e := range s.Elts { + w.checkExpr(e, cursor) + } + case *ast.ArrayType, + *ast.BasicLit, + *ast.BinaryExpr, + *ast.ChanType, + *ast.Ellipsis, + *ast.Ident, + *ast.IndexExpr, + *ast.IndexListExpr, + *ast.KeyValueExpr, + *ast.MapType, + *ast.ParenExpr, + *ast.SelectorExpr, + *ast.SliceExpr, + *ast.TypeAssertExpr, + *ast.UnaryExpr, + nil: + default: + } } -// 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) +func (w *WSL) checkDecl(decl ast.Decl, cursor *Cursor) { + switch d := decl.(type) { + case *ast.GenDecl: + for _, spec := range d.Specs { + w.checkSpec(spec, cursor) } + case *ast.FuncDecl: + w.checkStmt(d.Body, cursor) + case *ast.BadDecl: + default: + } +} - firstBodyStatement := p.firstBodyStatement(i, statements) - - // First statement, nothing to do. - if i == 0 { - continue +func (w *WSL) checkSpec(spec ast.Spec, cursor *Cursor) { + switch s := spec.(type) { + case *ast.ValueSpec: + for _, expr := range s.Values { + w.checkExpr(expr, cursor) } + case *ast.ImportSpec, *ast.TypeSpec: + default: + } +} - previousStatement := statements[i-1] - previousStatementIsMultiline := p.nodeStart(previousStatement) != p.nodeEnd(previousStatement) - cuddledWithLastStmt := p.nodeEnd(previousStatement) == p.nodeStart(stmt)-1 +func (w *WSL) checkBody(body []ast.Stmt) { + cursor := NewCursor(body) - // 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 - } + for cursor.Next() { + w.checkStmt(cursor.Stmt(), cursor) + } +} - // We don't force error cuddling for multilines. (#86) - if p.config.ForceCuddleErrCheckAndAssign && previousStatementIsMultiline && !cuddledWithLastStmt { - continue - } +func (w *WSL) checkCuddlingBlock(stmt ast.Node, blockList []ast.Stmt, cursor *Cursor, maxAllowedStatements int) { + var firstBlockStmt ast.Node + if len(blockList) > 0 { + firstBlockStmt = blockList[0] + } - // 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 + w.checkCuddlingMaxAllowed(stmt, firstBlockStmt, cursor, maxAllowedStatements) +} - // 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 +func (w *WSL) checkCuddling(stmt ast.Node, cursor *Cursor, maxAllowedStatements int) { + w.checkCuddlingMaxAllowed(stmt, nil, cursor, maxAllowedStatements) +} - // Check if the previous statement spans over multiple lines. - cuddledWithMultiLineAssignment := cuddledWithLastStmt && p.nodeStart(previousStatement) != p.nodeStart(stmt)-1 +func (w *WSL) checkCuddlingMaxAllowed( + stmt ast.Node, + firstBlockStmt ast.Node, + cursor *Cursor, + maxAllowedStatements int, +) { + var previousNode ast.Node - // 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) - } + resetCursor := cursor.Save() - // 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) - } - } + if cursor.Previous() { + previousNode = cursor.Stmt() + } - // We could potentially have a block which require us to check the first - // argument before ruling out an allowed cuddle. - var calledOrAssignedFirstInBlock []string + resetCursor() - if firstBodyStatement != nil { - calledOrAssignedFirstInBlock = append(p.findLHS(firstBodyStatement), p.findRHS(firstBodyStatement)...) - } + numStmtsAbove := w.numberOfStatementsAbove(cursor) + previousIdents := identsFromNode(previousNode, true) - var ( - leftHandSide = p.findLHS(stmt) - rightHandSide = p.findRHS(stmt) - rightAndLeftHandSide = append(leftHandSide, rightHandSide...) - calledOrAssignedOnLineAbove = append(calledOnLineAbove, assignedOnLineAbove...) - ) + // If we don't have any statements above, we only care about potential error + // cuddling (for if statements) so check that. + if numStmtsAbove == 0 { + w.checkError(numStmtsAbove, stmt, previousNode, previousIdents, cursor) + return + } - // If we called some kind of lock on the line above we allow cuddling - // anything. - if atLeastOneInListsMatch(calledOnLineAbove, p.config.AllowCuddleWithCalls) { - continue - } + nodeIsAssignDeclOrIncDec := func(n ast.Node) bool { + _, a := n.(*ast.AssignStmt) + _, d := n.(*ast.DeclStmt) + _, i := n.(*ast.IncDecStmt) - // If we call some kind of unlock on this line we allow cuddling with - // anything. - if atLeastOneInListsMatch(rightHandSide, p.config.AllowCuddleWithRHS) { - continue - } + return a || d || i + } - nStatementsBefore := func(n int) bool { - if i < n { - return false - } + _, currIsDefer := stmt.(*ast.DeferStmt) - for j := 1; j < n; j++ { - s1 := statements[i-j] - s2 := statements[i-(j+1)] + // We're cuddled but not with an assign, declare or defer statement which is + // never allowed. + if !nodeIsAssignDeclOrIncDec(previousNode) && !currIsDefer { + w.addErrorInvalidTypeCuddle(cursor.Stmt().Pos(), cursor.checkType) + return + } - if p.nodeStart(s1)-1 != p.nodeEnd(s2) { - return false - } + checkIntersection := func(other []*ast.Ident) bool { + anyIntersects := identIntersection(previousIdents, other) + if len(anyIntersects) > 0 { + // We have matches, but too many statements above. + if maxAllowedStatements != -1 && numStmtsAbove > maxAllowedStatements { + w.addErrorTooManyStatements(previousNode.Pos(), cursor.checkType) } 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] + return false + } - if p.nodeEnd(s1)+1 != p.nodeStart(s2) { - return false - } - } + // FEATURE(AllowWholeBlock): Allow identifier used anywhere in block + // (including recursive blocks). + if w.config.AllowWholeBlock { + allIdentsInBlock := identsFromNode(stmt, false) + if checkIntersection(allIdentsInBlock) { + return + } + } - return true + // FEATURE(AllowFirstInBlock): Allow identifiers used first in block. + if !w.config.AllowWholeBlock && w.config.AllowFirstInBlock { + firstStmtIdents := identsFromNode(firstBlockStmt, true) + if checkIntersection(firstStmtIdents) { + return } + } - 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 - } - } + currentIdents := identsFromNode(stmt, true) + if checkIntersection(currentIdents) { + return + } - return false - } + intersects := identIntersection(currentIdents, previousIdents) + if len(intersects) > 0 { + return + } - // 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 - } - } - } + // We're cuddled but the line immediately above doesn't contain any + // variables used in this statement. + w.addErrorNoIntersection(stmt.Pos(), cursor.checkType) +} - 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, - ) - } - } +func (w *WSL) checkCuddlingWithoutIntersection(stmt ast.Node, cursor *Cursor) { + if w.numberOfStatementsAbove(cursor) == 0 { + return + } - // 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 - } - } + previousNode := cursor.PreviousNode() - 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) - } + currAssign, currIsAssign := stmt.(*ast.AssignStmt) + previousAssign, prevIsAssign := previousNode.(*ast.AssignStmt) + _, prevIsDecl := previousNode.(*ast.DeclStmt) + _, prevIsIncDec := previousNode.(*ast.IncDecStmt) + + // Cuddling without intersection is allowed for assignments and inc/dec + // statements. If however the check for declarations is disabled, we also + // allow cuddling with them as well. + // + // var x string + // x := "" + // y++ + if _, ok := w.config.Checks[CheckDecl]; ok { + prevIsDecl = false + } + + // If we enable exclusive assign checks we only allow new declarations or + // new assignments together but not mix and match. + // + // When this is enabled we also implicitly disable support to cuddle with + // anything else. + if _, ok := w.config.Checks[CheckAssignExclusive]; ok { + prevIsDecl = false + prevIsIncDec = false + + if prevIsAssign && currIsAssign { + prevIsAssign = previousAssign.Tok == currAssign.Tok } + } - switch t := stmt.(type) { - case *ast.IfStmt: - checkingErrInitializedInline := func() bool { - if t.Init == nil { - return false - } + prevIsValidType := previousNode == nil || prevIsAssign || prevIsDecl || prevIsIncDec - // 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 _, ok := w.config.Checks[CheckAssignExpr]; !ok { + if _, ok := previousNode.(*ast.ExprStmt); ok && hasIntersection(stmt, previousNode) { + prevIsValidType = prevIsValidType || ok + } + } - 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, - ) - } - } + if prevIsValidType { + return + } - continue - } + w.addErrorInvalidTypeCuddle(stmt.Pos(), cursor.checkType) +} - if len(assignedOnLineAbove) == 0 { - p.addWhitespaceBeforeError(t, reasonOnlyCuddleIfWithAssign) - continue - } +func (w *WSL) checkBlock(block *ast.BlockStmt) { + w.checkBlockLeadingNewline(block) + w.checkTrailingNewline(block) - if nStatementsBefore(2) { - reportNewlineTwoLinesAbove(t, statements[i-1], reasonOnlyOneCuddleBeforeIf) - continue - } + w.checkBody(block.List) +} - if atLeastOneInListsMatch(rightAndLeftHandSide, assignedOnLineAbove) { - continue - } +func (w *WSL) checkCaseClause(stmt *ast.CaseClause, cursor *Cursor) { + w.checkCaseLeadingNewline(stmt) - if atLeastOneInListsMatch(assignedOnLineAbove, calledOrAssignedFirstInBlock) { - continue - } + if w.config.CaseMaxLines != 0 { + w.checkCaseTrailingNewline(stmt.Body, cursor) + } - p.addWhitespaceBeforeError(t, reasonOnlyCuddleWithUsedAssign) - case *ast.ReturnStmt: - if isLastStatementInBlockOfOnlyTwoLines() { - continue - } + w.checkBody(stmt.Body) +} - p.addWhitespaceBeforeError(t, reasonOnlyCuddle2LineReturn) - case *ast.BranchStmt: - if isLastStatementInBlockOfOnlyTwoLines() { - continue - } +func (w *WSL) checkCommClause(stmt *ast.CommClause, cursor *Cursor) { + w.checkCommLeadingNewline(stmt) - 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) - } - } + if w.config.CaseMaxLines != 0 { + w.checkCaseTrailingNewline(stmt.Body, cursor) + } - continue - } + w.checkBody(stmt.Body) +} - switch previousStatement.(type) { - case *ast.AssignStmt, *ast.IncDecStmt: - continue - } +func (w *WSL) checkFunc(funcDecl *ast.FuncDecl) { + if funcDecl.Body == nil { + return + } - if p.config.AllowAssignAndAnythingCuddle { - continue - } + w.checkBlock(funcDecl.Body) +} - if _, ok := previousStatement.(*ast.DeclStmt); ok && p.config.AllowCuddleDeclaration { - continue - } +func (w *WSL) checkAssign(stmt *ast.AssignStmt, cursor *Cursor) { + defer func() { + for _, expr := range stmt.Rhs { + w.checkExpr(expr, cursor) + } + }() - // 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 - } - } + if _, ok := w.config.Checks[CheckAssign]; !ok { + return + } - p.addWhitespaceBeforeError(t, reasonAssignsCuddleAssign) - case *ast.IncDecStmt: - switch previousStatement.(type) { - case *ast.AssignStmt, *ast.IncDecStmt: - continue - } + cursor.SetChecker(CheckAssign) - p.addWhitespaceBeforeError(t, reasonAssignsCuddleAssign) + w.checkCuddlingWithoutIntersection(stmt, cursor) + w.checkAppend(stmt, cursor) +} - 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 - } +func (w *WSL) checkAppend(stmt *ast.AssignStmt, cursor *Cursor) { + if _, ok := w.config.Checks[CheckAppend]; !ok { + return + } - p.addWhitespaceBeforeError(t, reasonExpressionCuddledWithDeclOrRet) - case *ast.IfStmt, *ast.RangeStmt, *ast.SwitchStmt: - p.addWhitespaceBeforeError(t, reasonExpressionCuddledWithBlock) - } + if w.numberOfStatementsAbove(cursor) == 0 { + return + } - // 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 - } - } + previousNode := cursor.PreviousNode() - // 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 - } + var appendNode *ast.CallExpr - 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 - } + for _, expr := range stmt.Rhs { + e, ok := expr.(*ast.CallExpr) + if !ok { + 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 - } - } + if f, ok := e.Fun.(*ast.Ident); ok && f.Name == "append" { + appendNode = e + break + } + } - reportNewlineTwoLinesAbove(t, statements[i-1], reasonOnlyOneCuddleBeforeDefer) + if appendNode == nil { + return + } - continue - } + if !hasIntersection(appendNode, previousNode) { + w.addErrorNoIntersection(stmt.Pos(), cursor.checkType) + } +} - // 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 - } +func (w *WSL) checkBranch(stmt *ast.BranchStmt, cursor *Cursor) { + if _, ok := w.config.Checks[CheckBranch]; !ok { + return + } - // 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 - } - } + cursor.SetChecker(CheckBranch) - if atLeastOneInListsMatch(assignedOnLineAbove, calledOrAssignedFirstInBlock) { - continue - } + if w.numberOfStatementsAbove(cursor) == 0 { + return + } - if !atLeastOneInListsMatch(rightAndLeftHandSide, assignedOnLineAbove) { - p.addWhitespaceBeforeError(t, reasonDeferCuddledWithOtherVar) - } - case *ast.ForStmt: - if len(rightAndLeftHandSide) == 0 { - p.addWhitespaceBeforeError(t, reasonForWithoutCondition) - continue - } + lastStmtInBlock := cursor.statements[len(cursor.statements)-1] + firstStmts := cursor.Nth(0) - if nStatementsBefore(2) { - reportNewlineTwoLinesAbove(t, statements[i-1], reasonOnlyOneCuddleBeforeFor) - continue - } + if w.lineFor(lastStmtInBlock.End())-w.lineFor(firstStmts.Pos()) < w.config.BranchMaxLines { + return + } - // 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 - } + w.addErrorTooManyLines(stmt.Pos(), cursor.checkType) +} - if nStatementsBefore(2) { - reportNewlineTwoLinesAbove(t, statements[i-1], reasonOnlyOneCuddleBeforeGo) - continue - } +func (w *WSL) checkDeclStmt(stmt *ast.DeclStmt, cursor *Cursor) { + w.checkDecl(stmt.Decl, cursor) - if c, ok := t.Call.Fun.(*ast.SelectorExpr); ok { - goCallArgs := append(p.findLHS(c.X), p.findRHS(c.X)...) + if _, ok := w.config.Checks[CheckDecl]; !ok { + return + } - if atLeastOneInListsMatch(calledOnLineAbove, goCallArgs) { - continue - } - } + cursor.SetChecker(CheckDecl) - if c, ok := t.Call.Fun.(*ast.FuncLit); ok { - goCallArgs := append(p.findLHS(c.Body), p.findRHS(c.Body)...) + if w.numberOfStatementsAbove(cursor) == 0 { + return + } + + w.addErrorNeverAllow(stmt.Pos(), cursor.checkType) +} - if atLeastOneInListsMatch(assignedOnLineAbove, goCallArgs) { - continue +func (w *WSL) checkDefer(stmt *ast.DeferStmt, cursor *Cursor) { + w.maybeCheckExpr( + stmt, + stmt.Call, + cursor, + func(n ast.Node) (int, bool) { + _, previousIsDefer := n.(*ast.DeferStmt) + _, previousIsIf := n.(*ast.IfStmt) + + // We allow defer as a third node only if we have an if statement + // between, e.g. + // + // f, err := os.Open(file) + // if err != nil { + // return err + // } + // defer f.Close() + if previousIsIf && w.numberOfStatementsAbove(cursor) >= 2 { + defer cursor.Save()() + + cursor.Previous() + cursor.Previous() + + if hasIntersection(cursor.Stmt(), stmt) { + return 1, false } } - if !atLeastOneInListsMatch(rightAndLeftHandSide, assignedOnLineAbove) { - p.addWhitespaceBeforeError(t, reasonGoFuncWithoutAssign) - } - case *ast.SwitchStmt: - if nStatementsBefore(2) { - reportNewlineTwoLinesAbove(t, statements[i-1], reasonOnlyOneCuddleBeforeSwitch) - continue - } + // Only check cuddling if previous statement isn't also a defer. + return 1, !previousIsDefer + }, + CheckDefer, + ) +} - 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 - } +func (w *WSL) checkError( + stmtsAbove int, + ifStmt ast.Node, + previousNode ast.Node, + previousIdents []*ast.Ident, + cursor *Cursor, +) { + defer cursor.Save()() - // 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 ot trailing - // whitespaces within the block. There's nothing in the case itself - // that may be cuddled. - default: - p.addWarning(warnStmtNotImplemented, t.Pos(), t) - } + if _, ok := ifStmt.(*ast.IfStmt); !ok { + return } -} -// 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] + if _, ok := w.config.Checks[CheckErr]; !ok { + return + } - // 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") + cursor.SetChecker(CheckErr) - // 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 stmtsAbove > 0 || len(previousIdents) == 0 { + return + } - if !statementBody.IsValid() { - return firstBodyStatement + if !slices.ContainsFunc(previousIdents, func(ident *ast.Ident) bool { + return w.implementsErr(ident) + }) { + return } - 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] + previousNodeEnd := previousNode.End() + + comments := ast.NewCommentMap(w.fset, previousNode, w.file.Comments) + for _, cg := range comments { + for _, c := range cg { + if c.Pos() < previousNodeEnd || c.End() > ifStmt.Pos() { + continue + } + + if c.End() > previousNodeEnd { + // There's a comment between the error variable and the + // if-statement, we can't do much about this. Most likely, the + // comment has a meaning, but even if not we would end up with + // something like + // + // err := fn() + // // Some Comment + // if err != nil {} + // + // Which just feels marginally better than leaving the space + // anyway. + if w.lineFor(c.End()) != w.lineFor(previousNodeEnd) { + return } + + // If they are on the same line though, we can just extend where + // the line ends. + previousNodeEnd = c.End() } } + } - // 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] - } + w.addError(previousNodeEnd+1, previousNodeEnd, ifStmt.Pos(), messageRemoveWhitespace, cursor.checkType) - p.findLeadingAndTrailingWhitespaces(nil, stmt, nextStatement) - p.parseBlockStatements(statementBodyContent) - default: - p.addWarning( - warnBodyStmtTypeNotImplemented, - stmt.Pos(), statementBodyContent, - ) + // If we add the error at the same position but with a different fix + // range, only the fix range will be updated. + // + // a := 1 + // err := fn() + // + // if err != nil {} + // + // Should become + // + // a := 1 + // + // err := fn() + // if err != nil {} + cursor.Previous() + + // We report this fix on the same pos as the previous diagnostic, but the + // fix is different. The reason is to just stack more fixes for the same + // diagnostic, the issue isn't present until the first fix so this message + // will never be shown to the user. + if w.numberOfStatementsAbove(cursor) > 0 { + w.addError(previousNodeEnd+1, previousNode.Pos(), previousNode.Pos(), messageMissingWhitespaceAbove, cursor.checkType) } +} - return firstBodyStatement +func (w *WSL) checkExprStmt(stmt *ast.ExprStmt, cursor *Cursor) { + w.maybeCheckExpr( + stmt, + stmt.X, + cursor, + func(n ast.Node) (int, bool) { + _, ok := n.(*ast.ExprStmt) + return -1, !ok + }, + CheckExpr, + ) } -func (p *processor) findLHS(node ast.Node) []string { - var lhs []string +func (w *WSL) checkFor(stmt *ast.ForStmt, cursor *Cursor) { + w.maybeCheckBlock(stmt, stmt.Body, cursor, CheckFor) +} - if node == nil { - return lhs - } +func (w *WSL) checkGo(stmt *ast.GoStmt, cursor *Cursor) { + w.maybeCheckExpr( + stmt, + stmt.Call, + cursor, + // We can cuddle any amount `go` statements so only check cuddling if + // the previous one isn't a `go` call. + func(n ast.Node) (int, bool) { + _, ok := n.(*ast.GoStmt) + return 1, !ok + }, + CheckGo, + ) +} - 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) +func (w *WSL) checkIf(stmt *ast.IfStmt, cursor *Cursor, isElse bool) { + // if + w.checkBlock(stmt.Body) + + switch v := stmt.Else.(type) { + // else-if 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) - } + w.checkIf(v, cursor, true) - p.addWarning(warnUnknownLHS, t.Pos(), t) + // else + case *ast.BlockStmt: + w.checkBlock(v) } - return lhs + if _, ok := w.config.Checks[CheckIf]; !isElse && ok { + cursor.SetChecker(CheckIf) + w.checkCuddlingBlock(stmt, stmt.Body.List, cursor, 1) + } } -func (p *processor) findRHS(node ast.Node) []string { - var rhs []string +func (w *WSL) checkIncDec(stmt *ast.IncDecStmt, cursor *Cursor) { + defer w.checkExpr(stmt.X, cursor) - 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)...) - } + if _, ok := w.config.Checks[CheckIncDec]; !ok { + return + } - 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) - } + cursor.SetChecker(CheckIncDec) + + w.checkCuddlingWithoutIntersection(stmt, cursor) +} - p.addWarning(warnUnknownRHS, t.Pos(), t) +func (w *WSL) checkLabel(stmt *ast.LabeledStmt, cursor *Cursor) { + // We check the statement last because the statement is the same node as the + // label (it's a labeled statement). This means that we _first_ want to + // check any violations of cuddling the label (never cuddle label) before we + // actually check the inner statement. + // + // It's a subtle difference, but it makes the diagnostic make more sense. + // We do this by deferring the statmenet check so it happens last no matter + // if we have label checking enabled or not. + defer w.checkStmt(stmt.Stmt, cursor) + + if _, ok := w.config.Checks[CheckLabel]; !ok { + return } - return rhs -} + cursor.SetChecker(CheckLabel) -func (p *processor) isShortDecl(node ast.Node) bool { - if t, ok := node.(*ast.AssignStmt); ok { - return t.Tok == token.DEFINE + if w.numberOfStatementsAbove(cursor) == 0 { + return } - return false + w.addErrorNeverAllow(stmt.Pos(), cursor.checkType) } -func (p *processor) findBlockStmt(node ast.Node) []*ast.BlockStmt { - var blocks []*ast.BlockStmt +func (w *WSL) checkRange(stmt *ast.RangeStmt, cursor *Cursor) { + w.maybeCheckBlock(stmt, stmt.Body, cursor, CheckRange) +} - 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)...) +func (w *WSL) checkReturn(stmt *ast.ReturnStmt, cursor *Cursor) { + for _, expr := range stmt.Results { + w.checkExpr(expr, cursor) + } - 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)...) + if _, ok := w.config.Checks[CheckReturn]; !ok { + return } - return blocks -} + cursor.SetChecker(CheckReturn) -// 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 + // There's only a return statement. + if cursor.Len() <= 1 { + return } - n, ok := maybeHasX.Interface().(ast.Node) - if !ok { - return nil, false + if w.numberOfStatementsAbove(cursor) == 0 { + return } - return n, true + // If the distance between the first statement and the return statement is + // less than `n` LOC we're allowed to cuddle. + firstStmts := cursor.Nth(0) + if w.lineFor(stmt.End())-w.lineFor(firstStmts.Pos()) < w.config.BranchMaxLines { + return + } + + w.addErrorTooManyLines(stmt.Pos(), cursor.checkType) } -func atLeastOneInListsMatch(listOne, listTwo []string) bool { - sliceToMap := func(s []string) map[string]struct{} { - m := map[string]struct{}{} +func (w *WSL) checkSelect(stmt *ast.SelectStmt, cursor *Cursor) { + w.maybeCheckBlock(stmt, stmt.Body, cursor, CheckSelect) +} - for _, v := range s { - m[v] = struct{}{} - } +func (w *WSL) checkSend(stmt *ast.SendStmt, cursor *Cursor) { + defer w.checkExpr(stmt.Value, cursor) - return m + if _, ok := w.config.Checks[CheckSend]; !ok { + return } - m1 := sliceToMap(listOne) - m2 := sliceToMap(listTwo) + cursor.SetChecker(CheckSend) - for k1 := range m1 { - if _, ok := m2[k1]; ok { - return true - } - } + var stmts []ast.Stmt - for k2 := range m2 { - if _, ok := m1[k2]; ok { - return true + ast.Inspect(stmt.Value, func(n ast.Node) bool { + if b, ok := n.(*ast.BlockStmt); ok { + stmts = b.List + return false } - } - return false + return true + }) + + w.checkCuddlingBlock(stmt, stmts, cursor, 1) } -// 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 - ) +func (w *WSL) checkSwitch(stmt *ast.SwitchStmt, cursor *Cursor) { + w.maybeCheckBlock(stmt, stmt.Body, cursor, CheckSwitch) +} - // 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 +func (w *WSL) checkTypeSwitch(stmt *ast.TypeSwitchStmt, cursor *Cursor) { + w.maybeCheckBlock(stmt, stmt.Body, cursor, CheckTypeSwitch) +} + +func (w *WSL) checkCaseTrailingNewline(body []ast.Stmt, cursor *Cursor) { + if len(body) == 0 { + return + } + + defer cursor.Save()() + + if !cursor.Next() { + return + } + + var nextCase ast.Node + + switch n := cursor.Stmt().(type) { case *ast.CaseClause: - blockStatements = t.Body - blockStartPos = t.Colon - isCase = true + nextCase = n case *ast.CommClause: - blockStatements = t.Body - blockStartPos = t.Colon - isCase = true + nextCase = n default: - p.addWarning(warnWSNodeTypeNotImplemented, stmt.Pos(), stmt) - return } - // Ignore empty blocks even if they have newlines or just comments. - if len(blockStatements) < 1 { + firstStmt := body[0] + lastStmt := body[len(body)-1] + totalLines := w.lineFor(lastStmt.End()) - w.lineFor(firstStmt.Pos()) + 1 + + if totalLines < w.config.CaseMaxLines { 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 { + // Next case is not immediately after the last statement so must be newline + // already. + if w.lineFor(nextCase.Pos()) > w.lineFor(lastStmt.End())+1 { return } - var ( - firstStatement = blockStatements[0] - lastStatement = blockStatements[len(blockStatements)-1] - ) + w.addError(lastStmt.End(), nextCase.Pos(), nextCase.Pos(), messageMissingWhitespaceBelow, CheckCaseTrailingNewline) +} - // Get the comment related to the first statement, we do allow commends in - // the beginning of a block before the first statement. - var ( - openingNodePos = blockStartPos + 1 - lastLeadingComment ast.Node - ) +func (w *WSL) checkBlockLeadingNewline(body *ast.BlockStmt) { + comments := ast.NewCommentMap(w.fset, body, w.file.Comments) + w.checkLeadingNewline(body.Lbrace, body.List, comments) +} - var ( - firstStatementCommentGroups []*ast.CommentGroup - lastStatementCommentGroups []*ast.CommentGroup - ) +func (w *WSL) checkCaseLeadingNewline(caseClause *ast.CaseClause) { + comments := ast.NewCommentMap(w.fset, caseClause, w.file.Comments) + w.checkLeadingNewline(caseClause.Colon, caseClause.Body, comments) +} - 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 - } +func (w *WSL) checkCommLeadingNewline(commClause *ast.CommClause) { + comments := ast.NewCommentMap(w.fset, commClause, w.file.Comments) + w.checkLeadingNewline(commClause.Colon, commClause.Body, comments) +} - // 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) - } - } +func (w *WSL) checkLeadingNewline(startPos token.Pos, body []ast.Stmt, comments ast.CommentMap) { + if _, ok := w.config.Checks[CheckLeadingWhitespace]; !ok { + return + } - // 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 - } + // No statements in the block, let's leave it as is. + if len(body) == 0 { + return + } - for _, c := range cg { - if c.Pos() > lastStatement.End() && c.End() < nextStatement.Pos() { - lastStatementCommentGroups = append(lastStatementCommentGroups, c) + openLine := w.lineFor(startPos) + openingPos := startPos + 1 + firstStmt := body[0].Pos() + + for _, commentGroup := range comments { + for _, comment := range commentGroup { + // The comment starts after the current opening position (originally + // the LBrace) and ends before the current first statement + // (originally first body.List item). + if comment.Pos() > openingPos && comment.End() < firstStmt { + openingPosLine := w.lineFor(openingPos) + commentStartLine := w.lineFor(comment.Pos()) + + // If comment starts at the same line as the opening position it + // should just extend the position for the fixer if needed. + // func fn() { // This comment starts at the same line as LBrace + switch { + // The comment is on the same line as current opening position. + // E.g. func fn() { // A comment + case commentStartLine == openingPosLine: + openingPos = comment.End() + // Opening position is the same as `{` and the comment is + // directly on the line after (no empty line) + case openingPosLine == openLine && + commentStartLine == openLine+1: + openingPos = comment.End() + // The opening position has been updated, it's another comment. + case openingPosLine != openLine: + openingPos = comment.End() + // The opening position is still { and the comment is not + // directly above - it must be an empty line which shouldn't be + // there. + default: + firstStmt = comment.Pos() } } } + } - // 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() - }) + openingPosLine := w.fset.PositionFor(openingPos, false).Line + firstStmtLine := w.fset.PositionFor(firstStmt, false).Line - sort.Slice(lastStatementCommentGroups, func(i, j int) bool { - return lastStatementCommentGroups[i].Pos() < lastStatementCommentGroups[j].Pos() - }) + if firstStmtLine > openingPosLine+1 { + w.addError(openingPos+1, openingPos, firstStmt, messageRemoveWhitespace, CheckLeadingWhitespace) } +} - 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 - } +func (w *WSL) checkTrailingNewline(body *ast.BlockStmt) { + if _, ok := w.config.Checks[CheckTrailingWhitespace]; !ok { + return + } - // 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 - } + // No statements in the block, let's leave it as is. + if len(body.List) == 0 { + return + } + + lastStmt := body.List[len(body.List)-1] + + // We don't want to force removal of the empty line for the last case since + // it can be use for consistency and readability. + if _, ok := lastStmt.(*ast.CaseClause); ok { + return + } + + closingPos := body.Rbrace + lastStmtOrComment := lastStmt.End() - // We never allow leading whitespace for the first comment. - if lastLeadingComment == nil && p.nodeStart(commentGroup)-1 != blockStartLine { - p.addErrorRange( - openingNodePos, - openingNodePos, - commentGroup.Pos(), - reasonBlockStartsWithWS, - ) + // Empty label statements needs positional adjustment. #92 + if l, ok := lastStmt.(*ast.LabeledStmt); ok { + if _, ok := l.Stmt.(*ast.EmptyStmt); ok { + lastStmtOrComment = lastStmt.Pos() } + } - // 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, - ) + comments := ast.NewCommentMap(w.fset, body, w.file.Comments) + for _, commentGroup := range comments { + for _, comment := range commentGroup { + if comment.End() < closingPos && comment.Pos() > lastStmtOrComment { + lastStmtOrComment = comment.End() } } - - lastLeadingComment = commentGroup } - lastNodePos := openingNodePos - if lastLeadingComment != nil { - lastNodePos = lastLeadingComment.End() - blockStartLine = p.nodeEnd(lastLeadingComment) - } + closingPosLine := w.fset.PositionFor(closingPos, false).Line + lastStmtLine := w.fset.PositionFor(lastStmtOrComment, false).Line - // 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 closingPosLine > lastStmtLine+1 { + w.addError(lastStmtOrComment+1, lastStmtOrComment, closingPos, messageRemoveWhitespace, CheckTrailingWhitespace) } +} - // 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 - } +func (w *WSL) maybeCheckBlock( + node ast.Node, + blockStmt *ast.BlockStmt, + cursor *Cursor, + check CheckType, +) { + w.checkBlock(blockStmt) - 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 - } + if _, ok := w.config.Checks[check]; ok { + cursor.SetChecker(check) + + blockList := []ast.Stmt{} + if check != CheckSwitch && check != CheckTypeSwitch { + blockList = blockStmt.List } - // 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 - } + w.checkCuddlingBlock(node, blockList, cursor, 1) + } +} - trailingComments = append(trailingComments, commentGroup) - } +func (w *WSL) maybeCheckExpr( + node ast.Node, + expr ast.Expr, + cursor *Cursor, + predicate func(ast.Node) (int, bool), + check CheckType, +) { + w.checkExpr(expr, cursor) + + if _, ok := w.config.Checks[check]; ok { + cursor.SetChecker(check) + previousNode := cursor.PreviousNode() + + if n, ok := predicate(previousNode); ok { + w.checkCuddling(node, cursor, n) } + } +} - // 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, - ) - } +// numberOfStatementsAbove will find out how many lines above the cursor's +// current statement there is without any newlines between. +func (w *WSL) numberOfStatementsAbove(cursor *Cursor) int { + defer cursor.Save()() - lastNode = comment - } + statementsWithoutNewlines := 0 + currentStmtStartLine := w.lineFor(cursor.Stmt().Pos()) - if !p.config.AllowTrailingComment && p.nodeEnd(stmt)-1 != p.nodeEnd(lastStatement) { - p.addErrorRange( - blockEndPos, - lastNode.End(), - stmt.End()-1, - reasonBlockEndsWithWS, - ) + for cursor.Previous() { + previousStmtEndLine := w.lineFor(cursor.Stmt().End()) + if previousStmtEndLine != currentStmtStartLine-1 { + break } - return + currentStmtStartLine = w.lineFor(cursor.Stmt().Pos()) + statementsWithoutNewlines++ } - // Nothing to do if we're not looking for enforced newline. - if p.config.ForceCaseTrailingWhitespaceLimit == 0 { - return - } + return statementsWithoutNewlines +} - // 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 - } +func (w *WSL) lineFor(pos token.Pos) int { + return w.fset.PositionFor(pos, false).Line +} - 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 +func (w *WSL) implementsErr(node *ast.Ident) bool { + typeInfo := w.typeInfo.TypeOf(node) + if typeInfo == nil { + return false } - totalRowsInCase := p.nodeEnd(closingNode) - blockStartLine - if totalRowsInCase < p.config.ForceCaseTrailingWhitespaceLimit { - return + errorType, ok := types.Universe.Lookup("error").Type().Underlying().(*types.Interface) + if !ok { + return false } - if p.nodeEnd(closingNode)+1 == p.nodeStart(nextStatement) { - p.addErrorRange( - closingNode.Pos(), - closingNode.End(), - closingNode.End(), - reasonCaseBlockTooCuddly, - ) - } + return types.Implements(typeInfo, errorType) } -func isExampleFunc(ident *ast.Ident) bool { - return ident != nil && strings.HasPrefix(ident.Name, "Example") +func (w *WSL) addErrorInvalidTypeCuddle(pos token.Pos, ct CheckType) { + reportMessage := fmt.Sprintf("%s (invalid statement above %s)", messageMissingWhitespaceAbove, ct) + w.addErrorWithMessage(pos, pos, pos, reportMessage) } -func (p *processor) nodeStart(node ast.Node) int { - return p.fileSet.Position(node.Pos()).Line +func (w *WSL) addErrorTooManyStatements(pos token.Pos, ct CheckType) { + reportMessage := fmt.Sprintf("%s (too many statements above %s)", messageMissingWhitespaceAbove, ct) + w.addErrorWithMessage(pos, pos, pos, reportMessage) } -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 (w *WSL) addErrorNoIntersection(pos token.Pos, ct CheckType) { + reportMessage := fmt.Sprintf("%s (no shared variables above %s)", messageMissingWhitespaceAbove, ct) + w.addErrorWithMessage(pos, pos, pos, reportMessage) } -func isEmptyLabeledStmt(node ast.Node) bool { - v, ok := node.(*ast.LabeledStmt) - if !ok { - return false - } - - _, empty := v.Stmt.(*ast.EmptyStmt) +func (w *WSL) addErrorTooManyLines(pos token.Pos, ct CheckType) { + reportMessage := fmt.Sprintf("%s (too many lines above %s)", messageMissingWhitespaceAbove, ct) + w.addErrorWithMessage(pos, pos, pos, reportMessage) +} - return empty +func (w *WSL) addErrorNeverAllow(pos token.Pos, ct CheckType) { + reportMessage := fmt.Sprintf("%s (never cuddle %s)", messageMissingWhitespaceAbove, ct) + w.addErrorWithMessage(pos, pos, pos, reportMessage) } -func (p *processor) addWhitespaceBeforeError(node ast.Node, reason string) { - p.addErrorRange(node.Pos(), node.Pos(), node.Pos(), reason) +func (w *WSL) addError(report, start, end token.Pos, message string, ct CheckType) { + reportMessage := fmt.Sprintf("%s (%s)", message, ct) + w.addErrorWithMessage(report, start, end, reportMessage) } -func (p *processor) addErrorRange(reportAt, start, end token.Pos, reason string) { - report, ok := p.result[reportAt] +func (w *WSL) addErrorWithMessage(report, start, end token.Pos, message string) { + iss, ok := w.issues[report] if !ok { - report = result{ - reason: reason, - fixRanges: []fix{}, + iss = issue{ + message: message, + fixRanges: []fixRange{}, } } - report.fixRanges = append(report.fixRanges, fix{ + iss.fixRanges = append(iss.fixRanges, fixRange{ fixRangeStart: start, fixRangeEnd: end, }) - p.result[reportAt] = report + w.issues[report] = iss } -func (p *processor) addWarning(w string, pos token.Pos, t interface{}) { - position := p.fileSet.Position(pos) +func hasIntersection(a, b ast.Node) bool { + return len(nodeIdentIntersection(a, b)) > 0 +} + +func nodeIdentIntersection(a, b ast.Node) []*ast.Ident { + aI := identsFromNode(a, true) + bI := identsFromNode(b, true) + + return identIntersection(aI, bI) +} + +func identIntersection(a, b []*ast.Ident) []*ast.Ident { + intersects := []*ast.Ident{} + + for _, as := range a { + for _, bs := range b { + if as.Name == bs.Name { + intersects = append(intersects, as) + } + } + } + + return intersects +} - p.warnings = append(p.warnings, - fmt.Sprintf("%s:%d: %s (%T)", position.Filename, position.Line, w, t), +func identsFromNode(node ast.Node, skipBlock bool) []*ast.Ident { + var ( + idents []*ast.Ident + seen = map[string]struct{}{} ) + + if node == nil { + return idents + } + + ast.Inspect(node, func(n ast.Node) bool { + if skipBlock { + if _, ok := n.(*ast.BlockStmt); ok { + return false + } + } + + if ident, ok := n.(*ast.Ident); ok { + if _, exists := seen[ident.Name]; !exists { + idents = append(idents, ident) + seen[ident.Name] = struct{}{} + } + } + + return true + }) + + return idents } diff --git a/wsl_test.go b/wsl_test.go index 4847e0a..b0cd6a7 100644 --- a/wsl_test.go +++ b/wsl_test.go @@ -1,24 +1,27 @@ package wsl import ( + "os" "path/filepath" "testing" "golang.org/x/tools/go/analysis/analysistest" ) -func TestWIP(t *testing.T) { - testdata := analysistest.TestData() - analyzer := NewAnalyzer(nil) - - analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "wip") -} - func TestDefaultConfig(t *testing.T) { - testdata := analysistest.TestData() - analyzer := NewAnalyzer(nil) + dirs, err := os.ReadDir("./testdata/src/default_config") + if err != nil { + t.Fatal(err) + } + + for _, tc := range dirs { + t.Run(tc.Name(), func(t *testing.T) { + testdata := analysistest.TestData() + analyzer := NewAnalyzer(NewConfig()) - analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "default_config") + analysistest.RunWithSuggestedFixes(t, testdata, analyzer, filepath.Join("default_config", tc.Name())) + }) + } } func TestWithConfig(t *testing.T) { @@ -29,68 +32,62 @@ func TestWithConfig(t *testing.T) { configFn func(*Configuration) }{ { - subdir: "case_blocks", - configFn: func(config *Configuration) { - config.ForceCaseTrailingWhitespaceLimit = 3 - }, - }, - { - subdir: "multi_line_assign", + subdir: "if_errcheck", configFn: func(config *Configuration) { - config.AllowMultiLineAssignCuddle = false + config.Checks.Add(CheckErr) }, }, { - subdir: "assign_and_call", + subdir: "no_check_decl", configFn: func(config *Configuration) { - config.AllowAssignAndCallCuddle = false + config.Checks.Remove(CheckDecl) }, }, { - subdir: "trailing_comments", + subdir: "whole_block", configFn: func(config *Configuration) { - config.AllowTrailingComment = true + config.AllowWholeBlock = true }, }, { - subdir: "separate_leading_whitespace", + subdir: "first_in_block_n1", configFn: func(config *Configuration) { - config.AllowSeparatedLeadingComment = true + config.AllowFirstInBlock = true }, }, { - subdir: "error_check", + subdir: "case_max_lines", configFn: func(config *Configuration) { - config.ForceCuddleErrCheckAndAssign = true + config.CaseMaxLines = 3 }, }, { - subdir: "short_decl", + subdir: "branch_max_lines", configFn: func(config *Configuration) { - config.ForceExclusiveShortDeclarations = true + config.BranchMaxLines = 5 }, }, { - subdir: "strict_append", + subdir: "exclusive_short_decl", configFn: func(config *Configuration) { - config.StrictAppend = false + config.Checks.Add(CheckAssignExclusive) }, }, { - subdir: "assign_and_anything", + subdir: "assign_expr", configFn: func(config *Configuration) { - config.AllowAssignAndAnythingCuddle = true + config.Checks.Add(CheckAssignExpr) }, }, { - subdir: "decl", + subdir: "disable_all", configFn: func(config *Configuration) { - config.AllowCuddleDeclaration = true + config.Checks = NoChecks() }, }, } { t.Run(tc.subdir, func(t *testing.T) { - config := defaultConfig() + config := NewConfig() tc.configFn(config) analyzer := NewAnalyzer(config)