Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Flags:
-min-occurrences report from how many occurrences (default: 2)
-min-length only report strings with the minimum given length (default: 3)
-match-constant look for existing constants matching the values
-find-duplicates look for constants with identical values
-numbers search also for duplicated numbers
-min minimum value, only works with -numbers
-max maximum value, only works with -numbers
Expand Down
45 changes: 42 additions & 3 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@ package goconst
import (
"go/ast"
"go/token"
"sort"
"strings"
"sync"
)

// Issue represents a finding of duplicated strings or numbers.
// Issue represents a finding of duplicated strings, numbers, or constants.
// Each Issue includes the position where it was found, how many times it occurs,
// the string itself, and any matching constant name.
type Issue struct {
Pos token.Position
OccurrencesCount int
Str string
MatchingConst string
DuplicateConst string
DuplicatePos token.Position
}

// IssuePool provides a pool of Issue slices to reduce allocations
Expand Down Expand Up @@ -61,6 +64,8 @@ type Config struct {
NumberMax int
// ExcludeTypes allows excluding specific types of contexts
ExcludeTypes map[Type]bool
// FindDuplicated constants enables finding constants whose values match existing constants in other packages.
FindDuplicates bool
}

// Run analyzes the provided AST files for duplicated strings or numbers
Expand All @@ -74,6 +79,7 @@ func Run(files []*ast.File, fset *token.FileSet, cfg *Config) ([]Issue, error) {
cfg.IgnoreTests,
cfg.MatchWithConstants,
cfg.ParseNumbers,
cfg.FindDuplicates,
cfg.NumberMin,
cfg.NumberMax,
cfg.MinStringLength,
Expand Down Expand Up @@ -155,6 +161,8 @@ func Run(files []*ast.File, fset *token.FileSet, cfg *Config) ([]Issue, error) {
}
}

sort.Strings(stringKeys)

