diff --git a/.golangci.yaml b/.golangci.yaml index 0b59d2ccb..e1e70eeb4 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -73,6 +73,11 @@ linters: - whitespace - wrapcheck settings: + dupl: + # Token threshold for duplication detection + # Lower = more sensitive (catches smaller duplicates) + # Default is 150. We use 75 for lint (blocks CI), `mise run dup` uses 50 (advisory) + threshold: 75 errcheck: check-type-assertions: true check-blank: true diff --git a/CLAUDE.md b/CLAUDE.md index 59ede07aa..2d52377eb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,6 +47,27 @@ Integration tests use the `//go:build integration` build tag and are located in mise run fmt && mise run lint ``` +### Code Duplication Prevention + +Before implementing Go code, use `/go:discover-related` to find existing utilities and patterns that might be reusable. + +**Check for duplication:** +```bash +mise run dup # Comprehensive check (threshold 50) with summary +mise run dup:staged # Check only staged files +mise run lint # Normal lint includes dupl at threshold 75 (new issues only) +mise run lint:full # All issues at threshold 75 +``` + +**Tiered thresholds:** +- **75 tokens** (lint/CI) - Blocks on serious duplication (~20+ lines) +- **50 tokens** (dup) - Advisory, catches smaller patterns (~10+ lines) + +When duplication is found: +1. Check if a helper already exists in `common.go` or nearby utility files +2. If not, consider extracting the duplicated logic to a shared helper +3. If duplication is intentional (e.g., test setup), add a `//nolint:dupl` comment with explanation + ## Code Patterns ### Error Handling diff --git a/mise.toml b/mise.toml index 47033e993..7386a1166 100644 --- a/mise.toml +++ b/mise.toml @@ -52,4 +52,56 @@ mkdir completions for sh in bash zsh fish; do go run ./cmd/entire/main.go completion "$sh" >"completions/entire.$sh" done -""" \ No newline at end of file +""" + +[tasks.dup] +description = "Check for code duplication (threshold 50, with summary)" +run = """ +#!/usr/bin/env bash +set -euo pipefail + +# Create temp files with proper extensions (works on both Linux and macOS) +tmpdir=$(mktemp -d) +config="$tmpdir/config.yaml" +json_out="$tmpdir/output.json" + +cat > "$config" << 'YAML' +version: "2" +linters: + default: none + enable: [dupl] + settings: + dupl: + threshold: 50 +YAML + +# Run with JSON output for summary, text output for details +golangci-lint run -c "$config" --new=false --max-issues-per-linter=0 --max-same-issues=0 \ + --output.json.path="$json_out" --output.text.path=/dev/stderr ./... 2>&1 || true + +# Print summary grouped by file +echo "" +echo "=== Duplication Summary (by file) ===" +if command -v jq &>/dev/null && [ -s "$json_out" ]; then + jq -r '.Issues // [] | group_by(.Pos.Filename) | map({file: (.[0].Pos.Filename | split("/") | .[-1]), count: length}) | sort_by(-.count) | .[] | " " + (.count|tostring) + " " + .file' "$json_out" 2>/dev/null || echo " (no issues)" +else + echo " (install jq for summary)" +fi + +rm -rf "$tmpdir" +""" + +[tasks."dup:staged"] +description = "Check duplication in staged files only (threshold 75, same as CI)" +run = """ +#!/usr/bin/env bash +set -euo pipefail + +# Get staged Go files, preserving paths with spaces using null delimiters throughout +if ! git diff --cached --name-only -z --diff-filter=ACM | grep -z '\\.go$' | grep -zq .; then + echo "No staged Go files to check" + exit 0 +fi +echo "Checking staged files for duplication..." +git diff --cached --name-only -z --diff-filter=ACM | grep -z '\\.go$' | xargs -0 golangci-lint run --enable-only dupl --new=false --max-issues-per-linter=0 --max-same-issues=0 +"""