diff --git a/README.md b/README.md index 660c13d..431c84d 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,22 @@ Whitespace is a linter that checks for unnecessary newlines at the start and end To install as a standalone linter, run `go install github.com/ultraware/whitespace/cmd/whitespace@latest`. Whitespace is also included in [golangci-lint](https://github.com/golangci/golangci-lint/). Install it and enable whitespace. + +## Usage + +``` + +Example: ./whitespace ./... + +Usage: whitespace [-flag] [package] + +Configuration flags: + -ignore-leading + Do not check leading newlines + -ignore-trailing + Do not check trailing newlines + -multi-func + Check that multi line functions have a leading newline + -multi-if + Check that multi line if-statements have a leading newline +``` diff --git a/go.mod b/go.mod index 94a9572..0433176 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/ultraware/whitespace -go 1.20 +go 1.24.0 -require golang.org/x/tools v0.20.0 +require golang.org/x/tools v0.38.0 require ( - golang.org/x/mod v0.17.0 // indirect - golang.org/x/sync v0.7.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/sync v0.17.0 // indirect ) diff --git a/go.sum b/go.sum index 9e7e462..9b34f09 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= diff --git a/testdata/src/whitespace_no_leading/no_leading.go b/testdata/src/whitespace_no_leading/no_leading.go new file mode 100644 index 0000000..f978902 --- /dev/null +++ b/testdata/src/whitespace_no_leading/no_leading.go @@ -0,0 +1,168 @@ +package whitespace + +import "fmt" + +func fn1( + arg1 int, + arg2 int, +) { + + fmt.Println("Hello, World") + + if true && + false { + + fmt.Println("Hello, World") + } + + _ = func( + arg1 int, + arg2 int, + ) { + + fmt.Println("Hello, World") + } + + _ = func( + arg1 int, + arg2 int, + ) { + _ = func( + arg1 int, + arg2 int, + ) { + + fmt.Println("Hello, World") + } + } +} + +func fn2( + arg1 int, + arg2 int, +) { + + // A comment. + fmt.Println("Hello, World") +} + +func fn3( + arg1 int, + arg2 int, +) { + + + + // A comment. + fmt.Println("Hello, World") + + if true { + + + + fmt.Println("No comments") + + + } // want "unnecessary trailing newline" + // Also at end + + + +} // want "unnecessary trailing newline" + +// Regular func (FuncDecl) that's not `gofmt`:ed. +func fn4() { + + + + + fmt.Println("Hello, World") + + if true { + + + + fmt.Println("No comments") + + + } // want "unnecessary trailing newline" + + + + +} // want "unnecessary trailing newline" + +// Regular func (FuncDecl) that's not `gofmt`:ed. with comments +func fn5() { + // A comment that should still exist after this + // This one should also still exist + + + + fmt.Println("Hello, World") + + if true { + // A comment that should still exist after this + // This one should also still exist + + + + fmt.Println("No comments") + + + } // want "unnecessary trailing newline" + + + + +} // want "unnecessary trailing newline" + +// Regular func (FuncDecl) that's not `gofmt`:ed. with comment blocks +func fn6() { + /* + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Curabitur ornare dolor at nulla ultrices cursus. + Mauris pharetra metus ac condimentum sodales. + Fusce viverra libero vitae tellus dictum, sed congue risus sodales. + */ + + + + fmt.Println("Hello, World") + + if true { + /* + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Curabitur ornare dolor at nulla ultrices cursus. + Mauris pharetra metus ac condimentum sodales. + Fusce viverra libero vitae tellus dictum, sed congue risus sodales. + */ + + + fmt.Println("No comments") + + + } // want "unnecessary trailing newline" + + + + +} // want "unnecessary trailing newline" + +func fn7() { /* Multiline spaning 1 line */ + + fmt.Println("No comments") +} + +func fn8() { /* Multiline spaning + over several lines */ + + fmt.Println("No comments") +} + +func fn9() { /* Multiline spaning + + over several lines */ + + fmt.Println("No comments") +} diff --git a/testdata/src/whitespace_no_leading/no_leading.go.golden b/testdata/src/whitespace_no_leading/no_leading.go.golden new file mode 100644 index 0000000..548272a --- /dev/null +++ b/testdata/src/whitespace_no_leading/no_leading.go.golden @@ -0,0 +1,138 @@ +package whitespace + +import "fmt" + +func fn1( + arg1 int, + arg2 int, +) { + + fmt.Println("Hello, World") + + if true && + false { + + fmt.Println("Hello, World") + } + + _ = func( + arg1 int, + arg2 int, + ) { + + fmt.Println("Hello, World") + } + + _ = func( + arg1 int, + arg2 int, + ) { + _ = func( + arg1 int, + arg2 int, + ) { + + fmt.Println("Hello, World") + } + } +} + +func fn2( + arg1 int, + arg2 int, +) { + + // A comment. + fmt.Println("Hello, World") +} + +func fn3( + arg1 int, + arg2 int, +) { + + + + // A comment. + fmt.Println("Hello, World") + + if true { + + + + fmt.Println("No comments") + } // want "unnecessary trailing newline" + // Also at end +} // want "unnecessary trailing newline" + +// Regular func (FuncDecl) that's not `gofmt`:ed. +func fn4() { + + + + + fmt.Println("Hello, World") + + if true { + + + + fmt.Println("No comments") + } // want "unnecessary trailing newline" +} // want "unnecessary trailing newline" + +// Regular func (FuncDecl) that's not `gofmt`:ed. with comments +func fn5() { + // A comment that should still exist after this + // This one should also still exist + + fmt.Println("Hello, World") + + if true { + // A comment that should still exist after this + // This one should also still exist + + fmt.Println("No comments") + } // want "unnecessary trailing newline" +} // want "unnecessary trailing newline" + +// Regular func (FuncDecl) that's not `gofmt`:ed. with comment blocks +func fn6() { + /* + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Curabitur ornare dolor at nulla ultrices cursus. + Mauris pharetra metus ac condimentum sodales. + Fusce viverra libero vitae tellus dictum, sed congue risus sodales. + */ + + fmt.Println("Hello, World") + + if true { + /* + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Curabitur ornare dolor at nulla ultrices cursus. + Mauris pharetra metus ac condimentum sodales. + Fusce viverra libero vitae tellus dictum, sed congue risus sodales. + */ + + fmt.Println("No comments") + } // want "unnecessary trailing newline" +} // want "unnecessary trailing newline" + +func fn7() { /* Multiline spaning 1 line */ + + fmt.Println("No comments") +} + +func fn8() { /* Multiline spaning + over several lines */ + + fmt.Println("No comments") +} + +func fn9() { /* Multiline spaning + + over several lines */ + + fmt.Println("No comments") +} diff --git a/testdata/src/whitespace_no_trailing/no_trailing.go b/testdata/src/whitespace_no_trailing/no_trailing.go new file mode 100644 index 0000000..78d0ce9 --- /dev/null +++ b/testdata/src/whitespace_no_trailing/no_trailing.go @@ -0,0 +1,168 @@ +package whitespace + +import "fmt" + +func fn1( + arg1 int, + arg2 int, +) { // want "unnecessary leading newline" + + fmt.Println("Hello, World") + + if true && + false { // want "unnecessary leading newline" + + fmt.Println("Hello, World") + } + + _ = func( + arg1 int, + arg2 int, + ) { // want "unnecessary leading newline" + + fmt.Println("Hello, World") + } + + _ = func( + arg1 int, + arg2 int, + ) { + _ = func( + arg1 int, + arg2 int, + ) { // want "unnecessary leading newline" + + fmt.Println("Hello, World") + } + } +} + +func fn2( + arg1 int, + arg2 int, +) { // want "unnecessary leading newline" + + // A comment. + fmt.Println("Hello, World") +} + +func fn3( + arg1 int, + arg2 int, +) { // want "unnecessary leading newline" + + + + // A comment. + fmt.Println("Hello, World") + + if true { // want "unnecessary leading newline" + + + + fmt.Println("No comments") + + + } + // Also at end + + + +} + +// Regular func (FuncDecl) that's not `gofmt`:ed. +func fn4() { // want "unnecessary leading newline" + + + + + fmt.Println("Hello, World") + + if true { // want "unnecessary leading newline" + + + + fmt.Println("No comments") + + + } + + + + +} + +// Regular func (FuncDecl) that's not `gofmt`:ed. with comments +func fn5() { + // A comment that should still exist after this + // This one should also still exist + + + + fmt.Println("Hello, World") + + if true { + // A comment that should still exist after this + // This one should also still exist + + + + fmt.Println("No comments") + + + } + + + + +} + +// Regular func (FuncDecl) that's not `gofmt`:ed. with comment blocks +func fn6() { + /* + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Curabitur ornare dolor at nulla ultrices cursus. + Mauris pharetra metus ac condimentum sodales. + Fusce viverra libero vitae tellus dictum, sed congue risus sodales. + */ + + + + fmt.Println("Hello, World") + + if true { + /* + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Curabitur ornare dolor at nulla ultrices cursus. + Mauris pharetra metus ac condimentum sodales. + Fusce viverra libero vitae tellus dictum, sed congue risus sodales. + */ + + + fmt.Println("No comments") + + + } + + + + +} + +func fn7() { /* Multiline spaning 1 line */ // want "unnecessary leading newline" + + fmt.Println("No comments") +} + +func fn8() { /* Multiline spaning + over several lines */ + + fmt.Println("No comments") +} + +func fn9() { /* Multiline spaning + + over several lines */ + + fmt.Println("No comments") +} diff --git a/testdata/src/whitespace_no_trailing/no_trailing.go.golden b/testdata/src/whitespace_no_trailing/no_trailing.go.golden new file mode 100644 index 0000000..70710aa --- /dev/null +++ b/testdata/src/whitespace_no_trailing/no_trailing.go.golden @@ -0,0 +1,142 @@ +package whitespace + +import "fmt" + +func fn1( + arg1 int, + arg2 int, +) { // want "unnecessary leading newline" + fmt.Println("Hello, World") + + if true && + false { // want "unnecessary leading newline" + fmt.Println("Hello, World") + } + + _ = func( + arg1 int, + arg2 int, + ) { // want "unnecessary leading newline" + fmt.Println("Hello, World") + } + + _ = func( + arg1 int, + arg2 int, + ) { + _ = func( + arg1 int, + arg2 int, + ) { // want "unnecessary leading newline" + fmt.Println("Hello, World") + } + } +} + +func fn2( + arg1 int, + arg2 int, +) { // want "unnecessary leading newline" + // A comment. + fmt.Println("Hello, World") +} + +func fn3( + arg1 int, + arg2 int, +) { // want "unnecessary leading newline" + // A comment. + fmt.Println("Hello, World") + + if true { // want "unnecessary leading newline" + fmt.Println("No comments") + + + } + // Also at end + + + +} + +// Regular func (FuncDecl) that's not `gofmt`:ed. +func fn4() { // want "unnecessary leading newline" + fmt.Println("Hello, World") + + if true { // want "unnecessary leading newline" + fmt.Println("No comments") + + + } + + + + +} + +// Regular func (FuncDecl) that's not `gofmt`:ed. with comments +func fn5() { + // A comment that should still exist after this + // This one should also still exist + + fmt.Println("Hello, World") + + if true { + // A comment that should still exist after this + // This one should also still exist + + fmt.Println("No comments") + + + } + + + + +} + +// Regular func (FuncDecl) that's not `gofmt`:ed. with comment blocks +func fn6() { + /* + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Curabitur ornare dolor at nulla ultrices cursus. + Mauris pharetra metus ac condimentum sodales. + Fusce viverra libero vitae tellus dictum, sed congue risus sodales. + */ + + fmt.Println("Hello, World") + + if true { + /* + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Curabitur ornare dolor at nulla ultrices cursus. + Mauris pharetra metus ac condimentum sodales. + Fusce viverra libero vitae tellus dictum, sed congue risus sodales. + */ + + fmt.Println("No comments") + + + } + + + + +} + +func fn7() { /* Multiline spaning 1 line */ // want "unnecessary leading newline" + fmt.Println("No comments") +} + +func fn8() { /* Multiline spaning + over several lines */ + + fmt.Println("No comments") +} + +func fn9() { /* Multiline spaning + + over several lines */ + + fmt.Println("No comments") +} diff --git a/whitespace.go b/whitespace.go index 44e6812..79eae70 100644 --- a/whitespace.go +++ b/whitespace.go @@ -13,6 +13,9 @@ import ( type Settings struct { MultiIf bool MultiFunc bool + + IgnoreLeading bool + IgnoreTrailing bool } // NewAnalyzer creates a new whitespace analyzer. @@ -37,6 +40,8 @@ func flags(settings *Settings) flag.FlagSet { flags := flag.NewFlagSet("", flag.ExitOnError) flags.BoolVar(&settings.MultiIf, "multi-if", settings.MultiIf, "Check that multi line if-statements have a leading newline") flags.BoolVar(&settings.MultiFunc, "multi-func", settings.MultiFunc, "Check that multi line functions have a leading newline") + flags.BoolVar(&settings.IgnoreLeading, "ignore-leading", settings.IgnoreLeading, "Do not check leading newlines") + flags.BoolVar(&settings.IgnoreTrailing, "ignore-trailing", settings.IgnoreTrailing, "Do not check trailing newlines") return *flags } @@ -109,26 +114,30 @@ func (v *visitor) Visit(node ast.Node) ast.Visitor { } opening, first, last := firstAndLast(comments, v.fset, stmt) - startMsg := checkStart(v.fset, opening, first) - - if wantNewline && startMsg == nil && len(stmt.List) >= 1 { - v.messages = append(v.messages, analysis.Diagnostic{ - Pos: opening, - Message: "multi-line statement should be followed by a newline", - SuggestedFixes: []analysis.SuggestedFix{{ - TextEdits: []analysis.TextEdit{{ - Pos: stmt.List[0].Pos(), - End: stmt.List[0].Pos(), - NewText: []byte("\n"), + + if !v.settings.IgnoreLeading { + startMsg := checkStart(v.fset, opening, first) + if wantNewline && startMsg == nil && len(stmt.List) >= 1 { + v.messages = append(v.messages, analysis.Diagnostic{ + Pos: opening, + Message: "multi-line statement should be followed by a newline", + SuggestedFixes: []analysis.SuggestedFix{{ + TextEdits: []analysis.TextEdit{{ + Pos: stmt.List[0].Pos(), + End: stmt.List[0].Pos(), + NewText: []byte("\n"), + }}, }}, - }}, - }) - } else if !wantNewline && startMsg != nil { - v.messages = append(v.messages, *startMsg) + }) + } else if !wantNewline && startMsg != nil { + v.messages = append(v.messages, *startMsg) + } } - if msg := checkEnd(v.fset, stmt.Rbrace, last); msg != nil { - v.messages = append(v.messages, *msg) + if !v.settings.IgnoreTrailing { + if msg := checkEnd(v.fset, stmt.Rbrace, last); msg != nil { + v.messages = append(v.messages, *msg) + } } } diff --git a/whitespace_test.go b/whitespace_test.go index 8730853..3a9164d 100644 --- a/whitespace_test.go +++ b/whitespace_test.go @@ -25,3 +25,21 @@ func TestNoMultiline(t *testing.T) { analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "whitespace_no_multiline") } + +func TestNoLeading(t *testing.T) { + testdata := analysistest.TestData() + analyzer := NewAnalyzer(&Settings{ + IgnoreLeading: true, + }) + + analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "whitespace_no_leading") +} + +func TestNoTrailing(t *testing.T) { + testdata := analysistest.TestData() + analyzer := NewAnalyzer(&Settings{ + IgnoreTrailing: true, + }) + + analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "whitespace_no_trailing") +}