// Process strings in a predictable order for stable output
for _, str := range stringKeys {
positions := p.strs[str]
Expand All @@ -175,9 +183,9 @@ func Run(files []*ast.File, fset *token.FileSet, cfg *Config) ([]Issue, error) {
// Check for matching constants
if len(p.consts) > 0 {
p.constMutex.RLock()
if cst, ok := p.consts[str]; ok {
if csts, ok := p.consts[str]; ok && len(csts) > 0 {
// const should be in the same package and exported
issue.MatchingConst = cst.Name
issue.MatchingConst = csts[0].Name
}
p.constMutex.RUnlock()
}
Expand All @@ -188,6 +196,37 @@ func Run(files []*ast.File, fset *token.FileSet, cfg *Config) ([]Issue, error) {
p.stringCountMutex.RUnlock()
p.stringMutex.RUnlock()

// process duplicate constants
p.constMutex.RLock()

// reuse string buffer for const keys
stringKeys = stringKeys[:0]

// Create an array of strings and sort for stable output
for str := range p.consts {
if len(p.consts[str]) > 1 {
stringKeys = append(stringKeys, str)
}
}

sort.Strings(stringKeys)

// report an issue for every duplicated const
for _, str := range stringKeys {
positions := p.consts[str]

for i := 1; i < len(positions); i++ {
issueBuffer = append(issueBuffer, Issue{
Pos: positions[i].Position,
Str: str,
DuplicateConst: positions[0].Name,
DuplicatePos: positions[0].Position,
})
}
}

p.constMutex.RUnlock()

// Return string buffer to pool
PutStringBuffer(stringKeys)

Expand Down
115 changes: 85 additions & 30 deletions api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ func example() {
},
expectedIssues: 1,
},
{
name: "duplicate consts",
code: `package example
const ConstA = "test"
func example() {
const ConstB = "test"
}`,
config: &Config{
FindDuplicates: true,
},
expectedIssues: 1,
},
{
name: "string duplication with ignore",
code: `package example
Expand Down Expand Up @@ -212,50 +224,93 @@ func example() {

func TestMultipleFilesAnalysis(t *testing.T) {
// Test analyzing multiple files at once
code1 := `package example
tests := []struct {
name string
code1 string
code2 string
config *Config
expectedIssues int
expectedStr string
expectedOccurrenceCount int
}{
{
name: "duplicate strings",
code1: `package example
func example1() {
a := "shared"
b := "shared"
}
`
code2 := `package example
`,
code2: `package example
func example2() {
c := "shared"
d := "unique"
}
`
fset := token.NewFileSet()
f1, err := parser.ParseFile(fset, "file1.go", code1, 0)
if err != nil {
t.Fatalf("Failed to parse test code: %v", err)
`,
config: &Config{
MinStringLength: 3,
MinOccurrences: 2,
},
expectedIssues: 1,
expectedStr: "shared",
expectedOccurrenceCount: 3,
},
{
name: "duplicate consts in different packages",
code1: `package package1
const ConstA = "shared"
const ConstB = "shared"
`,
code2: `package package2
const (
ConstC = "shared"
ConstD = "shared"
ConstE= "unique"
)`,
config: &Config{
FindDuplicates: true,
},
expectedIssues: 3,
expectedStr: "shared",
expectedOccurrenceCount: 0,
},
}

f2, err := parser.ParseFile(fset, "file2.go", code2, 0)
if err != nil {
t.Fatalf("Failed to parse test code: %v", err)
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

config := &Config{
MinStringLength: 3,
MinOccurrences: 2,
}
fset := token.NewFileSet()
f1, err := parser.ParseFile(fset, "file1.go", tt.code1, 0)
if err != nil {
t.Fatalf("Failed to parse test code: %v", err)
}

issues, err := Run([]*ast.File{f1, f2}, fset, config)
if err != nil {
t.Fatalf("Run() error = %v", err)
}
f2, err := parser.ParseFile(fset, "file2.go", tt.code2, 0)
if err != nil {
t.Fatalf("Failed to parse test code: %v", err)
}

// Should find "shared" appearing 3 times across both files
if len(issues) != 1 {
t.Fatalf("Expected 1 issue, got %d", len(issues))
}
issues, err := Run([]*ast.File{f1, f2}, fset, tt.config)
if err != nil {
t.Fatalf("Run() error = %v", err)
}

issue := issues[0]
if issue.Str != "shared" {
t.Errorf("Issue.Str = %v, want %v", issue.Str, "shared")
}
if issue.OccurrencesCount != 3 {
t.Errorf("Issue.OccurrencesCount = %v, want 3", issue.OccurrencesCount)
// Should find "shared" appearing 3 times across both files
if len(issues) != tt.expectedIssues {
t.Fatalf("Expected %d issue, got %d", tt.expectedIssues, len(issues))
}

if len(issues) > 0 {
issue := issues[0]
if issue.Str != tt.expectedStr {
t.Errorf("Issue.Str = %v, want %v", issue.Str, tt.expectedStr)
}

if issue.OccurrencesCount != tt.expectedOccurrenceCount {
t.Errorf("Issue.OccurrencesCount = %v, want %d", issue.OccurrencesCount, tt.expectedOccurrenceCount)
}
}
})
}
}

Expand Down
8 changes: 7 additions & 1 deletion benchmarks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ func BenchmarkParseTree(b *testing.B) {
false, // ignoreTests
false, // matchConstant
false, // numbers
true, // findDuplicates
0, // numberMin
0, // numberMax
3, // minLength
Expand All @@ -104,6 +105,7 @@ func BenchmarkParseTree(b *testing.B) {
false, // ignoreTests
false, // matchConstant
true, // numbers
true, // findDuplicates
0, // numberMin
0, // numberMax
3, // minLength
Expand All @@ -127,6 +129,7 @@ func BenchmarkParseTree(b *testing.B) {
false, // ignoreTests
true, // matchConstant
false, // numbers
true, // findDuplicates
0, // numberMin
0, // numberMax
3, // minLength
Expand Down Expand Up @@ -157,6 +160,7 @@ func BenchmarkParallelProcessing(b *testing.B) {
false,
false,
true,
true,
0,
0,
3,
Expand Down Expand Up @@ -304,6 +308,7 @@ func helperFunction%d() string {
false,
false,
true,
true,
0,
0,
3,
Expand Down Expand Up @@ -332,6 +337,7 @@ func helperFunction%d() string {
false,
false,
true,
true,
0,
0,
3,
Expand Down Expand Up @@ -377,7 +383,7 @@ func BenchmarkFileReadingPerformance(b *testing.B) {

// Benchmark the optimized file reading
b.Run(fmt.Sprintf("OptimizedIO_%d", size), func(b *testing.B) {
parser := New("", "", "", false, false, false, 0, 0, 3, 2, make(map[Type]bool))
parser := New("", "", "", false, false, false, true, 0, 0, 3, 2, make(map[Type]bool))
b.ResetTimer()

for i := 0; i < b.N; i++ {
Expand Down
20 changes: 16 additions & 4 deletions cmd/goconst/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
-min-occurrences report from how many occurrences (default: 2)
-min-length only report strings with the minimum given length (default: 3)
-match-constant look for existing constants matching the strings
-find-duplicates look for constants with identical values
-numbers search also for duplicated numbers
-min minimum value, only works with -numbers
-max maximum value, only works with -numbers
Expand All @@ -49,6 +50,7 @@
flagMinOccurrences = flag.Int("min-occurrences", 2, "report from how many occurrences")
flagMinLength = flag.Int("min-length", 3, "only report strings with the minimum given length")
flagMatchConstant = flag.Bool("match-constant", false, "look for existing constants matching the strings")
flagFindDuplicates = flag.Bool("find-duplicates", false, "look for constants with duplicated values")
flagNumbers = flag.Bool("numbers", false, "search also for duplicated numbers")
flagMin = flag.Int("min", 0, "minimum value, only works with -numbers")
flagMax = flag.Int("max", 0, "maximum value, only works with -numbers")
Expand Down Expand Up @@ -98,6 +100,7 @@
*flagIgnoreTests,
*flagMatchConstant,
*flagNumbers,
*flagFindDuplicates,
*flagMin,
*flagMax,
*flagMinLength,
Expand Down Expand Up @@ -139,7 +142,7 @@
for str, item := range strs {
for _, xpos := range item {
fmt.Printf(
`%s:%d:%d:%d other occurrence(s) of "%s" found in: %s`,
`%s:%d:%d:%d other occurrence(s) of %q found in: %s`,
xpos.Filename,
xpos.Line,
xpos.Column,
Expand All @@ -157,10 +160,19 @@
if len(consts) == 0 {
continue
}
if cst, ok := consts[str]; ok {
if csts, ok := consts[str]; ok && len(csts) > 0 {
// const should be in the same package and exported
fmt.Printf(`A matching constant has been found for "%s": %s`, str, cst.Name)
fmt.Printf("\n\t%s\n", cst.String())
fmt.Printf(`A matching constant has been found for %q: %s`, str, csts[0].Name)
fmt.Printf("\n\t%s\n", csts[0].String())
}
}
for val, csts := range consts {
if len(csts) > 1 {
fmt.Printf("Duplicate constant(s) with value %q have been found:\n", val)

for i := 0; i < len(csts); i++ {
fmt.Printf("\t%s: %s\n", csts[i].String(), csts[i].Name)
}

Check warning on line 175 in cmd/goconst/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/goconst/main.go#L171-L175

Added lines #L171 - L175 were not covered by tests
}
}
default:
Expand Down
3 changes: 3 additions & 0 deletions integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func TestIntegrationWithTestdata(t *testing.T) {
numberMin int
numberMax int
minLength int
findDuplicates bool
minOccurrences int
expectedStrings int
}{
Expand Down Expand Up @@ -80,6 +81,7 @@ func TestIntegrationWithTestdata(t *testing.T) {
tt.ignoreTests,
tt.matchConstant,
tt.numbers,
tt.findDuplicates,
tt.numberMin,
tt.numberMax,
tt.minLength,
Expand Down Expand Up @@ -140,6 +142,7 @@ func TestIntegrationExcludeTypes(t *testing.T) {
false, // ignoreTests
false, // matchConstant
false, // numbers
false, // findDuplicates
0, // numberMin
0, // numberMax
3, // minLength
Expand Down
Loading
Loading