Skip to content

Feature Proposal: Add newline-after-block checks to whitespace linter #14

@breml

Description

@breml

Hi!

I'd like to propose extending the whitespace linter with complementary functionality that enforces blank lines after block statements as a new "opt-in" feature. This would pair naturally with whitespace's existing checks that remove unnecessary whitespace inside blocks, creating a comprehensive block-level whitespace management tool.

An implementation (based on go/analysis, including tests) of the proposed linter functionality can be found in newline-after-block.

Background

I am a contributor to multiple projects in the Incus ecosystem like incus, operations-center and migration-manager. All of these projects have the use of a simple linting shell script in common, which is called newline-after-block.sh. This script ensures a newline after each functional block (after if, for, switch/select, etc.). This is done by some sed and grep magic, which has some critical limitations:

  • It does not distinguish between block statements and composite literals, it would incorrectly flag struct/map/slice literals because the detection logic relies on the ending curly brace }.
  • It does not handle nested blocks correctly due to the way empty lines are detected after a closing curly brace, it would fail to detect a missing empty line after two closing curly braces (e.g. nested if)
  • it does not provide automatic fix capability, all corrections need to be done manually.

I created a simple newline-after-block linter in Go using the go/analysis framework, which does the job and resolves the above mentioned shortcomings of the shell script.

Why include in whitespace

Since our projects are also linted using golangci-lint, we would like to get this functionality included, which leaves us with two options:

  1. Ask for inclusion of newline-after-block into golangci-lint directly.
  2. Extend an existing linter with the required functionality.

In regards to option 1, I have already gone this route in the past (bidichk, errchkjson) and from this experience I know, that the maintainers of golangci-lint prefer to reduce duplication by multiple linters. Therefore I checked the existing linters already included in golangci-lint for similar functionality, which left me with two candidates:

  1. whitespace (this linter)
  2. wsl

After a quick analysis of both, I figured, that whitespace is the more natural fit for the following reasons:

  • Both linters manage whitespaces
  • Both linters share the same technical foundation, in particular go/analysis framework and analysistest
  • Both implement SuggestedFixes with TextEdit for automatic corrections
  • Both use analysistest for testing
  • Both operate at the package level

In contrast, wsl does not build on the go/analysis framework and uses a lot of custom logic instead. Inclusion into wsl would require a lot more work and some goals of newline-after-block might even be conflicting with the rules currently present in wsl.

The proposed checks are the natural counterpart to whitespace's existing functionality without overlap or conflict.

Proposed Checks

Newline After Block Statements

Enforce blank lines after these block statements (when not at the end of a statement list):

  • if statements (only when not followed by else)
  • for loops
  • range loops
  • switch statements
  • type switch statements
  • select statements

Newline Between Case Clauses

Enforce blank lines between non-empty case blocks within switch, type switch, and select statements (except before the closing brace).

Examples of What Would Be Detected

Example 1: Missing newline after if block

// Would be flagged
func process(condition bool) {
    if condition {
        doSomething()
    } // want "missing newline after block statement"
    nextStatement()
}

// After auto-fix
func process(condition bool) {
    if condition {
        doSomething()
    }

    nextStatement()
}

Example 2: Missing newline after for loop

// Would be flagged
func iterate() {
    for i := 0; i < 10; i++ {
        process(i)
    } // want "missing newline after block statement"
    fmt.Println("done")
}

// After auto-fix
func iterate() {
    for i := 0; i < 10; i++ {
        process(i)
    }

    fmt.Println("done")
}

Example 3: Missing newline after switch statement

// Would be flagged
func handleValue(v int) {
    switch v {
    case 1:
        handleOne()

    case 2:
        handleTwo()
    } // want "missing newline after block statement"
    cleanup()
}

// After auto-fix
func handleValue(v int) {
    switch v {
    case 1:
        handleOne()

    case 2:
        handleTwo()
    }

    cleanup()
}

Example 4: Missing newlines between case clauses

// Would be flagged
func handleSwitch(v int) {
    switch v {
    case 1:
        handleOne() // want "missing newline after case block"
    case 2:
        handleTwo() // want "missing newline after case block"
    default:
        handleDefault()
    }
}

// After auto-fix
func handleSwitch(v int) {
    switch v {
    case 1:
        handleOne()

    case 2:
        handleTwo()

    default:
        handleDefault()
    }
}

Example 5: Correctly ignored cases

// Block at end of function
func endOfFunc() {
    if condition {
        doSomething()
    }
}

// if-else chain
func withElse() {
    if condition {
        doSomething()
    } else {
        doSomethingElse()
    }
}

// Composite literal (not a block statement)
func structInit() {
    p := Person{
        Name: "Alice",
        Age:  30,
    }
    fmt.Println(p)
}

// Empty case (no newline needed)
func emptyCase(v int) {
    switch v {
    case 1:
    case 2:
        handleTwo()
    }
}

For more examples, check out the test cases.

Suggested Configuration Options

Following whitespace's pattern with -multi-if and -multi-func, I suggest adding:

  • -newline-after-block, Enforce newline after blocks (if, for, switch/select) (default: false for backwards compatibility)
  • -newline-between-cases, Enforce newlines between case clauses in switch/select (default: false for backwards compatibility)

Potential Future Extensions

  • Rules about newlines around defer (I prefer to have a newline after a defer statement. In most cases, I don't want to have a newline before defer, but this is not a strict rule. But it should be allowed to omit the newline after a block if defer is used right after the block (e.g. in the case, open file, check error, defer close, where these 3 parts belong together).

Final thoughs

I hope, this proposal finds your interest and we can work together on the inclusion of this functionality in the whitespace linter.

CC: @stgraber, @ldez

Edits:

  • wsl does also use go/analysis

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions