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
92 changes: 92 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.21', '1.22', '1.23']

steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}

- name: Build
run: go build -v ./...

- name: Test
run: go test -v -race -coverprofile=coverage.out ./...

- name: Upload coverage
if: matrix.go-version == '1.23'
uses: codecov/codecov-action@v4
with:
files: ./coverage.out
fail_ci_if_error: false

lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'

- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: latest

self-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'

- name: Build goperf
run: go build -o goperf .

- name: Self-audit (dogfooding)
run: ./goperf --fail-on=critical ./...

release:
needs: [test, lint, self-audit]
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'

- name: Build binaries
run: |
GOOS=linux GOARCH=amd64 go build -o goperf-linux-amd64 .
GOOS=linux GOARCH=arm64 go build -o goperf-linux-arm64 .
GOOS=darwin GOARCH=amd64 go build -o goperf-darwin-amd64 .
GOOS=darwin GOARCH=arm64 go build -o goperf-darwin-arm64 .
GOOS=windows GOARCH=amd64 go build -o goperf-windows-amd64.exe .

- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: binaries
path: goperf-*
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,47 @@ goperf --fail-on=critical ./...
| **MEDIUM** | Moderate impact | Should fix |
| **LOW** | Minor optimization | Nice to have |

## Ignoring Issues

Sometimes you need to suppress a warning - perhaps it's a false positive, or you've verified the code is intentional. Use `// perf:ignore` comments:

### Line-level Ignore

```go
// perf:ignore
for _, item := range items {
db.Exec(query, item) // This line is ignored
}
```

Or on the same line:
```go
db.Exec(query, item) // perf:ignore
```

### Ignore Specific Rule

```go
// perf:ignore sql-in-loop
for _, item := range items {
db.Exec(query, item) // Only sql-in-loop is ignored
result = append(result, item) // Still flagged for append-in-loop
}
```

### Block Ignore

```go
// perf:ignore-start
for _, item := range items {
db.Exec(query, item)
}
for _, other := range others {
db.Query(q, other)
}
// perf:ignore-end
```

## Contributing

Contributions welcome! Areas we'd love help with:
Expand Down
11 changes: 10 additions & 1 deletion rules/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,27 @@ func (a *Analyzer) Analyze(files []string) []Issue {
continue
}

// Parse ignore comments
ignoreSet := NewIgnoreSet(src)

// Run all rules
for _, rule := range a.rules {
issues := rule.Check(file, fset, src)
for i := range issues {
issues[i].File = filename

// Skip ignored issues
if ignoreSet.ShouldIgnore(issues[i].Line, issues[i].Rule) {
continue
}

if a.config.Context > 0 {
pos := fset.Position(token.Pos(issues[i].Line))
pos.Line = issues[i].Line
issues[i].Context = ExtractContext(src, pos, a.config.Context)
}
allIssues = append(allIssues, issues[i])
}
allIssues = append(allIssues, issues...)
}
}

Expand Down
143 changes: 143 additions & 0 deletions rules/ignore_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package rules

import (
"testing"
)

func TestIgnoreSet_LineIgnore(t *testing.T) {
src := []byte(`package main

func foo() {
// perf:ignore
for _, item := range items {
db.Exec(query, item) // This should be ignored
}
}
`)
is := NewIgnoreSet(src)

// Line 4 is the comment, line 5 is the for loop - both should be ignored
if !is.ShouldIgnore(4, "sql-in-loop") {
t.Error("Line 4 should be ignored")
}
if !is.ShouldIgnore(5, "sql-in-loop") {
t.Error("Line 5 should be ignored")
}
// Line 6 should NOT be ignored (only next line after comment)
if is.ShouldIgnore(7, "sql-in-loop") {
t.Error("Line 7 should NOT be ignored")
}
}

func TestIgnoreSet_SameLineIgnore(t *testing.T) {
src := []byte(`package main

func foo() {
db.Exec(query, item) // perf:ignore
}
`)
is := NewIgnoreSet(src)

// Line 4 has the ignore comment on the same line
if !is.ShouldIgnore(4, "sql-in-loop") {
t.Error("Line 4 should be ignored (same-line comment)")
}
}

func TestIgnoreSet_BlockIgnore(t *testing.T) {
src := []byte(`package main

func foo() {
// perf:ignore-start
for _, item := range items {
db.Exec(query, item)
}
for _, other := range others {
db.Query(q, other)
}
// perf:ignore-end

// This should NOT be ignored
db.Exec(query, x)
}
`)
is := NewIgnoreSet(src)

// Lines 4-11 should be ignored (inside block)
for line := 4; line <= 11; line++ {
if !is.ShouldIgnore(line, "sql-in-loop") {
t.Errorf("Line %d should be ignored (inside block)", line)
}
}

// Line 14 should NOT be ignored (after block)
if is.ShouldIgnore(14, "sql-in-loop") {
t.Error("Line 14 should NOT be ignored (after block)")
}
}

func TestIgnoreSet_SpecificRule(t *testing.T) {
src := []byte(`package main

func foo() {
// perf:ignore sql-in-loop
for _, item := range items {
db.Exec(query, item) // ignored
result = append(result, item) // NOT ignored - different rule
}
}
`)
is := NewIgnoreSet(src)

// sql-in-loop should be ignored
if !is.ShouldIgnore(5, "sql-in-loop") {
t.Error("sql-in-loop should be ignored on line 5")
}

// append-in-loop should NOT be ignored (different rule)
if is.ShouldIgnore(5, "append-in-loop") {
t.Error("append-in-loop should NOT be ignored on line 5")
}
}

func TestIgnoreSet_NoIgnore(t *testing.T) {
src := []byte(`package main

func foo() {
// This is a normal comment
db.Exec(query, item)
}
`)
is := NewIgnoreSet(src)

if is.ShouldIgnore(5, "sql-in-loop") {
t.Error("Line 5 should NOT be ignored")
}
}

func TestParseIgnoreComment(t *testing.T) {
tests := []struct {
line string
wantRule string
wantFound bool
}{
{"// perf:ignore", "", true},
{"// perf:ignore sql-in-loop", "sql-in-loop", true},
{" // perf:ignore", "", true},
{"code() // perf:ignore", "", true},
{"// perf:ignore-start", "", false}, // Should not match block markers
{"// perf:ignore-end", "", false},
{"// normal comment", "", false},
{"no comment", "", false},
}

for _, tt := range tests {
rule, found := parseIgnoreComment(tt.line)
if found != tt.wantFound {
t.Errorf("parseIgnoreComment(%q): found = %v, want %v", tt.line, found, tt.wantFound)
}
if rule != tt.wantRule {
t.Errorf("parseIgnoreComment(%q): rule = %q, want %q", tt.line, rule, tt.wantRule)
}
}
}
Loading
Loading