Skip to content

Commit cb090a4

Browse files
committed
Bump constant matching coverage
1 parent d8efeb9 commit cb090a4

File tree

3 files changed

+319
-8
lines changed

3 files changed

+319
-8
lines changed

integration_test.go

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ func TestIntegrationWithTestdata(t *testing.T) {
1717
findDuplicates bool
1818
minOccurrences int
1919
expectedStrings int
20+
expectedMatches map[string]string // string -> expected matching constant
2021
}{
2122
{
2223
name: "basic duplicate string detection",
@@ -26,7 +27,7 @@ func TestIntegrationWithTestdata(t *testing.T) {
2627
numbers: false,
2728
minLength: 3,
2829
minOccurrences: 2,
29-
expectedStrings: 3, // "should be constant", "another duplicate", "test context"
30+
expectedStrings: 7, // All strings that appear at least twice
3031
},
3132
{
3233
name: "match with constants",
@@ -36,7 +37,13 @@ func TestIntegrationWithTestdata(t *testing.T) {
3637
numbers: false,
3738
minLength: 3,
3839
minOccurrences: 2,
39-
expectedStrings: 3, // same strings, but one matches a constant
40+
expectedStrings: 7, // All strings that appear at least twice
41+
expectedMatches: map[string]string{
42+
"single constant": "SingleConst",
43+
"grouped constant": "GroupedConst1",
44+
"duplicate value": "DuplicateConst1",
45+
"special\nvalue\twith\rchars": "SpecialConst",
46+
},
4047
},
4148
{
4249
name: "include numbers",
@@ -46,7 +53,7 @@ func TestIntegrationWithTestdata(t *testing.T) {
4653
numbers: true,
4754
minLength: 3,
4855
minOccurrences: 2,
49-
expectedStrings: 4, // the 3 strings + "12345"
56+
expectedStrings: 8, // All strings + "12345"
5057
},
5158
{
5259
name: "filter by number range",
@@ -58,7 +65,7 @@ func TestIntegrationWithTestdata(t *testing.T) {
5865
numberMax: 1000,
5966
minLength: 3,
6067
minOccurrences: 2,
61-
expectedStrings: 3, // 12345 should be filtered out
68+
expectedStrings: 7, // All strings, 12345 should be filtered out
6269
},
6370
{
6471
name: "higher minimum occurrences",
@@ -68,7 +75,7 @@ func TestIntegrationWithTestdata(t *testing.T) {
6875
numbers: false,
6976
minLength: 3,
7077
minOccurrences: 5, // higher than any string in our testdata
71-
expectedStrings: 1, // "test context" appears 5 times
78+
expectedStrings: 1, // "test context" appears exactly 5 times
7279
},
7380
}
7481

@@ -89,7 +96,7 @@ func TestIntegrationWithTestdata(t *testing.T) {
8996
map[Type]bool{},
9097
)
9198

92-
strs, _, err := p.ParseTree()
99+
strs, consts, err := p.ParseTree()
93100
if err != nil {
94101
t.Fatalf("ParseTree() error = %v", err)
95102
}
@@ -100,6 +107,25 @@ func TestIntegrationWithTestdata(t *testing.T) {
100107
t.Logf("Found: %q with %d occurrences", str, len(occurrences))
101108
}
102109
}
110+
111+
// Verify constant matches if expected
112+
if tt.expectedMatches != nil {
113+
for str, wantConst := range tt.expectedMatches {
114+
foundConsts, ok := consts[str]
115+
if !ok {
116+
t.Errorf("String %q not found in constants map", str)
117+
continue
118+
}
119+
if len(foundConsts) == 0 {
120+
t.Errorf("No constants found for string %q", str)
121+
continue
122+
}
123+
if foundConsts[0].Name != wantConst {
124+
t.Errorf("String %q matched with constant %q, want %q",
125+
str, foundConsts[0].Name, wantConst)
126+
}
127+
}
128+
}
103129
})
104130
}
105131
}
@@ -113,12 +139,12 @@ func TestIntegrationExcludeTypes(t *testing.T) {
113139
{
114140
name: "no exclusions",
115141
excludeTypes: map[Type]bool{},
116-
expectedStrings: 3,
142+
expectedStrings: 7, // All strings that appear at least twice
117143
},
118144
{
119145
name: "exclude assignments",
120146
excludeTypes: map[Type]bool{Assignment: true},
121-
expectedStrings: 1, // After excluding assignments, only "test context" remains
147+
expectedStrings: 3, // After excluding assignments, only "test context", "grouped constant", and "matched" remain
122148
},
123149
{
124150
name: "exclude all types",

match_constant_test.go

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
package goconst
2+
3+
import (
4+
"go/ast"
5+
"go/parser"
6+
"go/token"
7+
"testing"
8+
)
9+
10+
func TestMatchConstant(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
code string
14+
wantIssues int
15+
wantMatches map[string]string // string -> matching const name
16+
}{
17+
{
18+
name: "basic constant match",
19+
code: `package example
20+
const MyConst = "test"
21+
func example() {
22+
str := "test"
23+
}`,
24+
wantIssues: 1,
25+
wantMatches: map[string]string{
26+
"test": "MyConst",
27+
},
28+
},
29+
{
30+
name: "multiple constants same value",
31+
code: `package example
32+
const (
33+
FirstConst = "test"
34+
SecondConst = "test"
35+
)
36+
func example() {
37+
str := "test"
38+
}`,
39+
wantIssues: 1,
40+
wantMatches: map[string]string{
41+
"test": "FirstConst", // Should match the first one defined
42+
},
43+
},
44+
{
45+
name: "constant after usage",
46+
code: `package example
47+
func example() {
48+
str := "test"
49+
}
50+
const MyConst = "test"`,
51+
wantIssues: 1,
52+
wantMatches: map[string]string{
53+
"test": "MyConst",
54+
},
55+
},
56+
{
57+
name: "constant in init",
58+
code: `package example
59+
func init() {
60+
const InitConst = "test"
61+
}
62+
func example() {
63+
str := "test"
64+
}`,
65+
wantIssues: 1,
66+
wantMatches: map[string]string{
67+
"test": "InitConst",
68+
},
69+
},
70+
{
71+
name: "constant in different scope",
72+
code: `package example
73+
const GlobalConst = "global"
74+
func example() {
75+
const localConst = "local"
76+
str1 := "global"
77+
str2 := "local"
78+
}`,
79+
wantIssues: 2,
80+
wantMatches: map[string]string{
81+
"global": "GlobalConst",
82+
"local": "localConst",
83+
},
84+
},
85+
{
86+
name: "exported vs unexported constants",
87+
code: `package example
88+
const (
89+
ExportedConst = "exported"
90+
unexportedConst = "unexported"
91+
)
92+
func example() {
93+
str1 := "exported"
94+
str2 := "unexported"
95+
}`,
96+
wantIssues: 2,
97+
wantMatches: map[string]string{
98+
"exported": "ExportedConst",
99+
"unexported": "unexportedConst",
100+
},
101+
},
102+
{
103+
name: "string matches multiple constants",
104+
code: `package example
105+
const (
106+
Const1 = "duplicate"
107+
Const2 = "duplicate"
108+
Const3 = "duplicate"
109+
)
110+
func example() {
111+
str := "duplicate"
112+
}`,
113+
wantIssues: 1,
114+
wantMatches: map[string]string{
115+
"duplicate": "Const1", // Should match the first one defined
116+
},
117+
},
118+
{
119+
name: "constant with special characters",
120+
code: `package example
121+
const SpecialConst = "test\nwith\tspecial\rchars"
122+
func example() {
123+
str := "test\nwith\tspecial\rchars"
124+
}`,
125+
wantIssues: 1,
126+
wantMatches: map[string]string{
127+
"test\nwith\tspecial\rchars": "SpecialConst",
128+
},
129+
},
130+
}
131+
132+
for _, tt := range tests {
133+
t.Run(tt.name, func(t *testing.T) {
134+
fset := token.NewFileSet()
135+
f, err := parser.ParseFile(fset, "example.go", tt.code, 0)
136+
if err != nil {
137+
t.Fatalf("Failed to parse test code: %v", err)
138+
}
139+
140+
config := &Config{
141+
MinStringLength: 1, // Set to 1 to catch all strings
142+
MinOccurrences: 1, // Set to 1 to catch all occurrences
143+
MatchWithConstants: true,
144+
}
145+
146+
issues, err := Run([]*ast.File{f}, fset, config)
147+
if err != nil {
148+
t.Fatalf("Run() error = %v", err)
149+
}
150+
151+
if len(issues) != tt.wantIssues {
152+
t.Errorf("Got %d issues, want %d", len(issues), tt.wantIssues)
153+
for _, issue := range issues {
154+
t.Logf("Found issue: %q matches constant %q", issue.Str, issue.MatchingConst)
155+
}
156+
}
157+
158+
// Verify constant matches
159+
for _, issue := range issues {
160+
if wantConst, ok := tt.wantMatches[issue.Str]; ok {
161+
if issue.MatchingConst != wantConst {
162+
t.Errorf("String %q matched with constant %q, want %q",
163+
issue.Str, issue.MatchingConst, wantConst)
164+
}
165+
} else {
166+
t.Errorf("Unexpected string found: %q", issue.Str)
167+
}
168+
}
169+
})
170+
}
171+
}
172+
173+
func TestMatchConstantMultiFile(t *testing.T) {
174+
// Test constant matching across multiple files
175+
files := map[string]string{
176+
"const.go": `package example
177+
const (
178+
SharedConst = "shared"
179+
PackageConst = "package"
180+
)`,
181+
"main.go": `package example
182+
func main() {
183+
str1 := "shared"
184+
str2 := "package"
185+
}`,
186+
}
187+
188+
fset := token.NewFileSet()
189+
astFiles := make([]*ast.File, 0, len(files))
190+
191+
for name, content := range files {
192+
f, err := parser.ParseFile(fset, name, content, 0)
193+
if err != nil {
194+
t.Fatalf("Failed to parse %s: %v", name, err)
195+
}
196+
astFiles = append(astFiles, f)
197+
}
198+
199+
config := &Config{
200+
MinStringLength: 1,
201+
MinOccurrences: 1,
202+
MatchWithConstants: true,
203+
}
204+
205+
issues, err := Run(astFiles, fset, config)
206+
if err != nil {
207+
t.Fatalf("Run() error = %v", err)
208+
}
209+
210+
wantMatches := map[string]string{
211+
"shared": "SharedConst",
212+
"package": "PackageConst",
213+
}
214+
215+
if len(issues) != len(wantMatches) {
216+
t.Errorf("Got %d issues, want %d", len(issues), len(wantMatches))
217+
}
218+
219+
for _, issue := range issues {
220+
if wantConst, ok := wantMatches[issue.Str]; ok {
221+
if issue.MatchingConst != wantConst {
222+
t.Errorf("String %q matched with constant %q, want %q",
223+
issue.Str, issue.MatchingConst, wantConst)
224+
}
225+
} else {
226+
t.Errorf("Unexpected string found: %q", issue.Str)
227+
}
228+
}
229+
}

testdata/match_constant.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package testdata
2+
3+
// Constants in different forms
4+
const SingleConst = "single constant"
5+
6+
const (
7+
// Grouped constants
8+
GroupedConst1 = "grouped constant"
9+
GroupedConst2 = "another grouped"
10+
11+
// Constants with the same value
12+
DuplicateConst1 = "duplicate value"
13+
DuplicateConst2 = "duplicate value"
14+
15+
// Constants with special characters
16+
SpecialConst = "special\nvalue\twith\rchars"
17+
)
18+
19+
// Constants in different scopes
20+
func scopedConstants() {
21+
const LocalConst = "local constant"
22+
str := "local constant" // Should match LocalConst
23+
24+
if true {
25+
const BlockConst = "block constant"
26+
str := "block constant" // Should match BlockConst
27+
}
28+
}
29+
30+
// Usage of constants from different contexts
31+
func useConstants() {
32+
// Assignment context
33+
str1 := "single constant" // Should match SingleConst
34+
str2 := "grouped constant" // Should match GroupedConst1
35+
str3 := "duplicate value" // Should match DuplicateConst1 (first defined)
36+
str4 := "special\nvalue\twith\rchars" // Should match SpecialConst
37+
38+
// Binary expression context
39+
if str1 == "single constant" {
40+
println("matched")
41+
}
42+
43+
// Case statement context
44+
switch str2 {
45+
case "grouped constant":
46+
println("matched")
47+
}
48+
49+
// Function call context
50+
println("duplicate value")
51+
52+
// Return statement context
53+
func() string {
54+
return "grouped constant"
55+
}()
56+
}

0 commit comments

Comments
 (0)