pre-commit-lint.sh 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. #!/bin/bash
  2. # hooks/pre-commit-lint.sh
  3. # PreToolUse hook - runs linter on staged files before commit
  4. # Matcher: Bash (when command contains "git commit")
  5. #
  6. # Configuration in .claude/settings.json:
  7. # {
  8. # "hooks": {
  9. # "PreToolUse": [{
  10. # "matcher": "Bash",
  11. # "hooks": ["bash hooks/pre-commit-lint.sh $TOOL_INPUT"]
  12. # }]
  13. # }
  14. # }
  15. INPUT="$1"
  16. # Only trigger on git commit commands
  17. if ! echo "$INPUT" | grep -qE 'git\s+commit'; then
  18. exit 0
  19. fi
  20. # Collect staged files
  21. STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null)
  22. if [[ -z "$STAGED_FILES" ]]; then
  23. exit 0
  24. fi
  25. ERRORS=0
  26. lint_js() {
  27. local files
  28. files=$(echo "$STAGED_FILES" | grep -E '\.(ts|tsx|js|jsx|mjs|cjs)$')
  29. if [[ -n "$files" ]]; then
  30. if command -v npx &>/dev/null && [[ -f "node_modules/.bin/eslint" ]]; then
  31. echo "Linting JS/TS files..."
  32. echo "$files" | xargs npx eslint --max-warnings 0 2>/dev/null
  33. return $?
  34. elif command -v biome &>/dev/null; then
  35. echo "Linting JS/TS files with Biome..."
  36. echo "$files" | xargs biome check 2>/dev/null
  37. return $?
  38. fi
  39. fi
  40. return 0
  41. }
  42. lint_python() {
  43. local files
  44. files=$(echo "$STAGED_FILES" | grep -E '\.py$')
  45. if [[ -n "$files" ]]; then
  46. if command -v ruff &>/dev/null; then
  47. echo "Linting Python files..."
  48. echo "$files" | xargs ruff check 2>/dev/null
  49. return $?
  50. elif command -v flake8 &>/dev/null; then
  51. echo "$files" | xargs flake8 2>/dev/null
  52. return $?
  53. fi
  54. fi
  55. return 0
  56. }
  57. lint_go() {
  58. local files
  59. files=$(echo "$STAGED_FILES" | grep -E '\.go$')
  60. if [[ -n "$files" ]]; then
  61. if command -v golangci-lint &>/dev/null; then
  62. echo "Linting Go files..."
  63. golangci-lint run --new-from-rev=HEAD 2>/dev/null
  64. return $?
  65. elif command -v go &>/dev/null; then
  66. go vet ./... 2>/dev/null
  67. return $?
  68. fi
  69. fi
  70. return 0
  71. }
  72. lint_rust() {
  73. local files
  74. files=$(echo "$STAGED_FILES" | grep -E '\.rs$')
  75. if [[ -n "$files" ]]; then
  76. if command -v cargo &>/dev/null && [[ -f "Cargo.toml" ]]; then
  77. echo "Linting Rust files..."
  78. cargo clippy --all-targets -- -D warnings 2>/dev/null
  79. return $?
  80. fi
  81. fi
  82. return 0
  83. }
  84. lint_php() {
  85. local files
  86. files=$(echo "$STAGED_FILES" | grep -E '\.php$')
  87. if [[ -n "$files" ]]; then
  88. if command -v ./vendor/bin/pint &>/dev/null; then
  89. echo "Linting PHP files..."
  90. echo "$files" | xargs ./vendor/bin/pint --test 2>/dev/null
  91. return $?
  92. elif command -v php &>/dev/null; then
  93. for f in $files; do
  94. php -l "$f" 2>/dev/null || return 1
  95. done
  96. fi
  97. fi
  98. return 0
  99. }
  100. # Run all applicable linters
  101. lint_js || ERRORS=$((ERRORS + 1))
  102. lint_python || ERRORS=$((ERRORS + 1))
  103. lint_go || ERRORS=$((ERRORS + 1))
  104. lint_rust || ERRORS=$((ERRORS + 1))
  105. lint_php || ERRORS=$((ERRORS + 1))
  106. if [[ $ERRORS -gt 0 ]]; then
  107. echo ""
  108. echo "LINT FAILED: $ERRORS linter(s) reported issues."
  109. echo "Fix the issues above before committing."
  110. exit 1
  111. fi
  112. exit 0