Simon Sawert d4b4ad8dfe Update WSL to v1.2.1 (#794)
* Update WSL to v1.2.1

* Add new tests for fixed false positives, don't derive defaults from WSL
2019-10-07 21:22:44 -04:00

10 KiB

WSL - Whitespace Linter

forthebadge forthebadge

Build Status Coverage Status

WSL is a linter that enforces a very non scientific vision of how to make code more readable by enforcing empty lines at the right places.

I think too much code out there is to cuddly and a bit too warm for it's own good, making it harder for other people to read and understand. The linter will warn about newlines in and around blocks, in the beginning of files and other places in the code.

I know this linter is aggressive and a lot of projects I've tested it on have failed miserably. For this linter to be useful at all I want to be open to new ideas, configurations and discussions! Also note that some of the warnings might be bugs or unintentional false positives so I would love an issue to fix, discuss, change or make something configurable!

Usage

Install by using go get -u github.com/bombsimon/wsl/cmd/....

Run with wsl [--no-test] <file1> [files...] or wsl ./package/.... The "..." wildcard is not used like other go commands but instead can only be to a relative or absolute path.

By default, the linter will run on ./... which means all go files in the current path and all subsequent paths, including test files. To disable linting test files, use -n or --no-test.

This linter can also be used as a part of golangci-lint

Rules

Note that this linter doesn't take in consideration the issues that will be fixed with gofmt so ensure that the code is properly formatted.

Never use empty lines

Even though this linter was built to promote the usage of empty lines, there are a few places where they should never be used.

Never use empty lines in the start or end of a block!

Don't

if someBooleanValue {

    fmt.Println("i like starting newlines")
}

if someOtherBooleanValue {
    fmt.Println("also trailing")

}

switch {

case 1:
    fmt.Println("switch is also a block")
}

switch {
case 1:

    fmt.Println("not in a case")
case 2:
    fmt.Println("or at the end")

}

func neverNewlineAfterReturn() {
    return true

}

func notEvenWithComments() {
    return false
    // I just forgot to say this...
}

Do

if someBooleanValue {
    fmt.Println("this is tight and nice")
}

switch {
case 1:
    fmt.Println("no blank lines here")
case 2:
    // Comments are fine here!
    fmt.Println("or here")
}

func returnCuddleded() {
    // My comment goes above the last statement!
    return true
}

Use empty lines

There's an easy way to improve logic and readability by enforcing whitespaces at the right places. Usually this is around blocks and after declarations.

If

If statements should only be cuddled with assignments/declarations of variables used in the condition and only one assignment should be cuddled. Never cuddle if with anything but assignments.

Don't

notConditional := "x"
if somethingElse == "y" {
    fmt.Println("what am i checking?")
}

first := 1
second := 2
third := 3
forever := 4
if forever {
    return true
}

if false {
    fmt.Println("first if")
}
if true {
    fmt.Println("second if is cuddled")
}

Do

val, err := SomeThing()
if err != nil {
    // err is assigned on line above
}

first := 1
second := 2
third := 3
forever := 4

if forever > 3 {
    return fmt.Sprintf("group multiple assignments away from if")
}

// Or separate from your condition.
first := 1
second := 2
third := 3

forever := 4
if forever > 3 {
    return fmt.Sprintf("group multiple assignments away from if")
}

if false {
    // This is one statement
}

if true {
    // This is another one, don't cuddled them!
}

Return

Return should be placed on a separate line from other statement unless the block consists of only two lines (including the return).

Don't

doSomething()
add := 1+2
fmt.Sprintf(add)
return false

if true {
    stmt.X = true
    stmt.Y = false
    return true
}

Do

doSomething()

add := 1+2
fmt.Sprintf(add))

return false

if true {
    stmt.X = "only one line without return, may cuddled"
    return true
}

if thisWorksToo {
    whitespace := true

    return false
}

Branch statement

The same rule as for return

Don't

for i := range make([]int, 5) {
    if i > 2 {
        sendToOne(i)
        sendToSecond(i)
        continue
    }
}

Do

for i := range make([]int, 5) {
    if i > 2 {
        sendToOne(i)
        sendToSecond(i)

        continue
    }

    if statement == "is short" {
        sendToOne(i)
        break
    }
}

Assignment

Assignments may only be cuddled with other assignments.

Don't

assignOne := "one"
callFunc(assignOne)
assignTwo := "two")
callFunc(assignTwo)

if true {
    return
}
assigningClose := "bad"

var x = 2
y := 3

Do

assignOne := "one"
assignTwo := "two")

callFunc(assignOne)
callFunc(assignTwo)

// Or group assignment and call by usage.
assignOne := "one"
callFunc(assignOne)

