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
9 changes: 5 additions & 4 deletions fixer/fixer.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,11 @@ func validatePathForWrite(filename string) error {

// FixIssues attempts to fix the given issues
func (f *Fixer) FixIssues(issues []rules.Issue) []Fix {
var fixes []Fix
// Preallocate: assume ~1 fix per issue
fixes := make([]Fix, 0, len(issues))

// Group issues by file
byFile := make(map[string][]rules.Issue)
// Group issues by file with size hint
byFile := make(map[string][]rules.Issue, len(issues)/2+1)
for _, issue := range issues {
byFile[issue.File] = append(byFile[issue.File], issue)
}
Expand All @@ -99,7 +100,7 @@ func (f *Fixer) FixIssues(issues []rules.Issue) []Fix {
}

func (f *Fixer) fixFile(filename string, issues []rules.Issue) []Fix {
var fixes []Fix
fixes := make([]Fix, 0, len(issues))

// Validate path before any operations
if err := validatePathForWrite(filename); err != nil {
Expand Down
7 changes: 4 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@
Verbose: *verboseFlag,
})

// Collect Go files
var files []string
// Collect Go files - estimate ~100 files per pattern
files := make([]string, 0, len(paths)*100)
for _, pattern := range paths {
matches, err := collectGoFiles(pattern, ignorePaths)
if err != nil {
Expand Down Expand Up @@ -236,7 +236,8 @@
}

func collectGoFiles(pattern string, ignorePaths []string) ([]string, error) {
var files []string
// Preallocate with reasonable estimate
files := make([]string, 0, 64)
fileCount := 0

// Validate the base pattern first
Expand Down Expand Up @@ -361,7 +362,7 @@
Low int `json:"low"`
}

func toJSON(issues []rules.Issue) string {

Check failure on line 365 in main.go

View workflow job for this annotation

GitHub Actions / lint

func `toJSON` is unused (unused)
summary := Summary{Total: len(issues)}
for _, issue := range issues {
switch issue.Severity {
Expand Down
4 changes: 3 additions & 1 deletion reporter/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,9 @@ func wordWrap(text string, width int, indent string) string {
return text
}

var lines []string
// Estimate lines needed: text length / width
estimatedLines := len(text)/width + 1
lines := make([]string, 0, estimatedLines)
var currentLine strings.Builder

for _, word := range words {
Expand Down
4 changes: 2 additions & 2 deletions rules/algorithm.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func (r *NestedRangeRule) Name() string { return "nested-range" }
func (r *NestedRangeRule) Category() string { return "algorithm" }

func (r *NestedRangeRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue {
var issues []Issue
issues := make([]Issue, 0, 4)

ast.Inspect(file, func(n ast.Node) bool {
funcDecl, ok := n.(*ast.FuncDecl)
Expand Down Expand Up @@ -187,7 +187,7 @@ func (r *LinearSearchInLoopRule) Name() string { return "linear-search-in-lo
func (r *LinearSearchInLoopRule) Category() string { return "algorithm" }

func (r *LinearSearchInLoopRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue {
var issues []Issue
issues := make([]Issue, 0, 4)

ast.Inspect(file, func(n ast.Node) bool {
funcDecl, ok := n.(*ast.FuncDecl)
Expand Down
6 changes: 3 additions & 3 deletions rules/allocation.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func (r *UnpreallocatedSliceRule) Name() string { return "unpreallocated-sli
func (r *UnpreallocatedSliceRule) Category() string { return "allocation" }

func (r *UnpreallocatedSliceRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue {
var issues []Issue
issues := make([]Issue, 0, 8)

// For each function, track preallocated slices
ast.Inspect(file, func(n ast.Node) bool {
Expand Down Expand Up @@ -186,7 +186,7 @@ func (r *StringConcatInLoopRule) Name() string { return "string-concat-loop"
func (r *StringConcatInLoopRule) Category() string { return "allocation" }

func (r *StringConcatInLoopRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue {
var issues []Issue
issues := make([]Issue, 0, 4)

// Track strings.Builder usage
builders := findStringBuilders(file)
Expand Down Expand Up @@ -320,7 +320,7 @@ func (r *MapWithoutSizeRule) Name() string { return "map-without-size" }
func (r *MapWithoutSizeRule) Category() string { return "allocation" }

func (r *MapWithoutSizeRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue {
var issues []Issue
issues := make([]Issue, 0, 4)

// Only flag maps that are actually populated in loops
ast.Inspect(file, func(n ast.Node) bool {
Expand Down
22 changes: 14 additions & 8 deletions rules/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ type Analyzer struct {

// NewAnalyzer creates a new analyzer with the given config
func NewAnalyzer(config AnalyzerConfig) *Analyzer {
a := &Analyzer{config: config}
// Estimate total rules: ~3 rules per category on average
estimatedRules := len(config.Rules) * 3
a := &Analyzer{
config: config,
rules: make([]Rule, 0, estimatedRules),
}

// Collect rules based on config
for _, category := range config.Rules {
Expand All @@ -30,7 +35,8 @@ func NewAnalyzer(config AnalyzerConfig) *Analyzer {

// Analyze runs all rules against the given files
func (a *Analyzer) Analyze(files []string) []Issue {
var allIssues []Issue
// Preallocate with estimate: ~2 issues per file on average
allIssues := make([]Issue, 0, len(files)*2)

fset := token.NewFileSet()

Expand Down Expand Up @@ -90,7 +96,7 @@ func (a *Analyzer) Analyze(files []string) []Issue {

// FindNestedRangeLoops finds nested for-range loops
func FindNestedRangeLoops(file *ast.File, fset *token.FileSet) []ast.Node {
var nested []ast.Node
nested := make([]ast.Node, 0, 4) // Most files have few nested loops

ast.Inspect(file, func(n ast.Node) bool {
outerRange, ok := n.(*ast.RangeStmt)
Expand All @@ -116,7 +122,7 @@ func FindNestedRangeLoops(file *ast.File, fset *token.FileSet) []ast.Node {

// FindAppendInLoop finds append calls inside loops without preallocation
func FindAppendInLoop(file *ast.File, fset *token.FileSet) []AppendInLoopInfo {
var results []AppendInLoopInfo
results := make([]AppendInLoopInfo, 0, 8) // Typical file has few append-in-loop issues

ast.Inspect(file, func(n ast.Node) bool {
// Look for for statements (both range and regular)
Expand Down Expand Up @@ -174,7 +180,7 @@ type AppendInLoopInfo struct {

// FindStringConcatInLoop finds string concatenation in loops
func FindStringConcatInLoop(file *ast.File, fset *token.FileSet) []token.Position {
var results []token.Position
results := make([]token.Position, 0, 4)

ast.Inspect(file, func(n ast.Node) bool {
var loopBody *ast.BlockStmt
Expand Down Expand Up @@ -215,7 +221,7 @@ func FindStringConcatInLoop(file *ast.File, fset *token.FileSet) []token.Positio

// FindSQLInLoop finds database query patterns inside loops
func FindSQLInLoop(file *ast.File, fset *token.FileSet) []SQLInLoopInfo {
var results []SQLInLoopInfo
results := make([]SQLInLoopInfo, 0, 4)

sqlMethods := map[string]bool{
"Query": true,
Expand Down Expand Up @@ -277,7 +283,7 @@ type SQLInLoopInfo struct {

// FindUnbufferedChannels finds unbuffered channel creation
func FindUnbufferedChannels(file *ast.File, fset *token.FileSet) []token.Position {
var results []token.Position
results := make([]token.Position, 0, 4)

ast.Inspect(file, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
Expand Down Expand Up @@ -347,7 +353,7 @@ func FindMutexHotspots(file *ast.File, fset *token.FileSet) map[string]int {

// FindJSONInLoop finds JSON marshal/unmarshal calls in loops
func FindJSONInLoop(file *ast.File, fset *token.FileSet) []JSONInLoopInfo {
var results []JSONInLoopInfo
results := make([]JSONInLoopInfo, 0, 4)

jsonFuncs := map[string]bool{
"Marshal": true,
Expand Down
45 changes: 28 additions & 17 deletions rules/benchmark.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package rules
import (
"go/ast"
"go/token"
"strings"
)

func init() {
Expand Down Expand Up @@ -132,33 +133,40 @@ func generateBenchmarkCode(funcName string, funcDecl *ast.FuncDecl) string {
// Generate basic benchmark scaffold
benchName := "Benchmark" + capitalizeFirst(funcName)

code := "func " + benchName + "(b *testing.B) {\n"
code += "\t// Setup: initialize test data\n"
var b strings.Builder
b.Grow(256) // Pre-allocate reasonable capacity

b.WriteString("func ")
b.WriteString(benchName)
b.WriteString("(b *testing.B) {\n")
b.WriteString("\t// Setup: initialize test data\n")

// Add parameter hints based on function signature
if funcDecl.Type.Params != nil && len(funcDecl.Type.Params.List) > 0 {
code += "\t// params: "
b.WriteString("\t// params: ")
for i, param := range funcDecl.Type.Params.List {
if i > 0 {
code += ", "
b.WriteString(", ")
}
for j, name := range param.Names {
if j > 0 {
code += ", "
b.WriteString(", ")
}
code += name.Name
b.WriteString(name.Name)
}
}
code += "\n"
b.WriteString("\n")
}

code += "\n\tb.ResetTimer()\n"
code += "\tfor i := 0; i < b.N; i++ {\n"
code += "\t\t" + funcName + "(...) // Add arguments\n"
code += "\t}\n"
code += "}"
b.WriteString("\n\tb.ResetTimer()\n")
b.WriteString("\tfor i := 0; i < b.N; i++ {\n")
b.WriteString("\t\t")
b.WriteString(funcName)
b.WriteString("(...) // Add arguments\n")
b.WriteString("\t}\n")
b.WriteString("}")

return code
return b.String()
}

func capitalizeFirst(s string) string {
Expand All @@ -177,12 +185,15 @@ func joinPatterns(patterns []perfPattern) string {
return ""
}

result := ""
var b strings.Builder
b.Grow(len(patterns) * 20) // Estimate ~20 chars per pattern
for i, p := range patterns {
if i > 0 {
result += ", "
b.WriteString(", ")
}
result += itoa(p.count) + " " + p.name
b.WriteString(itoa(p.count))
b.WriteString(" ")
b.WriteString(p.name)
}
return result
return b.String()
}
8 changes: 4 additions & 4 deletions rules/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func (r *RepeatedRegexpCompileRule) Name() string { return "repeated-regexp-
func (r *RepeatedRegexpCompileRule) Category() string { return "cache" }

func (r *RepeatedRegexpCompileRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue {
var issues []Issue
issues := make([]Issue, 0, 4)

// Track if we're inside a function
var inFunc bool
Expand Down Expand Up @@ -75,7 +75,7 @@ func (r *RepeatedTemplateParseRule) Name() string { return "repeated-templat
func (r *RepeatedTemplateParseRule) Category() string { return "cache" }

func (r *RepeatedTemplateParseRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue {
var issues []Issue
issues := make([]Issue, 0, 4)

var inFunc bool

Expand Down Expand Up @@ -134,7 +134,7 @@ func (r *RegexpMatchStringRule) Name() string { return "regexp-match-string-
func (r *RegexpMatchStringRule) Category() string { return "cache" }

func (r *RegexpMatchStringRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue {
var issues []Issue
issues := make([]Issue, 0, 4)

ast.Inspect(file, func(n ast.Node) bool {
var loopBody *ast.BlockStmt
Expand Down Expand Up @@ -202,7 +202,7 @@ func (r *JSONSchemaValidationRule) Name() string { return "json-schema-in-lo
func (r *JSONSchemaValidationRule) Category() string { return "cache" }

func (r *JSONSchemaValidationRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue {
var issues []Issue
issues := make([]Issue, 0, 4)

ast.Inspect(file, func(n ast.Node) bool {
var loopBody *ast.BlockStmt
Expand Down
6 changes: 3 additions & 3 deletions rules/concurrency.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func (r *UnbufferedChannelRule) Name() string { return "unbuffered-channel"
func (r *UnbufferedChannelRule) Category() string { return "concurrency" }

func (r *UnbufferedChannelRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue {
var issues []Issue
issues := make([]Issue, 0, 4)

ast.Inspect(file, func(n ast.Node) bool {
funcDecl, ok := n.(*ast.FuncDecl)
Expand Down Expand Up @@ -273,7 +273,7 @@ func (r *MutexInLoopRule) Name() string { return "mutex-in-loop" }
func (r *MutexInLoopRule) Category() string { return "concurrency" }

func (r *MutexInLoopRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue {
var issues []Issue
issues := make([]Issue, 0, 4)

ast.Inspect(file, func(n ast.Node) bool {
var loopBody *ast.BlockStmt
Expand Down Expand Up @@ -343,7 +343,7 @@ func (r *GoroutineLeakRule) Name() string { return "goroutine-leak" }
func (r *GoroutineLeakRule) Category() string { return "concurrency" }

func (r *GoroutineLeakRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue {
var issues []Issue
issues := make([]Issue, 0, 4)

ast.Inspect(file, func(n ast.Node) bool {
goStmt, ok := n.(*ast.GoStmt)
Expand Down
6 changes: 3 additions & 3 deletions rules/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
func (r *ContextBackgroundInHandlerRule) Category() string { return "context" }

func (r *ContextBackgroundInHandlerRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue {
var issues []Issue
issues := make([]Issue, 0, 4)

ast.Inspect(file, func(n ast.Node) bool {
funcDecl, ok := n.(*ast.FuncDecl)
Expand Down Expand Up @@ -82,7 +82,7 @@
func (r *MissingContextTimeoutRule) Category() string { return "context" }

func (r *MissingContextTimeoutRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue {
var issues []Issue
issues := make([]Issue, 0, 4)

// Track contexts with timeouts in each function
ast.Inspect(file, func(n ast.Node) bool {
Expand Down Expand Up @@ -151,7 +151,7 @@
}

// For Do, Get, etc. - check if context seems to have timeout
if !hasTimeoutCtx && (sel.Sel.Name == "Do" || sel.Sel.Name == "Get" || sel.Sel.Name == "Invoke") {

Check failure on line 154 in rules/context.go

View workflow job for this annotation

GitHub Actions / lint

SA9003: empty branch (staticcheck)
// This is a heuristic - we can't be 100% sure without type info
// Only flag if the receiver looks like an HTTP client
}
Expand All @@ -173,7 +173,7 @@
func (r *ContextLeakRule) Category() string { return "context" }

func (r *ContextLeakRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue {
var issues []Issue
issues := make([]Issue, 0, 4)

ast.Inspect(file, func(n ast.Node) bool {
funcDecl, ok := n.(*ast.FuncDecl)
Expand Down
4 changes: 2 additions & 2 deletions rules/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func (r *SQLInLoopRule) Name() string { return "sql-in-loop" }
func (r *SQLInLoopRule) Category() string { return "database" }

func (r *SQLInLoopRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue {
var issues []Issue
issues := make([]Issue, 0, 4)

// Track prepared statements declared in the current scope
preparedStmts := findPreparedStatements(file)
Expand Down Expand Up @@ -255,7 +255,7 @@ func (r *UnbatchedInsertRule) Name() string { return "unbatched-insert" }
func (r *UnbatchedInsertRule) Category() string { return "database" }

func (r *UnbatchedInsertRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue {
var issues []Issue
issues := make([]Issue, 0, 4)

ast.Inspect(file, func(n ast.Node) bool {
var loopBody *ast.BlockStmt
Expand Down
4 changes: 2 additions & 2 deletions rules/dbpool.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func (r *MissingConnectionPoolRule) Name() string { return "missing-connecti
func (r *MissingConnectionPoolRule) Category() string { return "database" }

func (r *MissingConnectionPoolRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue {
var issues []Issue
issues := make([]Issue, 0, 4)

// Find sql.Open calls
sqlOpenVars := make(map[string]token.Position)
Expand Down Expand Up @@ -111,7 +111,7 @@ func (r *UnlimitedConnectionPoolRule) Name() string { return "unlimited-conn
func (r *UnlimitedConnectionPoolRule) Category() string { return "database" }

func (r *UnlimitedConnectionPoolRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue {
var issues []Issue
issues := make([]Issue, 0, 2)

ast.Inspect(file, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
Expand Down
Loading
Loading