@@ -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.
913type 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.
1643type 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.
2867func 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