Skip to content

Commit 4a592e7

Browse files
committed
Improve overall performance, better test coverage
1 parent e745d46 commit 4a592e7

File tree

15 files changed

+3380
-134
lines changed

15 files changed

+3380
-134
lines changed

.github/workflows/test.yml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
name: Go Tests
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
branches: [master]
8+
9+
jobs:
10+
test:
11+
name: Test
12+
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
go-version: [1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x]
16+
17+
steps:
18+
- name: Set up Go
19+
uses: actions/setup-go@v2
20+
with:
21+
go-version: ${{ matrix.go-version }}
22+
23+
- name: Check out code
24+
uses: actions/checkout@v2
25+
26+
- name: Get dependencies
27+
run: go get -v -t -d ./...
28+
29+
- name: Build
30+
run: go build -v ./...
31+
32+
- name: Run tests with race detector
33+
run: go test -race -v ./...
34+
35+
- name: Run benchmarks
36+
run: go test -bench=. -run=^$ ./...
37+
38+
lint:
39+
name: Lint
40+
runs-on: ubuntu-latest
41+
steps:
42+
- name: Set up Go
43+
uses: actions/setup-go@v2
44+
with:
45+
go-version: 1.17.x
46+
47+
- name: Check out code
48+
uses: actions/checkout@v2
49+
50+
- name: golangci-lint
51+
uses: golangci/golangci-lint-action@v2
52+
with:
53+
version: latest
54+
55+
coverage:
56+
name: Code coverage
57+
runs-on: ubuntu-latest
58+
steps:
59+
- name: Set up Go
60+
uses: actions/setup-go@v2
61+
with:
62+
go-version: 1.17.x
63+
64+
- name: Check out code
65+
uses: actions/checkout@v2
66+
67+
- name: Run coverage
68+
run: go test -coverprofile=coverage.txt -covermode=atomic ./...
69+
70+
- name: Upload coverage to Codecov
71+
uses: codecov/codecov-action@v2
72+
with:
73+
file: ./coverage.txt
74+
fail_ci_if_error: false

