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
17 changes: 11 additions & 6 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package goconst
import (
"go/ast"
"go/token"
"go/types"
"sort"
"strings"
"sync"
Expand Down Expand Up @@ -64,16 +65,18 @@ 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 enables finding constants whose values match existing constants in other packages.
FindDuplicates bool
// EvalConstExpressions enables evaluation of constant expressions like Prefix + "suffix"
EvalConstExpressions bool
}

// NewWithIgnorePatterns creates a new instance of the parser with support for multiple ignore patterns.
// This is an alternative constructor that takes a slice of ignore string patterns.
func NewWithIgnorePatterns(
path, ignore string,
ignoreStrings []string,
ignoreTests, matchConstant, numbers, findDuplicates bool,
ignoreTests, matchConstant, numbers, findDuplicates, evalConstExpressions bool,
numberMin, numberMax, minLength, minOccurrences int,
excludeTypes map[Type]bool) *Parser {

Expand Down Expand Up @@ -101,6 +104,7 @@ func NewWithIgnorePatterns(
matchConstant,
numbers,
findDuplicates,
evalConstExpressions,
numberMin,
numberMax,
minLength,
Expand All @@ -111,7 +115,7 @@ func NewWithIgnorePatterns(

// RunWithConfig is a convenience function that runs the analysis with a Config object
// directly supporting multiple ignore patterns.
func RunWithConfig(files []*ast.File, fset *token.FileSet, cfg *Config) ([]Issue, error) {
func RunWithConfig(files []*ast.File, fset *token.FileSet, typeInfo *types.Info, cfg *Config) ([]Issue, error) {
p := NewWithIgnorePatterns(
"",
"",
Expand All @@ -120,6 +124,7 @@ func RunWithConfig(files []*ast.File, fset *token.FileSet, cfg *Config) ([]Issue
cfg.MatchWithConstants,
cfg.ParseNumbers,
cfg.FindDuplicates,
cfg.EvalConstExpressions,
cfg.NumberMin,
cfg.NumberMax,
cfg.MinStringLength,
Expand Down Expand Up @@ -176,9 +181,9 @@ func RunWithConfig(files []*ast.File, fset *token.FileSet, cfg *Config) ([]Issue
ast.Walk(&treeVisitor{
fileSet: fset,
packageName: emptyStr,
fileName: emptyStr,
p: p,
ignoreRegex: p.ignoreStringsRegex,
typeInfo: typeInfo,
}, f)
}(f)
}
Expand Down Expand Up @@ -277,6 +282,6 @@ func RunWithConfig(files []*ast.File, fset *token.FileSet, cfg *Config) ([]Issue
// Run analyzes the provided AST files for duplicated strings or numbers
// according to the provided configuration.
// It returns a slice of Issue objects containing the findings.
func Run(files []*ast.File, fset *token.FileSet, cfg *Config) ([]Issue, error) {
return RunWithConfig(files, fset, cfg)
func Run(files []*ast.File, fset *token.FileSet, typeInfo *types.Info, cfg *Config) ([]Issue, error) {
return RunWithConfig(files, fset, typeInfo, cfg)
}
127 changes: 118 additions & 9 deletions api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"go/ast"
"go/parser"
"go/token"
"go/types"
"testing"
)

Expand Down Expand Up @@ -53,6 +54,20 @@ func example() {
},
expectedIssues: 1,
},
{
name: "duplicate computed consts",
code: `package example
const ConstA = "te"
const Test = "test"
func example() {
const ConstB = ConstA + "st"
}`,
config: &Config{
FindDuplicates: true,
EvalConstExpressions: true,
},
expectedIssues: 1,
},
{
name: "string duplication with ignore",
code: `package example
Expand Down Expand Up @@ -164,7 +179,10 @@ func example() {
t.Fatalf("Failed to parse test code: %v", err)
}

issues, err := Run([]*ast.File{f}, fset, tt.config)
chkr, info := checker(fset)
_ = chkr.Files([]*ast.File{f})

issues, err := Run([]*ast.File{f}, fset, info, tt.config)
if err != nil {
t.Fatalf("Run() error = %v", err)
}
Expand Down Expand Up @@ -201,7 +219,10 @@ func example() {
MatchWithConstants: true,
}

issues, err := Run([]*ast.File{f}, fset, config)
chkr, info := checker(fset)
_ = chkr.Files([]*ast.File{f})

issues, err := Run([]*ast.File{f}, fset, info, config)
if err != nil {
t.Fatalf("Run() error = %v", err)
}
Expand Down Expand Up @@ -256,16 +277,16 @@ func example2() {
expectedOccurrenceCount: 3,
},
{
name: "duplicate consts in different packages",
code1: `package package1
name: "duplicate consts in different files",
code1: `package example
const ConstA = "shared"
const ConstB = "shared"
`,
code2: `package package2
code2: `package example
const (
ConstC = "shared"
ConstD = "shared"
ConstE= "unique"
ConstE = "unique"
)`,
config: &Config{
FindDuplicates: true,
Expand All @@ -290,7 +311,10 @@ const (
t.Fatalf("Failed to parse test code: %v", err)
}

issues, err := Run([]*ast.File{f1, f2}, fset, tt.config)
chkr, info := checker(fset)
_ = chkr.Files([]*ast.File{f1, f2})

issues, err := Run([]*ast.File{f1, f2}, fset, info, tt.config)
if err != nil {
t.Fatalf("Run() error = %v", err)
}
Expand Down Expand Up @@ -348,8 +372,10 @@ func allContexts(param string) string {
MinStringLength: 3,
MinOccurrences: 2,
}
chkr, info := checker(fset)
_ = chkr.Files([]*ast.File{f})

issues, err := Run([]*ast.File{f}, fset, config)
issues, err := Run([]*ast.File{f}, fset, info, config)
if err != nil {
t.Fatalf("Run() error = %v", err)
}
Expand Down Expand Up @@ -429,8 +455,10 @@ func multipleContexts() {
MinOccurrences: 2,
ExcludeTypes: tt.excludeTypes,
}
chkr, info := checker(fset)
_ = chkr.Files([]*ast.File{f})

issues, err := Run([]*ast.File{f}, fset, config)
issues, err := Run([]*ast.File{f}, fset, info, config)
if err != nil {
t.Fatalf("Run() error = %v", err)
}
Expand All @@ -453,3 +481,84 @@ func multipleContexts() {
})
}
}

func TestConstExpressions(t *testing.T) {
// Test detecting and matching string constants derived from expressions
code := `package example

const (
Prefix = "example.com/"
Label1 = Prefix + "some_label"
Label2 = Prefix + "another_label"
)

func example() {
// These should match the constants from expressions
a := "example.com/some_label"
b := "example.com/some_label"

// This should also match
web1 := "example.com/another_label"
web2 := "example.com/another_label"
}
`
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "example.go", code, 0)
if err != nil {
t.Fatalf("Failed to parse test code: %v", err)
}

config := &Config{
MinStringLength: 3,
MinOccurrences: 2,
MatchWithConstants: true,
EvalConstExpressions: true,
}
chkr, info := checker(fset)
_ = chkr.Files([]*ast.File{f})

issues, err := Run([]*ast.File{f}, fset, info, config)
if err != nil {
t.Fatalf("Run() error = %v", err)
}

// We expect issues for both labels
expectedMatches := map[string]string{
"example.com/some_label": "Label1",
"example.com/another_label": "Label2",
}

// Check that we have two issues
if len(issues) != 2 {
t.Errorf("Expected 2 issues, got %d", len(issues))
for _, issue := range issues {
t.Logf("Found issue: %q matches constant %q with %d occurrences",
issue.Str, issue.MatchingConst, issue.OccurrencesCount)
}
return
}

// Check that each string matches the expected constant
for _, issue := range issues {
expectedConst, ok := expectedMatches[issue.Str]
if !ok {
t.Errorf("Unexpected issue for string: %s", issue.Str)
continue
}

if issue.MatchingConst != expectedConst {
t.Errorf("For string %q: got matching const %q, want %q",
issue.Str, issue.MatchingConst, expectedConst)
}
}
}

func checker(fset *token.FileSet) (*types.Checker, *types.Info) {
cfg := &types.Config{
Error: func(err error) {},
}
info := &types.Info{
Types: make(map[ast.Expr]types.TypeAndValue),
}
return types.NewChecker(cfg, fset, types.NewPackage("", "example"), info), info
}
Loading
Loading