diff --git a/config.go b/config.go index 7c3424d..be7569b 100644 --- a/config.go +++ b/config.go @@ -70,6 +70,7 @@ var allRules = append([]lint.Rule{ &rule.RangeValInClosureRule{}, &rule.WaitGroupByValueRule{}, &rule.AtomicRule{}, + &rule.EmptyLinesRule{}, }, defaultRules...) var allFormatters = []lint.Formatter{ diff --git a/fixtures/empty-lines.go b/fixtures/empty-lines.go new file mode 100644 index 0000000..6a4e8df --- /dev/null +++ b/fixtures/empty-lines.go @@ -0,0 +1,59 @@ +// Test of empty-lines. + +package fixtures + +func f1(x *int) bool { // MATCH /extra empty line at the start of a block/ + + return x > 2 +} + +func f2(x *int) bool { + return x > 2 // MATCH /extra empty line at the end of a block/ + +} + +func f3(x *int) bool { // MATCH /extra empty line at the start of a block/ + + return x > 2 // MATCH /extra empty line at the end of a block/ + +} + +func f4(x *int) bool { + // This is fine. + return x > 2 +} + +func f5(x *int) bool { // MATCH /extra empty line at the start of a block/ + + // This is _not_ fine. + return x > 2 +} + +func f6(x *int) bool { + return x > 2 + // This is fine. +} + +func f7(x *int) bool { + return x > 2 // MATCH /extra empty line at the end of a block/ + // This is _not_ fine. + +} + +func f8(*int) bool { + if x > 2 { // MATCH /extra empty line at the start of a block/ + + return true + } + + return false +} + +func f9(*int) bool { + if x > 2 { + return true // MATCH /extra empty line at the end of a block/ + + } + + return false +} diff --git a/lint/file.go b/lint/file.go index ee50dee..c829edb 100644 --- a/lint/file.go +++ b/lint/file.go @@ -56,6 +56,11 @@ func (f *File) Render(x interface{}) string { return buf.String() } +// CommentMap builds a comment map for the file. +func (f *File) CommentMap() ast.CommentMap { + return ast.NewCommentMap(f.Pkg.fset, f.AST, f.AST.Comments) +} + var basicTypeKinds = map[types.BasicKind]string{ types.UntypedBool: "bool", types.UntypedInt: "int", diff --git a/rule/empty-lines.go b/rule/empty-lines.go new file mode 100644 index 0000000..143fb83 --- /dev/null +++ b/rule/empty-lines.go @@ -0,0 +1,113 @@ +package rule + +import ( + "go/ast" + "go/token" + + "github.com/mgechev/revive/lint" +) + +// EmptyLinesRule lints empty lines in blocks. +type EmptyLinesRule struct{} + +// Apply applies the rule to given file. +func (r *EmptyLinesRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure { + var failures []lint.Failure + + onFailure := func(failure lint.Failure) { + failures = append(failures, failure) + } + + w := lintEmptyLines{file, file.CommentMap(), onFailure} + ast.Walk(w, file.AST) + return failures +} + +// Name returns the rule name. +func (r *EmptyLinesRule) Name() string { + return "empty-lines" +} + +type lintEmptyLines struct { + file *lint.File + cmap ast.CommentMap + onFailure func(lint.Failure) +} + +func (w lintEmptyLines) Visit(node ast.Node) ast.Visitor { + block, ok := node.(*ast.BlockStmt) + if !ok { + return w + } + + w.checkStart(block) + w.checkEnd(block) + + return w +} + +func (w lintEmptyLines) checkStart(block *ast.BlockStmt) { + if len(block.List) == 0 { + return + } + + start := w.position(block.Lbrace) + firstNode := block.List[0] + + if w.commentBetween(start, firstNode) { + return + } + + first := w.position(firstNode.Pos()) + if first.Line-start.Line > 1 { + w.onFailure(lint.Failure{ + Confidence: 1, + Node: block, + Category: "style", + URL: "#empty-lines", + Failure: "extra empty line at the start of a block", + }) + } +} + +func (w lintEmptyLines) checkEnd(block *ast.BlockStmt) { + if len(block.List) < 1 { + return + } + + end := w.position(block.Rbrace) + lastNode := block.List[len(block.List)-1] + + if w.commentBetween(end, lastNode) { + return + } + + last := w.position(lastNode.Pos()) + if end.Line-last.Line > 1 { + w.onFailure(lint.Failure{ + Confidence: 1, + Node: lastNode, + Category: "style", + URL: "#empty-lines", + Failure: "extra empty line at the end of a block", + }) + } +} + +func (w lintEmptyLines) commentBetween(position token.Position, node ast.Node) bool { + comments := w.cmap.Filter(node).Comments() + if len(comments) == 0 { + return false + } + + commentStart := w.position(comments[0].Pos()) + if commentStart.Line-position.Line == 1 || commentStart.Line-position.Line == -1 { + return true + } + + return false +} + +func (w lintEmptyLines) position(pos token.Pos) token.Position { + return w.file.ToPosition(pos) +} diff --git a/test/empty-lines_test.go b/test/empty-lines_test.go new file mode 100644 index 0000000..ef27605 --- /dev/null +++ b/test/empty-lines_test.go @@ -0,0 +1,12 @@ +package test + +import ( + "testing" + + "github.com/mgechev/revive/rule" +) + +// TestEmptyLines rule. +func TestEmptyLines(t *testing.T) { + testRule(t, "empty-lines", &rule.EmptyLinesRule{}) +}