README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Usage:
2323
Flags:
2424
2525
-ignore exclude files matching the given regular expression
26-
-ignore-strings exclude strings matching the given regular expression
26+
-ignore-strings exclude strings matching the given regular expression
2727
-ignore-tests exclude tests from the search (default: true)
2828
-min-occurrences report from how many occurrences (default: 2)
2929
-min-length only report strings with the minimum given length (default: 3)
@@ -43,10 +43,43 @@ Examples:
4343
goconst -min-occurrences 5 $(go list -m -f '{{.Dir}}')
4444
```
4545

46+
### Development
47+
48+
#### Running Tests
49+
50+
The project includes a comprehensive test suite. To run the tests:
51+
52+
```bash
53+
# Run all tests
54+
go test ./...
55+
56+
# Run tests with verbose output
57+
go test -v ./...
58+
59+
# Run tests with race detector
60+
go test -race ./...
61+
62+
# Run benchmarks
63+
go test -bench=. ./...
64+
65+
# Check test coverage
66+
go test -cover ./...
67+
```
68+
69+
#### Contributing
70+
71+
Contributions are welcome! Before submitting a PR:
72+
73+
1. Make sure all tests pass
74+
2. Add tests for new functionality
75+
3. Ensure your code passes linting checks
76+
4. Update documentation as needed
77+
4678
### Other static analysis tools
4779

4880
- [gogetimports](https://github.com/jgautheron/gogetimports): Get a JSON-formatted list of imports.
4981
- [usedexports](https://github.com/jgautheron/usedexports): Find exported variables that could be unexported.
5082

5183
### License
84+
5285
MIT

api.go

Lines changed: 142 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,66 @@ import (
44
"go/ast"
55
"go/token"
66
"strings"
7+
"sync"
78
)
89

10+
// Issue represents a finding of duplicated strings or numbers.
11+
// Each Issue includes the position where it was found, how many times it occurs,
12+
// the string itself, and any matching constant name.
913
type Issue struct {
1014
Pos token.Position
1115
OccurrencesCount int
1216
Str string
1317
MatchingConst string
1418
}
1519

20+
// IssuePool provides a pool of Issue slices to reduce allocations
21+
var IssuePool = sync.Pool{
22+
New: func() interface{} {
23+
return make([]Issue, 0, 100)
24+
},
25+
}
26+
27+
// GetIssueBuffer retrieves an Issue slice from the pool
28+
func GetIssueBuffer() []Issue {
29+
return IssuePool.Get().([]Issue)[:0] // Reset length but keep capacity
30+
}
31+
32+
// PutIssueBuffer returns an Issue slice to the pool
33+
func PutIssueBuffer(issues []Issue) {
34+
// Make sure to clear references before returning to pool
35+
for i := range issues {
36+
issues[i].MatchingConst = ""
37+
issues[i].Str = ""
38+
}
39+
IssuePool.Put(issues[:0]) // Reset length but keep capacity
40+
}
41+
42+
// Config contains all configuration options for the goconst analyzer.
1643
type Config struct {
17-
IgnoreStrings string
18-
IgnoreTests bool
44+
// IgnoreStrings is a regular expression to filter strings
45+
IgnoreStrings string
46+
// IgnoreTests indicates whether test files should be excluded
47+
IgnoreTests bool
48+
// MatchWithConstants enables matching strings with existing constants
1949
MatchWithConstants bool
20-
MinStringLength int
21-
MinOccurrences int
22-
ParseNumbers bool
23-
NumberMin int
24-
NumberMax int
25-
ExcludeTypes map[Type]bool
50+
// MinStringLength is the minimum length a string must have to be reported
51+
MinStringLength int
52+
// MinOccurrences is the minimum number of occurrences required to report a string
53+
MinOccurrences int
54+
// ParseNumbers enables detection of duplicated numbers
55+
ParseNumbers bool
56+
// NumberMin sets the minimum value for reported number matches
57+
NumberMin int
58+
// NumberMax sets the maximum value for reported number matches
59+
NumberMax int
60+
// ExcludeTypes allows excluding specific types of contexts
61+
ExcludeTypes map[Type]bool
2662
}
2763

64+
// Run analyzes the provided AST files for duplicated strings or numbers
65+
// according to the provided configuration.
66+
// It returns a slice of Issue objects containing the findings.
2867
func Run(files []*ast.File, fset *token.FileSet, cfg *Config) ([]Issue, error) {
2968
p := New(
3069
"",
@@ -39,38 +78,117 @@ func Run(files []*ast.File, fset *token.FileSet, cfg *Config) ([]Issue, error) {
3978
cfg.MinOccurrences,
4079
cfg.ExcludeTypes,
4180
)
42-
var issues []Issue
81+
82+
// Pre-allocate slice based on estimated result size
83+
expectedIssues := len(files) * 5 // Assuming average of 5 issues per file
84+
if expectedIssues > 1000 {
85+
expectedIssues = 1000 // Cap at reasonable maximum
86+
}
87+
88+
// Get issue buffer from pool instead of allocating
89+
issueBuffer := GetIssueBuffer()
90+
if cap(issueBuffer) < expectedIssues {
91+
// Only allocate new buffer if existing one is too small
92+
PutIssueBuffer(issueBuffer)
93+
issueBuffer = make([]Issue, 0, expectedIssues)
94+
}
95+
96+
// Process files concurrently
97+
var wg sync.WaitGroup
98+
sem := make(chan struct{}, p.maxConcurrency)
99+
100+
// Create a filtered files slice with capacity hint
101+
filteredFiles := make([]*ast.File, 0, len(files))
102+
103+
// Filter test files first if needed
43104
for _, f := range files {
44105
if p.ignoreTests {
45-
if filename := fset.Position(f.Pos()).Filename; strings.HasSuffix(filename, testSuffix) {
106+
if filename := fset.Position(f.Pos()).Filename; strings.HasSuffix(filename, "_test.go") {
46107
continue
47108
}
48109
}
49-
ast.Walk(&treeVisitor{
50-
fileSet: fset,
51-
packageName: "",
52-
fileName: "",
53-
p: p,
54-
}, f)
110+
filteredFiles = append(filteredFiles, f)
111+
}
112+
113+
// Process each file in parallel
114+
for _, f := range filteredFiles {
115+
wg.Add(1)
116+
sem <- struct{}{} // acquire semaphore
117+
118+
go func(f *ast.File) {
119+
defer func() {
120+
<-sem // release semaphore
121+
wg.Done()
122+
}()
123+
124+
// Use empty interned strings for package/file names
125+
// The visitor logic will set these appropriately
126+
emptyStr := InternString("")
127+
128+
ast.Walk(&treeVisitor{
129+
fileSet: fset,
130+
packageName: emptyStr,
131+
fileName: emptyStr,
132+
p: p,
133+
ignoreRegex: p.ignoreStringsRegex,
134+
}, f)
135+
}(f)
55136
}
137+
138+
wg.Wait()
139+
56140
p.ProcessResults()
57141

58-
for str, item := range p.strs {
59-
fi := item[0]
60-
i := Issue{
142+
// Process each string that passed the filters
143+
p.stringMutex.RLock()
144+
p.stringCountMutex.RLock()
145+
146+
// Get a string buffer from pool instead of allocating
147+
stringKeys := GetStringBuffer()
148+
149+
// Create an array of strings to sort for stable output
150+
for str := range p.strs {
151+
if count := p.stringCount[str]; count >= p.minOccurrences {
152+
stringKeys = append(stringKeys, str)
153+
}
154+
}
155+
156+
// Process strings in a predictable order for stable output
157+
for _, str := range stringKeys {
158+
positions := p.strs[str]
159+
if len(positions) == 0 {
160+
continue
161+
}
162+
163+
// Use the first position as representative
164+
fi := positions[0]
165+
166+
// Create issue using the counted value to avoid recounting
167+
issue := Issue{
61168
Pos: fi.Position,
62-
OccurrencesCount: len(item),
169+
OccurrencesCount: p.stringCount[str],
63170
Str: str,
64171
}
65172

66-
if len(p.consts) != 0 {
173+
// Check for matching constants
174+
if len(p.consts) > 0 {
175+
p.constMutex.RLock()
67176
if cst, ok := p.consts[str]; ok {
68177
// const should be in the same package and exported
69-
i.MatchingConst = cst.Name
178+
issue.MatchingConst = cst.Name
70179
}
180+
p.constMutex.RUnlock()
71181
}
72-
issues = append(issues, i)
182+
183+
issueBuffer = append(issueBuffer, issue)
73184
}
74185

75-
return issues, nil
186+
p.stringCountMutex.RUnlock()
187+
p.stringMutex.RUnlock()
188+
189+
// Return string buffer to pool
190+
PutStringBuffer(stringKeys)
191+
192+
// Don't return the buffer to pool as the caller now owns it
193+
return issueBuffer, nil
76194
}

0 commit comments

Comments
 (0)