From ddcbeffa7491e1cc0c627a72604b644061745af1 Mon Sep 17 00:00:00 2001 From: Corey Schuman Date: Mon, 5 Jan 2026 22:27:33 -0500 Subject: [PATCH] docs: add comprehensive OSS community infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add standard files expected of a mature open source project: Community files: - CONTRIBUTING.md: Development setup, adding rules, PR guidelines - CODE_OF_CONDUCT.md: Contributor Covenant v2.0 - SECURITY.md: Vulnerability reporting policy Build & Development: - Makefile: build, test, lint, coverage, release targets - .editorconfig: Consistent formatting across editors GitHub templates: - .github/ISSUE_TEMPLATE/bug_report.md - .github/ISSUE_TEMPLATE/feature_request.md - .github/ISSUE_TEMPLATE/config.yml - .github/PULL_REQUEST_TEMPLATE.md Examples: - examples/allocation.go: Slice/map/string patterns - examples/algorithm.go: O(n²) loop patterns - examples/database.go: N+1 query patterns - examples/concurrency.go: Mutex/goroutine patterns - examples/io.go: Buffering/file patterns - examples/http.go: HTTP client patterns Documentation improvements: - README.md: More badges, links to new docs - rules/types.go: Comprehensive GoDoc comments --- .editorconfig | 35 +++++ .github/ISSUE_TEMPLATE/bug_report.md | 50 +++++++ .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/feature_request.md | 47 ++++++ .github/PULL_REQUEST_TEMPLATE.md | 62 ++++++++ CODE_OF_CONDUCT.md | 129 ++++++++++++++++ CONTRIBUTING.md | 165 ++++++++++++++++++++ Makefile | 98 ++++++++++++ README.md | 13 +- SECURITY.md | 67 +++++++++ examples/README.md | 60 ++++++++ examples/algorithm.go | 59 ++++++++ examples/allocation.go | 62 ++++++++ examples/concurrency.go | 104 +++++++++++++ examples/database.go | 94 ++++++++++++ examples/http.go | 80 ++++++++++ examples/io.go | 87 +++++++++++ rules/types.go | 174 ++++++++++++++++++---- 18 files changed, 1366 insertions(+), 28 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Makefile create mode 100644 SECURITY.md create mode 100644 examples/README.md create mode 100644 examples/algorithm.go create mode 100644 examples/allocation.go create mode 100644 examples/concurrency.go create mode 100644 examples/database.go create mode 100644 examples/http.go create mode 100644 examples/io.go diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..da23f49 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,35 @@ +# EditorConfig helps maintain consistent coding styles +# https://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = tab +indent_size = 4 + +[*.go] +indent_style = tab + +[*.md] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = false + +[*.yml] +indent_style = space +indent_size = 2 + +[*.yaml] +indent_style = space +indent_size = 2 + +[*.json] +indent_style = space +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..41db9f7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,50 @@ +--- +name: Bug Report +about: Report a bug in goperf +title: '[BUG] ' +labels: bug +assignees: '' +--- + +## Describe the Bug + +A clear and concise description of what the bug is. + +## To Reproduce + +Steps to reproduce the behavior: + +1. Create a file with this code: +```go +// paste code here +``` + +2. Run goperf: +```bash +goperf ./... +``` + +3. See error/unexpected output + +## Expected Behavior + +A clear and concise description of what you expected to happen. + +## Actual Behavior + +What actually happened. Include any error messages or unexpected output. + +## Environment + +- **goperf version**: (run `goperf --version`) +- **Go version**: (run `go version`) +- **OS**: (e.g., macOS 14.0, Ubuntu 22.04, Windows 11) +- **Architecture**: (e.g., amd64, arm64) + +## Additional Context + +Add any other context about the problem here. + +## Possible Fix + +If you have suggestions on how to fix the bug, please describe them here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..5d496d3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Questions & Discussions + url: https://github.com/unsaid-dev/goperf/discussions + about: Ask questions and discuss goperf usage + - name: Security Issues + url: https://github.com/unsaid-dev/goperf/blob/main/SECURITY.md + about: Report security vulnerabilities privately diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..6645ab4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,47 @@ +--- +name: Feature Request +about: Suggest a new feature or improvement +title: '[FEATURE] ' +labels: enhancement +assignees: '' +--- + +## Problem Statement + +A clear and concise description of the problem or limitation you're facing. + +Example: "I'm frustrated when goperf doesn't detect X pattern..." + +## Proposed Solution + +A clear description of what you want to happen. + +## Example Code + +If applicable, provide example Go code that demonstrates the pattern you want detected: + +```go +// This pattern should be detected +func example() { + // code here +} +``` + +## Expected goperf Output + +What should goperf report for this pattern? + +``` +file.go:10:5: [medium] description of issue (category) + Suggestion: how to fix it +``` + +## Alternatives Considered + +A description of alternative solutions or features you've considered. + +## Additional Context + +- Is this pattern documented in any Go performance guides? +- How common is this pattern in real codebases? +- Links to relevant resources, blog posts, or documentation diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..73d9c47 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,62 @@ +## Description + +Brief description of the changes in this PR. + +## Type of Change + +- [ ] Bug fix (non-breaking change that fixes an issue) +- [ ] New feature (non-breaking change that adds functionality) +- [ ] New rule (adds a new performance pattern detector) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) +- [ ] Documentation update +- [ ] Refactoring (no functional changes) + +## Related Issues + +Fixes #(issue number) + +## Changes Made + +- Change 1 +- Change 2 +- Change 3 + +## Testing + +- [ ] I have run `make test` and all tests pass +- [ ] I have run `make lint` with no new warnings +- [ ] I have run `make audit` (dogfooding) and reviewed the output +- [ ] I have added tests for new functionality +- [ ] I have tested manually with sample code + +## For New Rules + +If adding a new detection rule: + +- [ ] Rule is registered in `init()` +- [ ] Rule has corresponding tests in `*_test.go` +- [ ] Rule is documented in README.md +- [ ] Example code is provided in `examples/` + +## Documentation + +- [ ] I have updated the README if needed +- [ ] I have added/updated GoDoc comments for exported functions +- [ ] I have updated CHANGELOG.md + +## Screenshots/Output + +If applicable, show example output: + +``` +$ goperf ./examples/ +examples/sample.go:10:5: [medium] description (category) + Suggestion: how to fix +``` + +## Checklist + +- [ ] My code follows the project's code style +- [ ] I have performed a self-review of my code +- [ ] I have commented my code where necessary +- [ ] My changes generate no new warnings diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..81d26c8 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,129 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +**security@unsaid.io**. + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0afab3c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,165 @@ +# Contributing to goperf + +Thank you for your interest in contributing to goperf! This document provides guidelines and information for contributors. + +## Code of Conduct + +This project adheres to the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. + +## How to Contribute + +### Reporting Bugs + +Before creating a bug report, please check existing issues to avoid duplicates. When creating a bug report, include: + +- **Go version** (`go version`) +- **OS and architecture** +- **goperf version** (`goperf --version`) +- **Minimal reproducible example** +- **Expected vs actual behavior** + +### Suggesting Features + +Feature requests are welcome! Please include: + +- **Use case**: What problem does this solve? +- **Proposed solution**: How should it work? +- **Alternatives considered**: What else did you think about? + +### Pull Requests + +1. **Fork the repository** and create your branch from `main` +2. **Write tests** for any new functionality +3. **Run the test suite**: `make test` +4. **Run the linter**: `make lint` +5. **Run self-audit**: `make audit` (we dogfood!) +6. **Update documentation** if needed +7. **Write a clear PR description** + +## Development Setup + +```bash +# Clone your fork +git clone https://github.com/YOUR_USERNAME/goperf.git +cd goperf + +# Install dependencies +go mod download + +# Build +make build + +# Run tests +make test + +# Run linter +make lint + +# Self-audit (dogfooding) +make audit +``` + +## Project Structure + +``` +goperf/ +├── main.go # CLI entry point +├── rules/ # Detection rules +│ ├── analyzer.go # Core analysis engine +│ ├── types.go # Issue, Severity types +│ ├── algorithm.go # O(n²) detection +│ ├── allocation.go # Memory allocation patterns +│ ├── database.go # N+1 query detection +│ └── ... +├── fixer/ # Auto-fix suggestions +├── reporter/ # Output formatters (console, JSON) +└── examples/ # Example problematic code +``` + +## Adding a New Rule + +1. **Create or edit a file** in `rules/` (e.g., `rules/mypattern.go`) + +2. **Implement the Rule interface**: +```go +type MyRule struct{} + +func (r *MyRule) Name() string { return "my-rule-name" } +func (r *MyRule) Category() string { return "category" } + +func (r *MyRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue { + issues := make([]Issue, 0, 4) + // Your detection logic here + return issues +} +``` + +3. **Register in init()**: +```go +func init() { + RegisterRule("category", &MyRule{}) +} +``` + +4. **Add tests** in `rules/mypattern_test.go` + +5. **Update documentation** in README.md + +## Code Style + +- Follow standard Go conventions (`gofmt`, `go vet`) +- Use meaningful variable names +- Add comments for non-obvious logic +- Preallocate slices when size is known: `make([]T, 0, n)` +- Keep functions focused and small + +## Testing + +```bash +# Run all tests +make test + +# Run with coverage +make coverage + +# Run specific test +go test -v -run TestMyRule ./rules/ +``` + +## Commit Messages + +Follow conventional commits: + +- `feat:` New feature +- `fix:` Bug fix +- `docs:` Documentation only +- `test:` Adding tests +- `refactor:` Code change that neither fixes a bug nor adds a feature +- `perf:` Performance improvement +- `chore:` Maintenance tasks + +Examples: +``` +feat(rules): add detection for sync.Pool misuse +fix(analyzer): handle nil pointer in nested loops +docs: update contributing guide +``` + +## Release Process + +Releases are automated via GitHub Actions when a tag is pushed: + +```bash +git tag -a v0.2.0 -m "v0.2.0 - Description" +git push origin v0.2.0 +``` + +## Getting Help + +- **Questions**: Open a [Discussion](https://github.com/unsaid-dev/goperf/discussions) +- **Bugs**: Open an [Issue](https://github.com/unsaid-dev/goperf/issues) +- **Security**: See [SECURITY.md](SECURITY.md) + +## License + +By contributing, you agree that your contributions will be licensed under the MIT License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d904d0a --- /dev/null +++ b/Makefile @@ -0,0 +1,98 @@ +.PHONY: build test lint audit coverage clean install help + +# Binary name +BINARY=goperf + +# Go parameters +GOCMD=go +GOBUILD=$(GOCMD) build +GOTEST=$(GOCMD) test +GOGET=$(GOCMD) get +GOMOD=$(GOCMD) mod +GOFMT=gofmt + +# Build info +VERSION?=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") +LDFLAGS=-ldflags "-X main.version=$(VERSION)" + +## help: Show this help message +help: + @echo "goperf - Performance Pattern Detector for Go" + @echo "" + @echo "Usage: make [target]" + @echo "" + @echo "Targets:" + @grep -E '^## ' $(MAKEFILE_LIST) | sed 's/## / /' + +## build: Build the binary +build: + $(GOBUILD) $(LDFLAGS) -o $(BINARY) . + +## install: Install to GOPATH/bin +install: + $(GOCMD) install $(LDFLAGS) . + +## test: Run tests +test: + $(GOTEST) -v -race ./... + +## coverage: Run tests with coverage +coverage: + $(GOTEST) -v -race -coverprofile=coverage.out -covermode=atomic ./... + $(GOCMD) tool cover -html=coverage.out -o coverage.html + @echo "Coverage report: coverage.html" + +## lint: Run golangci-lint +lint: + @which golangci-lint > /dev/null || (echo "Installing golangci-lint..." && go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest) + golangci-lint run ./... + +## fmt: Format code +fmt: + $(GOFMT) -s -w . + +## vet: Run go vet +vet: + $(GOCMD) vet ./... + +## audit: Run goperf on itself (dogfooding) +audit: build + ./$(BINARY) ./... + +## audit-strict: Run goperf with strict settings +audit-strict: build + ./$(BINARY) --fail-on=medium ./... + +## clean: Remove build artifacts +clean: + rm -f $(BINARY) + rm -f $(BINARY)-* + rm -f coverage.out coverage.html + rm -f *.prof + +## deps: Download dependencies +deps: + $(GOMOD) download + $(GOMOD) tidy + +## check: Run all checks (test, lint, vet, audit) +check: test lint vet audit + @echo "All checks passed!" + +## release: Build release binaries for all platforms +release: clean + GOOS=linux GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BINARY)-linux-amd64 . + GOOS=linux GOARCH=arm64 $(GOBUILD) $(LDFLAGS) -o $(BINARY)-linux-arm64 . + GOOS=darwin GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BINARY)-darwin-amd64 . + GOOS=darwin GOARCH=arm64 $(GOBUILD) $(LDFLAGS) -o $(BINARY)-darwin-arm64 . + GOOS=windows GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BINARY)-windows-amd64.exe . + @echo "Release binaries built:" + @ls -la $(BINARY)-* + +## docker: Build Docker image +docker: + docker build -t goperf:$(VERSION) . + +## example: Run goperf on examples directory +example: build + ./$(BINARY) ./examples/... diff --git a/README.md b/README.md index 48091e9..173e185 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,11 @@ **Preventive performance analysis for Go** - Catch O(n²) loops, N+1 queries, and other performance anti-patterns before they hit production. +[![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go)](https://go.dev) [![Go Report Card](https://goreportcard.com/badge/github.com/unsaid-dev/goperf)](https://goreportcard.com/report/github.com/unsaid-dev/goperf) +[![GoDoc](https://pkg.go.dev/badge/github.com/unsaid-dev/goperf)](https://pkg.go.dev/github.com/unsaid-dev/goperf) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) ## Why goperf? @@ -218,7 +221,9 @@ This demonstrates that `goperf` finds real issues - including in itself - and th ## Contributing -Contributions welcome! Areas we'd love help with: +Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + +Areas we'd love help with: - [ ] More detection rules - [ ] False positive reduction @@ -226,6 +231,12 @@ Contributions welcome! Areas we'd love help with: - [ ] Benchmark integration - [ ] Auto-fix suggestions +Please read our [Code of Conduct](CODE_OF_CONDUCT.md) before contributing. + +## Security + +Found a security issue? Please report it responsibly. See [SECURITY.md](SECURITY.md) for details. + ## License MIT License - see [LICENSE](LICENSE) for details. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..cd965cf --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,67 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 0.1.x | :white_check_mark: | + +## Reporting a Vulnerability + +We take security seriously. If you discover a security vulnerability in goperf, please report it responsibly. + +### How to Report + +**DO NOT** create a public GitHub issue for security vulnerabilities. + +Instead, please email **security@unsaid.io** with: + +1. **Description** of the vulnerability +2. **Steps to reproduce** the issue +3. **Potential impact** assessment +4. **Suggested fix** (if you have one) + +### What to Expect + +- **Acknowledgment**: We will acknowledge receipt within 48 hours +- **Assessment**: We will assess the vulnerability within 7 days +- **Fix timeline**: Critical issues will be addressed within 30 days +- **Disclosure**: We will coordinate disclosure timing with you + +### Scope + +Security issues we care about: + +- **Code execution**: Arbitrary code execution via malicious Go files +- **Path traversal**: Reading/writing files outside the target directory +- **Denial of service**: Crashes or hangs on crafted input +- **Information disclosure**: Leaking sensitive information + +### Out of Scope + +- Issues in dependencies (report to the dependency maintainer) +- Issues requiring physical access to the machine +- Social engineering attacks + +### Recognition + +We appreciate responsible disclosure and will: + +- Credit you in the release notes (unless you prefer anonymity) +- Add you to our security acknowledgments + +## Security Design + +goperf is designed with security in mind: + +1. **Read-only by default**: Only analyzes code, doesn't modify unless `--fix` is used +2. **Path validation**: Refuses to operate outside the current working directory +3. **Symlink protection**: Validates symlinks don't escape the working directory +4. **Resource limits**: Caps file count, file size, and directory depth +5. **No network access**: Operates entirely locally + +## Best Practices for Users + +1. **Review before fixing**: Always use `--dry-run` before `--fix` +2. **Trust but verify**: Review auto-fix suggestions before applying +3. **Keep updated**: Use the latest version for security fixes diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..91aa332 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,60 @@ +# goperf Examples + +This directory contains sample Go code demonstrating performance anti-patterns that `goperf` can detect. + +## Running goperf on Examples + +```bash +# From the project root +goperf ./examples/... + +# Or with the Makefile +make example +``` + +## Example Files + +| File | Patterns Demonstrated | +|------|----------------------| +| `allocation.go` | Slice preallocation, map size hints, string concatenation | +| `algorithm.go` | O(n*m) nested loops, quadratic complexity | +| `database.go` | N+1 queries, queries in loops | +| `concurrency.go` | Mutex in loops, unbounded goroutines, goroutine leaks | +| `io.go` | Unbuffered I/O, small buffers, repeated file opens | +| `http.go` | HTTP client in loops, response body leaks | + +## Pattern Structure + +Each file contains: + +1. **Bad Example**: Function demonstrating the anti-pattern +2. **Good Example**: Function showing the corrected version +3. **Comments**: Explaining why it's a problem and how to fix it + +## Sample Output + +``` +$ goperf ./examples/... + +examples/allocation.go:11:2: [medium] slice 'results' appended in loop without preallocation (allocation) + Suggestion: preallocate with make([]string, 0, len(items)) + +examples/allocation.go:23:2: [low] map created without size hint (allocation) + Suggestion: use make(map[string]Item, len(items)) for known size + +examples/database.go:19:3: [high] potential N+1 query: SQL query inside loop (database) + Suggestion: batch queries or use JOIN + +examples/concurrency.go:15:3: [medium] mutex Lock() called inside loop body (concurrency) + Suggestion: consider restructuring to reduce lock contention + +Found 15 issues (3 high, 5 medium, 7 low) +``` + +## Adding Examples + +When contributing new detection rules, please: + +1. Add example code here demonstrating the pattern +2. Include both BAD (anti-pattern) and GOOD (fixed) versions +3. Add clear comments explaining the performance impact diff --git a/examples/algorithm.go b/examples/algorithm.go new file mode 100644 index 0000000..4ea530d --- /dev/null +++ b/examples/algorithm.go @@ -0,0 +1,59 @@ +// Package examples contains sample code demonstrating performance anti-patterns. +package examples + +// NestedLoopLookup demonstrates O(n*m) complexity that could be O(n+m). +// This pattern is common when checking membership in a slice. +func NestedLoopLookup(users []User, allowedIDs []string) []User { + var allowed []User + for _, user := range users { // O(n) + for _, id := range allowedIDs { // O(m) - BAD: linear search + if user.ID == id { + allowed = append(allowed, user) + break + } + } + } + return allowed +} + +// NestedLoopLookupFixed shows the O(n+m) version using a map. +func NestedLoopLookupFixed(users []User, allowedIDs []string) []User { + // Build lookup map: O(m) + allowedSet := make(map[string]bool, len(allowedIDs)) + for _, id := range allowedIDs { + allowedSet[id] = true + } + + // Filter users: O(n) + allowed := make([]User, 0, len(users)) + for _, user := range users { + if allowedSet[user.ID] { // O(1) lookup - GOOD + allowed = append(allowed, user) + } + } + return allowed +} + +// QuadraticStringMatch demonstrates O(n*m) substring matching. +func QuadraticStringMatch(texts []string, patterns []string) []string { + var matches []string + for _, text := range texts { // O(n) + for _, pattern := range patterns { // O(m) - potentially O(n²) + if containsPattern(text, pattern) { + matches = append(matches, text) + break + } + } + } + return matches +} + +// User is a sample data type. +type User struct { + ID string + Name string +} + +func containsPattern(text, pattern string) bool { + return false // placeholder +} diff --git a/examples/allocation.go b/examples/allocation.go new file mode 100644 index 0000000..c8c32e7 --- /dev/null +++ b/examples/allocation.go @@ -0,0 +1,62 @@ +// Package examples contains sample code demonstrating performance anti-patterns +// that goperf can detect. Run: goperf ./examples/... +package examples + +// SliceAppendInLoop demonstrates the slice growth anti-pattern. +// Each append may cause a reallocation and copy. +// goperf suggests: preallocate with make([]string, 0, len(items)) +func SliceAppendInLoop(items []int) []string { + var results []string // BAD: no preallocation + for _, item := range items { + results = append(results, processItem(item)) + } + return results +} + +// SliceAppendFixed shows the corrected version. +func SliceAppendFixed(items []int) []string { + results := make([]string, 0, len(items)) // GOOD: preallocated + for _, item := range items { + results = append(results, processItem(item)) + } + return results +} + +// MapWithoutSizeHint demonstrates map growth overhead. +// Maps resize when they exceed their capacity. +func MapWithoutSizeHint(items []Item) map[string]Item { + result := make(map[string]Item) // BAD: no size hint + for _, item := range items { + result[item.ID] = item + } + return result +} + +// MapWithSizeHint shows the corrected version. +func MapWithSizeHint(items []Item) map[string]Item { + result := make(map[string]Item, len(items)) // GOOD: size hint + for _, item := range items { + result[item.ID] = item + } + return result +} + +// StringConcatInLoop demonstrates inefficient string building. +// String concatenation creates new strings each iteration. +func StringConcatInLoop(parts []string) string { + var result string // BAD: string concatenation in loop + for _, part := range parts { + result += part + } + return result +} + +// Item is a sample data type for examples. +type Item struct { + ID string + Name string +} + +func processItem(i int) string { + return "" +} diff --git a/examples/concurrency.go b/examples/concurrency.go new file mode 100644 index 0000000..e95349f --- /dev/null +++ b/examples/concurrency.go @@ -0,0 +1,104 @@ +// Package examples contains sample code demonstrating performance anti-patterns. +package examples + +import ( + "sync" + "time" +) + +// MutexInLoop demonstrates acquiring a lock on every iteration. +// This serializes all goroutines and defeats concurrency. +func MutexInLoop(items []int) int { + var mu sync.Mutex + var sum int + + for _, item := range items { + mu.Lock() // BAD: lock acquired every iteration + sum += item + mu.Unlock() + } + return sum +} + +// MutexBatched shows a better approach - batch updates. +func MutexBatched(items []int) int { + var mu sync.Mutex + var sum int + + // Calculate locally first + localSum := 0 + for _, item := range items { + localSum += item + } + + // Single lock for final update - GOOD + mu.Lock() + sum += localSum + mu.Unlock() + + return sum +} + +// UnboundedGoroutines spawns goroutines without limits. +// This can exhaust memory and overwhelm the scheduler. +func UnboundedGoroutines(items []string) { + for _, item := range items { // BAD: unbounded goroutine creation + go processAsync(item) + } +} + +// BoundedGoroutines uses a worker pool pattern. +func BoundedGoroutines(items []string) { + const maxWorkers = 10 + sem := make(chan struct{}, maxWorkers) + + var wg sync.WaitGroup + for _, item := range items { + sem <- struct{}{} // Acquire semaphore - GOOD: bounded + wg.Add(1) + go func(s string) { + defer wg.Done() + defer func() { <-sem }() + processAsync(s) + }(item) + } + wg.Wait() +} + +// GoroutineLeak demonstrates a goroutine that may never terminate. +func GoroutineLeak(ch chan int) { + go func() { // BAD: no way to cancel this goroutine + for { + val := <-ch // Blocks forever if ch is never closed + _ = val + } + }() +} + +// GoroutineWithContext shows proper cancellation. +func GoroutineWithContext(ch chan int, done chan struct{}) { + go func() { // GOOD: can be cancelled via done channel + for { + select { + case val := <-ch: + _ = val + case <-done: + return + } + } + }() +} + +// TimeTickerLeak creates a ticker that's never stopped. +func TimeTickerLeak() { + ticker := time.NewTicker(time.Second) // BAD: ticker never stopped + go func() { + for range ticker.C { + // do something + } + }() +} + +func processAsync(s string) { + // placeholder +} diff --git a/examples/database.go b/examples/database.go new file mode 100644 index 0000000..692d255 --- /dev/null +++ b/examples/database.go @@ -0,0 +1,94 @@ +// Package examples contains sample code demonstrating performance anti-patterns. +package examples + +import ( + "context" + "database/sql" +) + +// NPlusOneQuery demonstrates the N+1 query anti-pattern. +// This executes 1 query for users + N queries for their orders. +func NPlusOneQuery(ctx context.Context, db *sql.DB) ([]UserWithOrders, error) { + // Query 1: Get all users + rows, err := db.QueryContext(ctx, "SELECT id, name FROM users") + if err != nil { + return nil, err + } + defer rows.Close() + + var results []UserWithOrders + for rows.Next() { + var u UserWithOrders + if err := rows.Scan(&u.ID, &u.Name); err != nil { + return nil, err + } + + // BAD: Query N+1, N+2, ... for each user's orders + orderRows, err := db.QueryContext(ctx, + "SELECT id, amount FROM orders WHERE user_id = ?", u.ID) + if err != nil { + return nil, err + } + + for orderRows.Next() { + var o Order + if err := orderRows.Scan(&o.ID, &o.Amount); err != nil { + orderRows.Close() + return nil, err + } + u.Orders = append(u.Orders, o) + } + orderRows.Close() + + results = append(results, u) + } + return results, nil +} + +// BatchQuery shows the corrected version using a JOIN or batch query. +func BatchQuery(ctx context.Context, db *sql.DB) ([]UserWithOrders, error) { + // GOOD: Single query with JOIN + rows, err := db.QueryContext(ctx, ` + SELECT u.id, u.name, o.id, o.amount + FROM users u + LEFT JOIN orders o ON u.id = o.user_id + ORDER BY u.id + `) + if err != nil { + return nil, err + } + defer rows.Close() + + // Process joined results... + var results []UserWithOrders + // (implementation omitted for brevity) + _ = rows + return results, nil +} + +// QueryInLoop demonstrates query execution inside a loop. +func QueryInLoop(ctx context.Context, db *sql.DB, userIDs []string) ([]User, error) { + var users []User + for _, id := range userIDs { // BAD: N queries + row := db.QueryRowContext(ctx, "SELECT id, name FROM users WHERE id = ?", id) + var u User + if err := row.Scan(&u.ID, &u.Name); err != nil { + continue + } + users = append(users, u) + } + return users, nil +} + +// UserWithOrders contains a user and their orders. +type UserWithOrders struct { + ID string + Name string + Orders []Order +} + +// Order represents an order. +type Order struct { + ID string + Amount float64 +} diff --git a/examples/http.go b/examples/http.go new file mode 100644 index 0000000..f2c676f --- /dev/null +++ b/examples/http.go @@ -0,0 +1,80 @@ +// Package examples contains sample code demonstrating performance anti-patterns. +package examples + +import ( + "io" + "net/http" +) + +// HTTPClientInLoop creates a new client for each request. +// This defeats connection pooling and reuse. +func HTTPClientInLoop(urls []string) error { + for _, url := range urls { + client := &http.Client{} // BAD: new client each iteration + resp, err := client.Get(url) + if err != nil { + return err + } + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + } + return nil +} + +// HTTPClientReused shares a single client across requests. +func HTTPClientReused(urls []string) error { + client := &http.Client{} // GOOD: client reused + for _, url := range urls { + resp, err := client.Get(url) + if err != nil { + return err + } + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + } + return nil +} + +// ResponseBodyNotClosed forgets to close the response body. +// This leaks connections and file descriptors. +func ResponseBodyNotClosed(url string) ([]byte, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + // BAD: resp.Body never closed + return io.ReadAll(resp.Body) +} + +// ResponseBodyClosed properly closes the response body. +func ResponseBodyClosed(url string) ([]byte, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() // GOOD: always close + return io.ReadAll(resp.Body) +} + +// ResponseBodyNotDrained doesn't fully read the body. +// This prevents connection reuse in the pool. +func ResponseBodyNotDrained(url string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + // BAD: body not fully read, connection can't be reused + return nil +} + +// ResponseBodyDrained fully reads and discards the body. +func ResponseBodyDrained(url string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + io.Copy(io.Discard, resp.Body) // GOOD: drain body for reuse + return nil +} diff --git a/examples/io.go b/examples/io.go new file mode 100644 index 0000000..66c060b --- /dev/null +++ b/examples/io.go @@ -0,0 +1,87 @@ +// Package examples contains sample code demonstrating performance anti-patterns. +package examples + +import ( + "bufio" + "io" + "os" +) + +// UnbufferedRead reads a file byte-by-byte. +// This causes excessive system calls. +func UnbufferedRead(filename string) ([]byte, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + var data []byte + buf := make([]byte, 1) // BAD: tiny buffer + for { + n, err := file.Read(buf) + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + if n > 0 { + data = append(data, buf[0]) + } + } + return data, nil +} + +// BufferedRead uses a buffered reader for efficiency. +func BufferedRead(filename string) ([]byte, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + reader := bufio.NewReader(file) // GOOD: buffered I/O + return io.ReadAll(reader) +} + +// SmallBufferCopy uses a tiny buffer for copying. +func SmallBufferCopy(dst io.Writer, src io.Reader) error { + buf := make([]byte, 64) // BAD: very small buffer + _, err := io.CopyBuffer(dst, src, buf) + return err +} + +// OptimalBufferCopy uses an appropriately sized buffer. +func OptimalBufferCopy(dst io.Writer, src io.Reader) error { + buf := make([]byte, 32*1024) // GOOD: 32KB buffer + _, err := io.CopyBuffer(dst, src, buf) + return err +} + +// ReadFileInLoop opens and reads a file on each iteration. +func ReadFileInLoop(filename string, count int) error { + for i := 0; i < count; i++ { + // BAD: opening file repeatedly + data, err := os.ReadFile(filename) + if err != nil { + return err + } + _ = data + } + return nil +} + +// ReadFileOnce reads the file once and reuses the data. +func ReadFileOnce(filename string, count int) error { + // GOOD: read once, use many times + data, err := os.ReadFile(filename) + if err != nil { + return err + } + + for i := 0; i < count; i++ { + _ = data + } + return nil +} diff --git a/rules/types.go b/rules/types.go index 8dfc663..214a460 100644 --- a/rules/types.go +++ b/rules/types.go @@ -1,3 +1,17 @@ +// Package rules provides the core types and interfaces for goperf's +// static analysis rules. Each rule detects specific performance anti-patterns +// in Go source code. +// +// Rules are organized by category (algorithm, allocation, database, etc.) +// and registered via the RegisterRule function during package initialization. +// +// Example usage: +// +// analyzer := rules.NewAnalyzer(rules.AnalyzerConfig{ +// Rules: []string{"algorithm", "database"}, +// Context: 3, +// }) +// issues := analyzer.Analyze("./...") package rules import ( @@ -5,13 +19,28 @@ import ( "go/token" ) -// Severity levels for issues +// Severity represents the impact level of a detected performance issue. +// Higher severity issues have greater performance impact and should +// be addressed with higher priority. type Severity int const ( + // SeverityLow indicates a minor optimization opportunity. + // These are "nice to have" improvements that may not have + // measurable impact in most applications. SeverityLow Severity = iota + + // SeverityMedium indicates a moderate performance concern. + // These issues should be addressed but may not be critical. SeverityMedium + + // SeverityHigh indicates a significant performance problem. + // These issues will likely cause noticeable slowdowns and + // should be fixed before release. SeverityHigh + + // SeverityCritical indicates a severe performance issue + // that will cause production problems. Fix immediately. SeverityCritical ) @@ -28,45 +57,111 @@ func (s Severity) String() string { } } -// Issue represents a detected performance issue +// Issue represents a detected performance anti-pattern in source code. +// Each issue includes location information, a description, an explanation +// of why it's a problem, and a suggested fix. type Issue struct { - Rule string `json:"rule"` - Category string `json:"category"` - Severity Severity `json:"severity"` - File string `json:"file"` - Line int `json:"line"` - Column int `json:"column"` - Message string `json:"message"` - Why string `json:"why"` - Fix string `json:"fix"` - CodeSnippet string `json:"code_snippet,omitempty"` - Context []string `json:"context,omitempty"` + // Rule is the identifier of the rule that detected this issue + // (e.g., "nested-loop", "append-in-loop", "sql-in-loop"). + Rule string `json:"rule"` + + // Category groups related rules together + // (e.g., "algorithm", "allocation", "database"). + Category string `json:"category"` + + // Severity indicates the impact level of this issue. + Severity Severity `json:"severity"` + + // File is the path to the source file containing the issue. + File string `json:"file"` + + // Line is the 1-indexed line number where the issue occurs. + Line int `json:"line"` + + // Column is the 1-indexed column number where the issue occurs. + Column int `json:"column"` + + // Message is a brief description of the detected issue. + Message string `json:"message"` + + // Why explains the performance impact of this pattern. + Why string `json:"why"` + + // Fix suggests how to resolve the issue. + Fix string `json:"fix"` + + // CodeSnippet contains the problematic line of code. + CodeSnippet string `json:"code_snippet,omitempty"` + + // Context contains surrounding lines of code for display. + Context []string `json:"context,omitempty"` } -// Rule is the interface all detection rules must implement +// Rule is the interface that all detection rules must implement. +// Rules are the core building blocks of goperf's analysis engine. +// +// To create a custom rule: +// +// 1. Implement the Rule interface +// 2. Register it using RegisterRule in an init() function +// 3. The analyzer will automatically pick it up +// +// Example: +// +// type MyRule struct{} +// +// func (r *MyRule) Name() string { return "my-pattern" } +// func (r *MyRule) Category() string { return "custom" } +// func (r *MyRule) Check(file *ast.File, fset *token.FileSet, src []byte) []Issue { +// // Analyze the AST and return issues +// return nil +// } +// +// func init() { +// RegisterRule("custom", &MyRule{}) +// } type Rule interface { + // Name returns a unique identifier for this rule (e.g., "append-in-loop"). Name() string + + // Category returns the rule's category (e.g., "allocation", "database"). Category() string + + // Check analyzes a Go source file and returns any detected issues. + // The file parameter is the parsed AST, fset provides position info, + // and src is the original source code for context extraction. Check(file *ast.File, fset *token.FileSet, src []byte) []Issue } -// AnalyzerConfig configures the analyzer +// AnalyzerConfig configures the behavior of the analyzer. type AnalyzerConfig struct { - Rules []string + // Rules is a list of rule categories to run. + // An empty list means all rules. Example: []string{"algorithm", "database"} + Rules []string + + // IgnorePaths is a list of path patterns to skip during analysis. + // Supports glob patterns. Example: []string{"vendor/**", "**/*_test.go"} IgnorePaths []string - Context int - Verbose bool + + // Context is the number of lines of code to include around each issue. + Context int + + // Verbose enables detailed output during analysis. + Verbose bool } -// RuleRegistry holds all available rules +// RuleRegistry maps category names to the rules in that category. +// Rules are added to this registry via RegisterRule during init(). var RuleRegistry = make(map[string][]Rule) -// RegisterRule adds a rule to the registry +// RegisterRule adds a rule to the registry under the specified category. +// This should be called from init() functions in rule implementation files. func RegisterRule(category string, rule Rule) { RuleRegistry[category] = append(RuleRegistry[category], rule) } -// Helper to extract code context around a position +// ExtractContext returns lines of code surrounding the given position. +// It returns up to contextLines lines before and after the specified position. func ExtractContext(src []byte, pos token.Position, contextLines int) []string { lines := splitLines(src) if pos.Line <= 0 || pos.Line > len(lines) { @@ -111,18 +206,43 @@ func splitLines(src []byte) []string { return lines } -// IgnoreSet tracks which lines should be ignored based on perf:ignore comments +// IgnoreSet tracks which lines should be ignored based on perf:ignore comments. +// This allows developers to suppress specific warnings when they've verified +// the code is intentional or when there's a false positive. type IgnoreSet struct { lines map[int]bool // Line-level ignores ranges [][2]int // Start/end ranges for block ignores rules map[int]string // Optional: specific rules to ignore per line } -// NewIgnoreSet parses source code for perf:ignore comments -// Supports: -// - // perf:ignore - ignore the next line or same line -// - // perf:ignore rule-name - ignore specific rule -// - // perf:ignore-start / // perf:ignore-end - block ignore +// NewIgnoreSet parses source code to find perf:ignore comments. +// It supports several ignore patterns: +// +// Line-level ignore (ignores the current and next line): +// +// // perf:ignore +// for _, item := range items { +// db.Exec(query, item) // This line is ignored +// } +// +// Same-line ignore: +// +// db.Exec(query, item) // perf:ignore +// +// Rule-specific ignore: +// +// // perf:ignore sql-in-loop +// for _, item := range items { +// db.Exec(query, item) // Only sql-in-loop is ignored +// } +// +// Block ignore (ignores all lines between start and end): +// +// // perf:ignore-start +// for _, item := range items { +// db.Exec(query, item) +// } +// // perf:ignore-end func NewIgnoreSet(src []byte) *IgnoreSet { is := &IgnoreSet{ lines: make(map[int]bool),