assignTwo := "two")
callFunc(assignTwo)

if true {
    return
}

notAssigningClose := "not bad"

var x = 2

y := 3

Declarations

Declarations should never be cuddled with anything, not even other declarations.

Don't

var x int
var y int

z := 2
var a

Do

// Group declarations, they'll align nice and tidy!
var (
    x int
    y int
)

z := 2

var a

Expressions

Expressions (function calls) may never be cuddled with declarations or return statements. Expressions may also not be cuddled with assignments if not passed to the expression func.

Don't

var a bool
fmt.Println(a)

foo := true
someFunc(false)

if someBool() {
    fmt.Println("doing things")
}
x.Calling()

Do

var b bool

fmt.Println(b)

foo := true
someFunc(foo)

bar := false

someFunc(true)

if someBool() {
    fmt.Println("doing things")
}

x.Calling()

Ranges

Range statements may only be cuddled with assignments that are used in the range. Just like if statements this only applies if it's a single assignment cuddled and not multiple.

Ranges may also be cuddled with assignments that are used directly in the block as first statement.

Don't

noRangeList := []string{"a", "b", "c"}
for _, x := range anotherList {
    fmt.Println(x)
}

oneList := []int{1, 2, 3}
twoList := []int{4, 5, 6}
for i := range twoList {
    fmt.Println("too much assignments!")
}

myCount := 0
for _, v := range aList {
    fmt.Sprintf("first statement doesn't use assignment")
}

Do

rangeList := []string{"a", "b", "c"}
for _, x := range rangeList {
    fmt.Println(x)
}

oneList := []int{1, 2, 3}

twoList := []int{4, 5, 6}
for i := range twoList {
    fmt.Println("too much assignments!")
}

myCount := 0
for _, v := range aList {
    myCount += v

    fmt.Sprintf("first statement uses cuddled assignment")
}

Defer

Defer is almost handled like return statements but there are cases where grouping defer statements with each other or expression calls may improve readability.

Defer statements may be cuddled with other defer statements as many times as you like. It may also be cuddled with assignments above or expression variables on the line above.

Don't

first := getFirst()
defer first.Close()
second := getSecond() // This will fail
defer second.Close()

first := getFirst()
second := getSecond()
defer first.Close() // Too many assignments above
defer second.Close()

m1.Lock()
defer m2.RUnlock() // Not the expression above

Do

first := getFirst()
second := getSecond()

defer first.Close()
defer second.Close()

// Or group by usage.
first := getFirst()
defer first.Close()

second := getSecond()
defer second.Close()

m.Lock()
defer m.Unlock()

For loops

For statements works similar like ranges except that anonymous (infinite) loops may never be cuddled. Just like the range statement, variables used in the for or in first statement in the body may be cuddled.

Don't

t := true
for notT {
    fmt.Println("where's t used?")
}

n := 0
m := 1
for x < 100 {
    n += x // m not used in for or body
}

n := 1
for {
    fmt.Println("never cuddled for without condition")
}

Do

t := true
for t {
    fmt.Println("t used in for")
}

n := 0
for x < 100 {
    n += x // n used in first block statement.
}

n := 1

for {
    fmt.Println("never cuddled for without condition")
}

Go

Go routines may only be executed if there's a maximum of one assignments above and that assignment is used in the expression.

Don't

first := func() {}
second := func() {}
go second()

notUsed := func() {}
go first()

x := "1"
go func() {
    fmt.Println("where's x used!=")
}()

Do

first := func() {}
go first()

notUsed := func() {}

first := func() {}
go first()

Switch and Type switch

The same rules applies for switch and type switch statements with the exception of anonymous type switches. That means type switches where a new variable is not assigned. It's also allowed to cuddled type switches with variables used if it's used as the first argument in the first case.

Type switches may only be cuddled with one assignment above and if that assignment is used in the switch.

Don't

notSome := SomeInt()
switch some {
case 1:
    fmt.Println("1")
default:
    fmt.Println("not 1")
}

notSwitched := SomeInt()
switch {
case 1 > 2:
    fmt.Println("whitespace between assignments")
}

n := 0
switch v := some.(type):
case typeOne:
    x := v.X // n not used in switch or body
case typeTwo:
    x := v.X
}

Do

some := SomeInt()
switch some {
case 1:
    fmt.Println("1")
default:
    fmt.Println("not 1")
}

notSwitched := SomeInt()

switch {
case 1 > 2:
    fmt.Println("whitespace between assignments")
}

n := 0
switch v := some.(type):
case typeOne:
    n = v.X // n is used first in block, OK to cuddle
case typeTwo:
    n = v.Y
}