Skip to content

Commit 3bead6f

Browse files
authored
feat: add file-length-limit rule (#1072)
1 parent 599874c commit 3bead6f

10 files changed

+248
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,7 @@ List of all available rules. The rules ported from `golint` are left unchanged a
547547
| [`enforce-repeated-arg-type-style`](./RULES_DESCRIPTIONS.md#enforce-repeated-arg-type-style) | string (defaults to "any") | Enforces consistent style for repeated argument and/or return value types. | no | no |
548548
| [`max-control-nesting`](./RULES_DESCRIPTIONS.md#max-control-nesting) | int (defaults to 5) | Sets restriction for maximum nesting of control structures. | no | no |
549549
| [`comments-density`](./RULES_DESCRIPTIONS.md#comments-density) | int (defaults to 0) | Enforces a minimum comment / code relation | no | no |
550+
| [`file-length-limit`](./RULES_DESCRIPTIONS.md#file-length-limit) | map (optional)| Enforces a maximum number of lines per file | no | no |
550551

551552
## Configurable rules
552553

RULES_DESCRIPTIONS.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ List of all available rules.
3838
- [errorf](#errorf)
3939
- [exported](#exported)
4040
- [file-header](#file-header)
41+
- [file-length-limit](#file-length-limit)
4142
- [flag-parameter](#flag-parameter)
4243
- [function-length](#function-length)
4344
- [function-result-limit](#function-result-limit)
@@ -501,6 +502,23 @@ Example:
501502
arguments = ["This is the text that must appear at the top of source files."]
502503
```
503504

505+
## file-length-limit
506+
507+
_Description_: This rule enforces a maximum number of lines per file, in order to aid in maintainability and reduce complexity.
508+
509+
_Configuration_:
510+
511+
* `max` (int) a maximum number of lines in a file. Must be non-negative integers. 0 means the rule is disabled (default `0`);
512+
* `skipComments` (bool) if true ignore and do not count lines containing just comments (default `false`);
513+
* `skipBlankLines` (bool) if true ignore and do not count lines made up purely of whitespace (default `false`).
514+
515+
Example:
516+
517+
```toml
518+
[rule.file-length-limit]
519+
arguments = [{max=100,skipComments=true,skipBlankLines=true}]
520+
```
521+
504522
## flag-parameter
505523

506524
_Description_: If a function controls the flow of another by passing it information on what to do, both functions are said to be [control-coupled](https://en.wikipedia.org/wiki/Coupling_(computer_programming)#Procedural_programming).

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ var allRules = append([]lint.Rule{
9696
&rule.EnforceSliceStyleRule{},
9797
&rule.MaxControlNestingRule{},
9898
&rule.CommentsDensityRule{},
99+
&rule.FileLengthLimitRule{},
99100
}, defaultRules...)
100101

101102
var allFormatters = []lint.Formatter{

rule/file-length-limit.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package rule
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"fmt"
7+
"go/ast"
8+
"go/token"
9+
"strings"
10+
"sync"
11+
12+
"github.com/mgechev/revive/lint"
13+
)
14+
15+
// FileLengthLimitRule lints the number of lines in a file.
16+
type FileLengthLimitRule struct {
17+
// max is the maximum number of lines allowed in a file. 0 means the rule is disabled.
18+
max int
19+
// skipComments indicates whether to skip comment lines when counting lines.
20+
skipComments bool
21+
// skipBlankLines indicates whether to skip blank lines when counting lines.
22+
skipBlankLines bool
23+
sync.Mutex
24+
}
25+
26+
// Apply applies the rule to given file.
27+
func (r *FileLengthLimitRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
28+
r.configure(arguments)
29+
30+
if r.max <= 0 {
31+
// when max is negative or 0 the rule is disabled
32+
return nil
33+
}
34+
35+
all := 0
36+
blank := 0
37+
scanner := bufio.NewScanner(bytes.NewReader(file.Content()))
38+
for scanner.Scan() {
39+
all++
40+
if len(bytes.TrimSpace(scanner.Bytes())) == 0 {
41+
blank++
42+
}
43+
}
44+
45+
if err := scanner.Err(); err != nil {
46+
panic(err.Error())
47+
}
48+
49+
lines := all
50+
if r.skipComments {
51+
lines -= countCommentLines(file.AST.Comments)
52+
}
53+
54+
if r.skipBlankLines {
55+
lines -= blank
56+
}
57+
58+
if lines <= r.max {
59+
return nil
60+
}
61+
62+
return []lint.Failure{
63+
{
64+
Category: "code-style",
65+
Confidence: 1,
66+
Position: lint.FailurePosition{
67+
Start: token.Position{
68+
Filename: file.Name,
69+
Line: all,
70+
},
71+
},
72+
Failure: fmt.Sprintf("file length is %d lines, which exceeds the limit of %d", lines, r.max),
73+
},
74+
}
75+
}
76+
77+
func (r *FileLengthLimitRule) configure(arguments lint.Arguments) {
78+
r.Lock()
79+
defer r.Unlock()
80+
81+
if r.max != 0 {
82+
return // already configured
83+
}
84+
85+
if len(arguments) < 1 {
86+
return // use default
87+
}
88+
89+
argKV, ok := arguments[0].(map[string]any)
90+
if !ok {
91+
panic(fmt.Sprintf(`invalid argument to the "file-length-limit" rule. Expecting a k,v map, got %T`, arguments[0]))
92+
}
93+
for k, v := range argKV {
94+
switch k {
95+
case "max":
96+
maxLines, ok := v.(int64)
97+
if !ok || maxLines < 0 {
98+
panic(fmt.Sprintf(`invalid configuration value for max lines in "file-length-limit" rule; need positive int64 but got %T`, arguments[0]))
99+
}
100+
r.max = int(maxLines)
101+
case "skipComments":
102+
skipComments, ok := v.(bool)
103+
if !ok {
104+
panic(fmt.Sprintf(`invalid configuration value for skip comments in "file-length-limit" rule; need bool but got %T`, arguments[1]))
105+
}
106+
r.skipComments = skipComments
107+
case "skipBlankLines":
108+
skipBlankLines, ok := v.(bool)
109+
if !ok {
110+
panic(fmt.Sprintf(`invalid configuration value for skip blank lines in "file-length-limit" rule; need bool but got %T`, arguments[2]))
111+
}
112+
r.skipBlankLines = skipBlankLines
113+
}
114+
}
115+
}
116+
117+
// Name returns the rule name.
118+
func (*FileLengthLimitRule) Name() string {
119+
return "file-length-limit"
120+
}
121+
122+
func countCommentLines(comments []*ast.CommentGroup) int {
123+
count := 0
124+
for _, cg := range comments {
125+
for _, comment := range cg.List {
126+
if len(comment.Text) < 2 {
127+
continue
128+
}
129+
switch comment.Text[1] {
130+
case '/': // single-line comment
131+
count++
132+
case '*': // multi-line comment
133+
count += strings.Count(comment.Text, "\n") + 1
134+
}
135+
}
136+
}
137+
return count
138+
}

test/file-length-limit_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/mgechev/revive/lint"
7+
"github.com/mgechev/revive/rule"
8+
)
9+
10+
func TestFileLengthLimit(t *testing.T) {
11+
testRule(t, "file-length-limit-disabled", &rule.FileLengthLimitRule{}, &lint.RuleConfig{
12+
Arguments: []any{},
13+
})
14+
testRule(t, "file-length-limit-disabled", &rule.FileLengthLimitRule{}, &lint.RuleConfig{
15+
Arguments: []any{map[string]any{"max": int64(0)}},
16+
})
17+
testRule(t, "file-length-limit-disabled", &rule.FileLengthLimitRule{}, &lint.RuleConfig{
18+
Arguments: []any{map[string]any{"skipComments": true, "skipBlankLines": true}},
19+
})
20+
testRule(t, "file-length-limit-9", &rule.FileLengthLimitRule{}, &lint.RuleConfig{
21+
Arguments: []any{map[string]any{"max": int64(9)}},
22+
})
23+
testRule(t, "file-length-limit-7-skip-comments", &rule.FileLengthLimitRule{}, &lint.RuleConfig{
24+
Arguments: []any{map[string]any{"max": int64(7), "skipComments": true}},
25+
})
26+
testRule(t, "file-length-limit-6-skip-blank", &rule.FileLengthLimitRule{}, &lint.RuleConfig{
27+
Arguments: []any{map[string]any{"max": int64(6), "skipBlankLines": true}},
28+
})
29+
testRule(t, "file-length-limit-4-skip-comments-skip-blank", &rule.FileLengthLimitRule{}, &lint.RuleConfig{
30+
Arguments: []any{map[string]any{"max": int64(4), "skipComments": true, "skipBlankLines": true}},
31+
})
32+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package fixtures
2+
3+
import "fmt"
4+
5+
// Foo is a function.
6+
func Foo(a, b int) {
7+
fmt.Println("Hello, world!")
8+
}
9+
10+
// MATCH /file length is 5 lines, which exceeds the limit of 4/
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package fixtures
2+
3+
import "fmt"
4+
5+
// Foo is a function.
6+
func Foo(a, b int) {
7+
fmt.Println("Hello, world!")
8+
}
9+
10+
// MATCH /file length is 7 lines, which exceeds the limit of 6/
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package fixtures
2+
3+
import "fmt"
4+
5+
// Foo is a function.
6+
func Foo(a, b int) {
7+
// This
8+
/* is
9+
a
10+
*/
11+
// a comment.
12+
fmt.Println("Hello, world!")
13+
/*
14+
This is
15+
multiline
16+
comment.
17+
*/
18+
}
19+
20+
// MATCH /file length is 8 lines, which exceeds the limit of 7/

testdata/file-length-limit-9.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package fixtures
2+
3+
import "fmt"
4+
5+
// Foo is a function.
6+
func Foo(a, b int) {
7+
fmt.Println("Hello, world!")
8+
}
9+
10+
// MATCH /file length is 10 lines, which exceeds the limit of 9/
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package fixtures
2+
3+
import "fmt"
4+
5+
// Foo is a function.
6+
func Foo(a, b int) {
7+
fmt.Println("Hello, world!")
8+
}

0 commit comments

Comments
 (0)