+
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
11 changes: 7 additions & 4 deletions grit/cmd/stat/subcommands/complexity.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,18 @@ var ComplexityCmd = &cobra.Command{ //nolint:exhaustruct // no need to set all f
Short: "Finds the most complex files",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
repoPath, err := filepath.Abs(args[0])
path, err := filepath.Abs(args[0])
if err != nil {
return fmt.Errorf("error getting absolute path: %w", err)
}

flag.LogIfVerbose("Processing repository: %s\n", repoPath)
flag.LogIfVerbose("Processing repository: %s\n", path)

if err := complexity.PopulateOpts(&complexityOpts, excludeComplexityRegex); err != nil {
return fmt.Errorf("failed to create options: %w", err)
}

fileStat, err := complexity.RunComplexity(repoPath, &complexityOpts)
fileStat, err := complexity.RunComplexity(path, &complexityOpts)
if err != nil {
return fmt.Errorf("error running complexity analysis: %w", err)
}
Expand All @@ -51,7 +51,10 @@ func init() {
flags := ComplexityCmd.PersistentFlags()

flags.StringVarP(&complexityOpts.Engine, flag.LongEngine, flag.ShortEngine, complexity.Gocyclo,
fmt.Sprintf("Specify complexity calculation engine: [%s, %s]", complexity.Gocyclo, complexity.Gocognit))
fmt.Sprintf(`Specify complexity calculation engine: [%s, %s, %s].
When CSV engine is chosen, GRIT will try to read function complexity data from CSV file specified by <path> parameter.
The file should have following fields: "filename,function,complexity,line-count (optional),packages (optional)"
`, complexity.Gocyclo, complexity.Gocognit, complexity.CSV))
flags.IntVarP(&complexityOpts.Top, flag.LongTop, flag.ShortTop, git.DefaultTop, "Number of top files to display")
flags.BoolVarP(&flag.Verbose, flag.LongVerbose, flag.ShortVerbose, false, "Show detailed progress")
flags.StringVar(&excludeComplexityRegex, flag.LongExclude, "", "Exclude files matching regex pattern")
Expand Down
110 changes: 110 additions & 0 deletions pkg/complexity/csv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package complexity

import (
"encoding/csv"
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
)

func RunCSV(filepath string, opts *Options) ([]*FileStat, error) {
file, err := os.Open(filepath)
if err != nil {
return nil, fmt.Errorf("failed to open CSV file at %s: %w", filepath, err)
}
defer file.Close()

functionStats, err := readComplexityFromCSV(file)
if err != nil {
return nil, fmt.Errorf("failed to parse complexity data from CSV: %w", err)
}

fileMap := make(map[string][]FunctionStat)
for _, stat := range functionStats {
fileMap[stat.File] = append(fileMap[stat.File], *stat)
}

result := make([]*FileStat, 0, len(fileMap))

for file, functions := range fileMap {
if opts.ExcludeRegex != nil && opts.ExcludeRegex.MatchString(file) {
continue
}

result = append(result, &FileStat{
Path: file,
Functions: functions,
})
}

// Calculate average complexity for each file
AvgComplexity(result)

return result, nil
}

const minimalColumns = 4

func readComplexityFromCSV(r io.Reader) ([]*FunctionStat, error) { //nolint:cyclop // complexity is not a problem here
csvReader := csv.NewReader(r)
csvReader.FieldsPerRecord = -1 // Allow variable number of fields per record

records, err := csvReader.ReadAll()
if err != nil {
return nil, fmt.Errorf("failed to read CSV data: %w", err)
}

if len(records) == 0 {
return nil, errors.New("CSV data is empty")
}

result := make([]*FunctionStat, 0, len(records))

for pos, record := range records {
// Minimum required fields: filename, function, length, complexity
if len(record) < minimalColumns {
return nil, fmt.Errorf("row %d: insufficient columns, expected at least 4", pos+1)
}

stat := &FunctionStat{
File: record[0],
Name: record[1],
}

length, err := strconv.Atoi(record[2])
if err != nil {
return nil, fmt.Errorf("row %d: invalid length value '%s': %w", pos+1, record[2], err)
}

stat.Length = length

complexity, err := strconv.Atoi(record[3])
if err != nil {
return nil, fmt.Errorf("row %d: invalid complexity value '%s': %w", pos+1, record[3], err)
}

stat.Complexity = complexity

// Optional: line number
if len(record) > 4 && record[4] != "" {
line, err := strconv.Atoi(record[4])
if err != nil {
return nil, fmt.Errorf("row %d: invalid line value '%s': %w", pos+1, record[4], err)
}

stat.Line = line
}

// Optional: packages
if len(record) > 5 && record[5] != "" {
stat.Package = strings.Split(record[5], ";")
}

result = append(result, stat)
}

return result, nil
}
201 changes: 201 additions & 0 deletions pkg/complexity/csv_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package complexity

import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestReadComplexityFromCSV(t *testing.T) {
tests := []struct {
name string
csv string
want []*FunctionStat
wantErr bool
}{
{
name: "valid csv with all fields",
csv: `filename.go,Calculate,120,15,42,pkg1;pkg2
anothefile.go,Process,80,8,123,main`,
want: []*FunctionStat{
{
File: "filename.go",
Name: "Calculate",
Length: 120,
Complexity: 15,
Line: 42,
Package: []string{"pkg1", "pkg2"},
},
{
File: "anothefile.go",
Name: "Process",
Length: 80,
Complexity: 8,
Line: 123,
Package: []string{"main"},
},
},
wantErr: false,
},
{
name: "valid csv with header row",
csv: `filename.go,Calculate,120,15,42,pkg1;pkg2`,
want: []*FunctionStat{
{
File: "filename.go",
Name: "Calculate",
Length: 120,
Complexity: 15,
Line: 42,
Package: []string{"pkg1", "pkg2"},
},
},
wantErr: false,
},
{
name: "valid csv with minimum required fields",
csv: `filename.go,Calculate,120,15
anothefile.go,Process,80,8`,
want: []*FunctionStat{
{
File: "filename.go",
Name: "Calculate",
Length: 120,
Complexity: 15,
},
{
File: "anothefile.go",
Name: "Process",
Length: 80,
Complexity: 8,
},
},
wantErr: false,
},
{
name: "valid csv with mixed field counts",
csv: `filename.go,Calculate,120,15,42
anothefile.go,Process,80,8,,main`,
want: []*FunctionStat{
{
File: "filename.go",
Name: "Calculate",
Length: 120,
Complexity: 15,
Line: 42,
},
{
File: "anothefile.go",
Name: "Process",
Length: 80,
Complexity: 8,
Package: []string{"main"},
},
},
wantErr: false,
},
{
name: "empty csv",
csv: "",
want: nil,
wantErr: true,
},
{
name: "insufficient columns",
csv: "filename.go,Calculate,120",
want: nil,
wantErr: true,
},
{
name: "invalid length value",
csv: "filename.go,Calculate,invalid,15",
want: nil,
wantErr: true,
},
{
name: "invalid complexity value",
csv: "filename.go,Calculate,120,invalid",
want: nil,
wantErr: true,
},
{
name: "invalid line value",
csv: "filename.go,Calculate,120,15,invalid",
want: nil,
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reader := strings.NewReader(tt.csv)
got, err := readComplexityFromCSV(reader)

if tt.wantErr {
assert.Error(t, err)

return
}

require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}

func TestRunCSV(t *testing.T) {
csvContent := `file1.go,func1,50,5,10,pkg1
file1.go,func2,70,10,20,pkg1
file2.go,func3,30,3,15,pkg2
file3.go,func4,100,15,25,pkg3;pkg4`

csvPath := filepath.Join(t.TempDir(), "complexity.csv")
err := os.WriteFile(csvPath, []byte(csvContent), 0o600)

require.NoError(t, err)

opts := &Options{
Engine: CSV,
Top: 10,
}

results, err := RunCSV(csvPath, opts)
require.NoError(t, err)
assert.Len(t, results, 3)

expectedFiles := map[string]struct {
FunctionCount int
AvgComplexity float64
}{
"file1.go": {FunctionCount: 2, AvgComplexity: 7.5}, // (5+10)/2 = 7.5
"file2.go": {FunctionCount: 1, AvgComplexity: 3.0},
"file3.go": {FunctionCount: 1, AvgComplexity: 15.0},
}

for _, file := range results {
expected, exists := expectedFiles[file.Path]
assert.True(t, exists, "Unexpected file in results: %s", file.Path)

if exists {
assert.Len(t, file.Functions, expected.FunctionCount, "Incorrect function count for %s", file.Path)
assert.InEpsilon(t, expected.AvgComplexity, file.AvgComplexity, 0.001,
"Incorrect average complexity for %s", file.Path)
}
}

// Test with exclude regex
excludeOpts := &Options{
Engine: CSV,
ExcludeRegex: regexp.MustCompile(`file[12]\.go`),
}

filteredResults, err := RunCSV(csvPath, excludeOpts)
require.NoError(t, err)
assert.Len(t, filteredResults, 1)
assert.Equal(t, "file3.go", filteredResults[0].Path)
}
3 changes: 3 additions & 0 deletions pkg/complexity/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type Engine = string
const (
Gocyclo = "gocyclo"
Gocognit = "gocognit"
CSV = "csv-file"
)

type FileStat struct {
Expand Down Expand Up @@ -57,6 +58,8 @@ func RunComplexity(repoPath string, opts *Options) ([]*FileStat, error) {
return RunGocyclo(repoPath, opts)
case Gocognit:
return RunGocognit(repoPath, opts)
case CSV:
return RunCSV(repoPath, opts)
default:
return nil, ErrUnsupportedEngine
}
Expand Down
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载