check-conventions.sh 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. #!/bin/sh
  2. #
  3. # ArozOS contribution convention checker
  4. # =====================================
  5. #
  6. # Enforces the contribution rules documented in CLAUDE.md against *new* code.
  7. # It is intentionally written in portable POSIX sh with only the standard
  8. # git/grep tooling so it runs the same way on a contributor's machine, inside
  9. # the Claude Code PostToolUse hook and in CI (rule 5: no system dependencies).
  10. #
  11. # Usage:
  12. # scripts/check-conventions.sh <file> [<file> ...] Check specific files
  13. # scripts/check-conventions.sh --diff <base-ref> Check files changed vs base-ref
  14. # scripts/check-conventions.sh --hook Read a Claude Code hook
  15. # payload (JSON) from stdin
  16. #
  17. # Exit status:
  18. # 0 no ERROR-level violations (WARN findings may still be printed)
  19. # 1 at least one ERROR-level violation was found (CI / direct invocation)
  20. # 2 findings in --hook mode (surfaces the report back to Claude)
  21. #
  22. # Escape hatch:
  23. # Append the marker arozos-lint-ignore to a source line to skip the
  24. # line-level checks (raw logger / hardcoded path) for that single line.
  25. # Use it only with a short justification comment.
  26. set -u
  27. errors=0
  28. warns=0
  29. err() { printf ' [ERROR] %s\n' "$1" >&2; errors=$((errors + 1)); }
  30. warn() { printf ' [WARN] %s\n' "$1" >&2; warns=$((warns + 1)); }
  31. repo_root=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
  32. # is_platform_file returns success when a Go file is scoped to a single OS/arch
  33. # by its filename suffix (e.g. foo_linux.go, bar_windows_amd64.go). Such files
  34. # are the project's sanctioned home for platform-specific code, so the
  35. # portability checks do not apply to them.
  36. is_platform_file() {
  37. printf '%s' "$1" | grep -Eq \
  38. '_(linux|windows|darwin|freebsd|openbsd|netbsd|dragonfly|solaris|illumos|aix|android|js|wasm|plan9)(_[a-z0-9]+)?\.go$'
  39. }
  40. has_build_constraint() {
  41. [ -f "$1" ] && grep -Eq '^//go:build|^// \+build' "$1"
  42. }
  43. # check_lines applies the per-line rules to a stream of source lines supplied on
  44. # stdin. $1 is the file the lines belong to (used for context + exemptions).
  45. check_lines() {
  46. file=$1
  47. # Skip the marker so opted-out lines are not re-flagged.
  48. scan=$(grep -v 'arozos-lint-ignore' || true)
  49. # --- Rule 1: managed logger, not the standard log package -------------
  50. # The logger package itself legitimately wraps "log"; everything else must
  51. # route through logger.PrintAndLog so output lands in the system log.
  52. case "$file" in
  53. *mod/info/logger/*) ;;
  54. *)
  55. hits=$(printf '%s\n' "$scan" |
  56. grep -E 'log\.(Print|Printf|Println|Fatal|Fatalf|Fatalln|Panic|Panicf|Panicln)\(' || true)
  57. if [ -n "$hits" ]; then
  58. err "$file: uses the standard \"log\" package. New code must call logger.PrintAndLog(title, message, err) instead (rule 1)."
  59. fi
  60. ;;
  61. esac
  62. # --- Rule 5: portability, no hardcoded OS paths -----------------------
  63. if ! is_platform_file "$file"; then
  64. paths=$(printf '%s\n' "$scan" |
  65. grep -E '"(/usr/|/etc/|/var/|/bin/|/sbin/|/opt/|/root/|/home/)|"[A-Za-z]:\\\\|"[A-Za-z]:/' || true)
  66. if [ -n "$paths" ]; then
  67. err "$file: contains a hardcoded OS path literal. Build paths with filepath.Join and os.TempDir/UserHomeDir so ArozOS stays cross-platform (rule 5)."
  68. fi
  69. fi
  70. # --- Rule 4: new HTTP endpoints need a deliberate security decision ---
  71. endpoints=$(printf '%s\n' "$scan" | grep -E 'http\.HandleFunc\(' || true)
  72. if [ -n "$endpoints" ]; then
  73. case "$file" in
  74. *mod/prouter/*) ;; # the permission router wraps http.HandleFunc by design
  75. *)
  76. warn "$file: registers an endpoint with raw http.HandleFunc. Prefer prout.NewModuleRouter(...).HandleFunc for auth/permission, or confirm the endpoint is intentionally public (rule 4)."
  77. ;;
  78. esac
  79. fi
  80. }
  81. # check_file applies the file-level rules to a single Go source file path.
  82. check_file() {
  83. file=$1
  84. case "$file" in
  85. *_test.go) return ;; # test files are not themselves subject to these rules
  86. esac
  87. # --- Rule 5 (soft): isolate platform calls in build-tagged files -----
  88. if ! is_platform_file "$file" && ! has_build_constraint "$file"; then
  89. if grep -Eq 'exec\.Command\(|(^|[^.])syscall\.' "$file" 2>/dev/null; then
  90. warn "$file: calls exec.Command/syscall in a cross-platform file. Move OS-specific code into a *_linux.go / *_windows.go / *_darwin.go file or guard it with a //go:build tag (rule 5)."
  91. fi
  92. fi
  93. # --- Rule 2: every package ships tests -------------------------------
  94. case "$file" in
  95. *mod/*)
  96. dir=$(dirname "$file")
  97. if ! ls "$dir"/*_test.go >/dev/null 2>&1; then
  98. warn "$file: package $dir has no *_test.go file. New functions must ship with tests (rule 2)."
  99. fi
  100. ;;
  101. esac
  102. }
  103. # license_reminder fires once when dependency manifests change.
  104. license_reminder() {
  105. warn "go.mod/go.sum changed: confirm every new dependency is MIT, BSD, Apache-2.0, MPL-2.0 or ISC (GPL-compatible and OK for commercial use). Reject GPL/AGPL/unknown-licensed modules (rule 3)."
  106. }
  107. scan_one() {
  108. file=$1
  109. case "$file" in
  110. go.mod | go.sum | */go.mod | */go.sum)
  111. license_reminder
  112. return
  113. ;;
  114. *.go) ;;
  115. *) return ;;
  116. esac
  117. # Single-file / hook mode scans the whole file content.
  118. check_lines "$file" <"$file"
  119. check_file "$file"
  120. }
  121. mode=${1:---help}
  122. case "$mode" in
  123. --hook)
  124. # Extract tool_input.file_path from the hook JSON payload on stdin without
  125. # requiring jq (rule 5: no extra system dependencies).
  126. payload=$(cat)
  127. file=$(printf '%s' "$payload" |
  128. sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)
  129. [ -z "$file" ] && exit 0
  130. scan_one "$file"
  131. if [ "$errors" -gt 0 ] || [ "$warns" -gt 0 ]; then
  132. printf '\nArozOS convention check: %d error(s), %d warning(s). See CLAUDE.md.\n' \
  133. "$errors" "$warns" >&2
  134. exit 2
  135. fi
  136. exit 0
  137. ;;
  138. --diff)
  139. base=${2:-}
  140. if [ -z "$base" ]; then
  141. echo "usage: $0 --diff <base-ref>" >&2
  142. exit 1
  143. fi
  144. cd "$repo_root" || exit 1
  145. changed=$(git diff --name-only --diff-filter=ACM "$base" -- '*.go' 'go.mod' 'go.sum' '**/go.mod' '**/go.sum')
  146. [ -z "$changed" ] && {
  147. echo "No Go/module changes to check." >&2
  148. exit 0
  149. }
  150. # Iterate over a temp file rather than a pipe so the err/warn counters,
  151. # which live in this shell, survive (a piped while-loop runs in a subshell).
  152. tmp=$(mktemp)
  153. added=$(mktemp)
  154. printf '%s\n' "$changed" >"$tmp"
  155. while IFS= read -r f; do
  156. [ -n "$f" ] || continue
  157. case "$f" in
  158. go.mod | go.sum | */go.mod | */go.sum)
  159. license_reminder
  160. continue
  161. ;;
  162. *.go) ;;
  163. *) continue ;;
  164. esac
  165. printf 'Checking %s\n' "$f" >&2
  166. # Diff mode only scans *added* lines for the per-line rules. Feed them
  167. # via redirection (not a pipe) so check_lines runs in this shell.
  168. git diff -U0 "$base" -- "$f" | grep -E '^\+' | grep -Ev '^\+\+\+' | sed 's/^+//' >"$added"
  169. check_lines "$f" <"$added"
  170. check_file "$f"
  171. done <"$tmp"
  172. rm -f "$tmp" "$added"
  173. ;;
  174. --help | -h)
  175. sed -n '2,40p' "$0"
  176. exit 0
  177. ;;
  178. *)
  179. # Treat all arguments as explicit file paths.
  180. for f in "$@"; do
  181. scan_one "$f"
  182. done
  183. ;;
  184. esac
  185. printf '\nArozOS convention check: %d error(s), %d warning(s).\n' "$errors" "$warns" >&2
  186. [ "$errors" -gt 0 ] && exit 1
  187. exit 0