scripts/check-codebase.sh
Rama main · réplica de lectura del repositorio original.
#!/bin/bash
# Enforces codebase-wide conventions. Run via: make check-codebase
# NOTE: pipefail intentionally NOT set. The while(1) check uses
# tail | head | grep pipelines where SIGPIPE is expected.
set +o pipefail
ROOT="$(dirname "$0")/.."
err_count=0
warn_count=0
red() { printf "\033[31m%s\033[0m\n" "$*"; }
green() { printf "\033[32m%s\033[0m\n" "$*"; }
yellow() { printf "\033[33m%s\033[0m\n" "$*"; }
warn() { warn_count=$((warn_count + 1)); yellow " [WARN] $*"; }
err() { err_count=$((err_count + 1)); red " [FAIL] $*"; }
check_header() { echo ""; echo "--- $* ---"; }
# ===========================================================================
# Rule 1: while(1) loops in parser must guard TOK_EOF
# Accept TOK_EOF, break; after zpanic_at, or implicit break on non-match.
# ===========================================================================
check_header "Rule 1: while(1) loops must guard TOK_EOF"
while IFS= read -r line; do
f=$(echo "$line" | cut -d: -f1)
ln=$(echo "$line" | cut -d: -f2)
body=$(tail -n +"$ln" "$f" | head -40)
# Accept: explicit TOK_EOF, or break; (safe termination)
if echo "$body" | grep -q 'TOK_EOF'; then continue; fi
if echo "$body" | grep -qE '^\s+break;\s*$'; then continue; fi
warn "$f:$ln"
done < <(grep -rn 'while (1)' "$ROOT/src/parser" "$ROOT/src/analysis" 2>/dev/null)
# ===========================================================================
# Rule 2: No exit(1) outside whitelisted locations
# ===========================================================================
check_header "Rule 2: exit(1) outside whitelist"
while IFS= read -r line; do
case "$line" in
*zfatal*) continue ;;
*diagnostics.c*) continue ;;
*utils_plugins.c*) continue ;;
*"// whitelisted"*) continue ;;
*lsp/lsp_project.c*) continue ;;
*codegen_decl_emit.c*|\
*codegen_decl_preamble.c*) continue ;;
*"/* exit */"*) continue ;;
esac
err "$line"
done < <(grep -rn 'exit(1)' "$ROOT/src" 2>/dev/null)
# ===========================================================================
# Rule 3: No strdup() — use xstrdup()
# ===========================================================================
check_header "Rule 3: strdup() should use xstrdup"
while IFS= read -r line; do
case "$line" in
*xstrdup*) continue ;;
*define*) continue ;;
*'"'*strdup*'"'*) continue ;;
esac
err "$line"
done < <(grep -rn '\bstrdup\b' "$ROOT/src" 2>/dev/null)
# ===========================================================================
# Rule 4: Use TOKEN_UNKNOWN not bare (Token){0}
# ===========================================================================
check_header "Rule 4: (Token){0} should use TOKEN_UNKNOWN"
while IFS= read -r line; do
case "$line" in
*TOKEN_UNKNOWN*) continue ;;
*"= {0}"*) continue ;;
*token.h*) continue ;;
*".o:"*) continue ;;
*"return (Token){0}"*) continue ;; # zero-init Token return value
esac
err "$line"
done < <(grep -rn '(Token){0}' "$ROOT/src" 2>/dev/null)
# ===========================================================================
# Rule 5: Plugin Token must set .col
# ===========================================================================
check_header "Rule 5: Plugin Token.col must be set"
while IFS= read -r line; do
f=$(echo "$line" | cut -d: -f1)
ln=$(echo "$line" | cut -d: -f2)
if ! tail -n +"$ln" "$f" | head -5 | grep -q '\.col'; then
err "$f:$ln"
fi
done < <(grep -rn 'Token t = {0};' "$ROOT/src/plugins" 2>/dev/null)
# ===========================================================================
# Rule 6: No zfree() on realpath() or malloc() allocations
# realpath() returns system-heap memory which must be freed with free(),
# not zfree() (which is a no-op for arena memory).
# ===========================================================================
check_header "Rule 6: zfree() on realpath() pointers"
if grep -rn 'zfree(real_path\|zfree(realpath' "$ROOT/src" 2>/dev/null; then
err "zfree() used on realpath() return value — must use free()"
fi
# ===========================================================================
# Rule 7: No g_compiler or g_config access in parser/analysis/codegen
# These should go through ctx->compiler or ctx->config for thread safety.
# ===========================================================================
check_header "Rule 7: g_compiler/g_config in parser/analysis/codegen"
while IFS= read -r line; do
case "$line" in
*".h:"*) continue ;; # header declarations
*"g_compiler"*"config"*) ;;
*) continue ;;
esac
warn "$line"
done < <(grep -rn 'g_compiler\.\|g_config\.' "$ROOT/src/parser" "$ROOT/src/analysis" "$ROOT/src/codegen" 2>/dev/null)
# ===========================================================================
# Rule 8: parse_type_formal return value should be NULL-checked
# ===========================================================================
check_header "Rule 8: parse_type_formal() NULL check (heuristic)"
while IFS= read -r line; do
f=$(echo "$line" | cut -d: -f1)
ln=$(echo "$line" | cut -d: -f2)
# Skip the definition and header declarations
case "$line" in
*"Type \*parse_type_formal"*) continue ;;
*".h:"*) continue ;;
*"Type *parse_type_formal("*) continue ;;
esac
# Check next 3 lines for a NULL check pattern
context=$(tail -n +"$ln" "$f" | head -5)
if ! echo "$context" | grep -qE 'if\s*\([!&]|== NULL|!= NULL|!\w+\s*\)|[?]|parse_type_formal\(ctx, l\)\s*==\s*NULL'; then
warn "$f:$ln"
fi
done < <(grep -rn 'parse_type_formal(' "$ROOT/src/parser" "$ROOT/src/analysis" "$ROOT/src/codegen" 2>/dev/null)
# ===========================================================================
# Rule 9: sprintf() without bounds — use snprintf()
# ===========================================================================
check_header "Rule 9: sprintf() should use snprintf()"
count=0
while IFS= read -r line; do
case "$line" in
*"/* safe */"*) continue ;;
*"/* TODO"*) continue ;;
*codegen_decl_*) continue ;; # codegen string literals
*cJSON.c*) continue ;; # vendored library
*.h:*) continue ;;
esac
# Skip sprintf inside string literals (codegen output)
line_text=$(echo "$line" | cut -d: -f3-)
if echo "$line_text" | grep -qE '^\s*"'; then
continue
fi
warn "$line"
count=$((count + 1))
done < <(grep -rn '\bsprintf(' "$ROOT/src" 2>/dev/null)
[ "$count" -gt 0 ] && yellow " ($count sprintf() calls — needs review)"
# ===========================================================================
# Rule 10: Functions over 600 lines (heuristic)
# ===========================================================================
check_header "Rule 10: Functions over 600 lines"
while IFS= read -r line; do
f=$(echo "$line" | cut -d: -f1)
len=$(echo "$line" | cut -d' ' -f2)
warn "$f ($len lines)"
done < <(wc -l "$ROOT/src/parser/expr/expr_primary.c" \
"$ROOT/src/parser/expr/expr_prec.c" \
"$ROOT/src/parser/parser_stmt.c" \
"$ROOT/src/codegen/codegen_expr_handlers.c" 2>/dev/null | awk '$1 > 2000 {print $2 " " $1}')
# ===========================================================================
# Rule 11: bare realloc() outside arena-macro scope
# ===========================================================================
check_header "Rule 11: bare realloc() without NULL check"
while IFS= read -r line; do
case "$line" in
*xrealloc*) continue ;;
*codegen_decl_*) continue ;; # codegen string literals
*zmap.h*) continue ;; # macro definition
*zvec.h*) continue ;; # macro definition
*zalloc.h*) continue ;; # macro definition
*zc_utils.h*) continue ;; # realloc macro redirect
*arena.h*) continue ;; # realloc macro redirect
*cJSON.c*) continue ;; # vendored library
esac
f=$(echo "$line" | cut -d: -f1)
ln=$(echo "$line" | cut -d: -f2)
# Parser and REPL files include arena.h indirectly via compiler.h
case "$f" in
*parser/*) continue ;;
*repl/*) continue ;;
esac
# Only flag files outside parser/repl that use raw realloc
# Check if the next line has a NULL check
context=$(tail -n +"$ln" "$f" | head -3)
if echo "$context" | grep -qE 'if\s*\(!|== NULL|!= NULL'; then
continue # has NULL check, safe
fi
err "$line"
done < <(grep -rn '\brealloc(' "$ROOT/src" 2>/dev/null)
# ===========================================================================
# Rule 12: zpanic_at() without break/return on next line
# ===========================================================================
check_header "Rule 12: zpanic_at() missing break/return"
while IFS= read -r line; do
f=$(echo "$line" | cut -d: -f1)
ln=$(echo "$line" | cut -d: -f2)
# Skip files with known complex multi-line zpanic_at patterns
# (These have been manually verified to have return/break after the
# closing paren of the zpanic_at call, but the heuristic here can't
# see past multi-line argument lists.)
case "$f" in
*expr_prec.c*) continue ;;
*type_base.c*) continue ;;
*type_formal.c*) continue ;;
*stmt_import.c*) continue ;;
*decl_var.c*) continue ;;
*expr_literal.c*) continue ;;
*expr_primary.c*) continue ;;
*parser_stmt.c*) continue ;;
*struct_impl.c*) continue ;;
esac
# Check the next non-blank non-brace line after zpanic_at
next=$(tail -n +"$((ln + 1))" "$f" | grep -v '^\s*$\|^\s*}\s*$' | head -1)
case "$next" in
*break\;*) continue ;;
*return*) continue ;;
*continue\;*) continue ;;
*"// no-break") continue ;;
""|"}") continue ;; # end of function or block — safe
esac
# zpanic_at is the last thing in an else branch — that's ok
prev=$(sed -n "$((ln - 1))p" "$f")
case "$prev" in
*else*) continue ;;
esac
warn "$f:$ln (no break/return after zpanic_at)"
done < <(grep -rn 'zpanic_at(' "$ROOT/src/parser" "$ROOT/src/analysis" 2>/dev/null)
# ===========================================================================
# Rule 13: atoi() is banned — use strtol() with error checking
# atoi() has undefined behavior on integer overflow.
# ===========================================================================
check_header "Rule 13: atoi() should use strtol()"
count=0
while IFS= read -r line; do
err "$line"
count=$((count + 1))
done < <(grep -rn '\batoi(' "$ROOT/src" 2>/dev/null | grep -v 'cJSON.c' | grep -v '\.h:' | grep -v '\.json:')
[ "$count" -gt 0 ] && red " ($count atoi() calls — must fix)"
# ===========================================================================
# Rule 14: Bare strcat() is banned in core compiler code
# strcat() has no bounds checking — overflow risk on fixed-size buffers.
# ===========================================================================
check_header "Rule 14: strcat() should use snprintf() or size-tracked concat"
count=0
while IFS= read -r line; do
case "$line" in
*codegen_decl_*) continue ;; # codegen string literals
*codegen_backend_*.c*) continue ;; # codegen output strings
*cJSON.c*) continue ;; # vendored library
*".h:"*) continue ;;
*"/* safe */"*) continue ;;
*"/* strcat OK */"*) continue ;;
esac
warn "$line"
count=$((count + 1))
done < <(grep -rn '\bstrcat(' "$ROOT/src" 2>/dev/null)
[ "$count" -gt 0 ] && yellow " ($count strcat() calls — needs review)"
# ===========================================================================
# Rule 15: Bare strcpy() is banned in core compiler code
# strcpy() has no bounds checking — overflow risk on fixed-size buffers.
# ===========================================================================
check_header "Rule 15: strcpy() should use snprintf() or size-checked copy"
count=0
while IFS= read -r line; do
case "$line" in
*codegen_decl_*) continue ;; # codegen string literals
*cJSON.c*) continue ;; # vendored library
*".h:"*) continue ;;
*"/* safe */"*) continue ;;
esac
warn "$line"
count=$((count + 1))
done < <(grep -rn '\bstrcpy(' "$ROOT/src" 2>/dev/null)
[ "$count" -gt 0 ] && yellow " ($count strcpy() calls — needs review)"
# ===========================================================================
# Rule 16: strncpy() must guarantee null-termination
# strncpy() does NOT add \0 if source length >= destination size.
# ===========================================================================
check_header "Rule 16: strncpy() must guarantee null-termination"
count=0
while IFS= read -r line; do
f=$(echo "$line" | cut -d: -f1)
ln=$(echo "$line" | cut -d: -f2)
case "$f" in
*cJSON.c*) continue ;; # vendored library
*\.h) continue ;;
*docs.json) continue ;;
esac
# Check for null-termination within next 5 lines (some patterns span more lines)
after=$(tail -n +"$((ln + 1))" "$f" | head -5)
# Check if any line after strncpy contains a null-termination of a likely buffer
# Look for patterns like: buf[index] = '\0' or = 0, or snprintf, or explicit \0 assignment
if echo "$after" | grep -qE '\[.*\]\s*=\s*0\s*;|\[.*\]\s*=\s*'"'"'\\\\0'"'"'\s*;'; then
continue # some buffer is null-terminated — assume correct
fi
if echo "$after" | head -2 | grep -qE '\bsnprintf\b'; then
continue # switched to snprintf
fi
# Try to check if the specific destination buffer is null-terminated
# First, check if an array-style dest (like "buf + offset") has base buf null-terminated
base_dest=$(echo "$line" | sed 's/.*strncpy(\([a-zA-Z_][a-zA-Z0-9_]*\)[^,]*,[^,]*,[^)]*).*/\1/')
if [ "$base_dest" != "$line" ] && [ -n "$base_dest" ]; then
if echo "$after" | grep -qE "${base_dest}\[.*\]\s*=\s*0\s*;"; then
continue # base buffer is null-terminated after copy
fi
fi
# Now check for sizeof guard
if echo "$line" | grep -qE 'sizeof\([^)]+\)\s*-\s*[0-9]+'; then
warn "$line (uses sizeof(buf)-N — verify null-termination follows)"
count=$((count + 1))
elif echo "$line" | grep -qE 'sizeof\([^)]+\)'; then
warn "$line (strncpy with sizeof — verify null-termination: doesn't add \0 if src >= dst)"
count=$((count + 1))
else
warn "$line (strncpy without sizeof guard — verify null-termination)"
count=$((count + 1))
fi
done < <(grep -rn '\bstrncpy(' "$ROOT/src" 2>/dev/null)
[ "$count" -gt 0 ] && yellow " ($count strncpy() calls — needs review)"
# ===========================================================================
# Rule 17: ftell() result must be validated
# ftell() returns -1 on error, which becomes SIZE_MAX when cast to size_t.
# ===========================================================================
check_header "Rule 17: ftell() result must be validated"
while IFS= read -r line; do
f=$(echo "$line" | cut -d: -f1)
ln=$(echo "$line" | cut -d: -f2)
# Check next 5 lines for error validation
context=$(tail -n +"$ln" "$f" | head -5)
if echo "$context" | grep -qE 'if\s*\(\s*\w+\s*<\s*0'; then
continue # ftell result validated with if (var < 0)
fi
if echo "$context" | grep -qE 'ftell.*< 0'; then
continue # ftell result checked inline
fi
err "$line (ftell() result not validated — can silently return SIZE_MAX)"
done < <(grep -rn '\bftell(' "$ROOT/src" 2>/dev/null | grep -v '\.h:')
# ===========================================================================
# Rule 18: No exit() outside main/lsp/diagnostics (in library code)
# exit() kills the embedding process. Library code (parser/analysis/codegen)
# must never call exit() directly.
# ===========================================================================
check_header "Rule 18: exit() in library code (parser/analysis/codegen)"
while IFS= read -r line; do
case "$line" in
*diagnostics.c*) continue ;;
*"/* exit */"*) continue ;;
*"// whitelisted"*) continue ;;
*codegen_decl_*.c*) continue ;; # codegen emits exit() in generated C code
esac
# Skip comments and string literals (generated code)
line_text=$(echo "$line" | cut -d: -f3-)
case "$line_text" in
*//*) continue ;; # comment
*/\**) continue ;; # block comment start
*\**) continue ;; # block comment end or mid
*'"'*exit*'"'*) continue ;; # inside string literal
esac
err "$line"
done < <(grep -rn '\bexit(' "$ROOT/src/parser" "$ROOT/src/analysis" "$ROOT/src/codegen" 2>/dev/null)
# ===========================================================================
# Rule 19: No variable-length arrays (VLA) on stack
# VLAs can cause stack overflow with attacker-controlled sizes.
# ===========================================================================
check_header "Rule 19: Variable-length arrays (VLA) on stack"
count=0
while IFS= read -r line; do
case "$line" in
*cJSON.c*) continue ;; # vendored library
*\.h:*) continue ;;
esac
# Match: type name[size] where size is not a literal
# Look for patterns like: char buf[len] or int arr[n]
# but NOT: char buf[64] or char buf[MAX_LEN]
line_text=$(echo "$line" | cut -d: -f3-)
case "$line_text" in
*"["*"]"*) ;;
*) continue ;;
esac
# Check if the array size is a variable (not a literal/named constant)
if echo "$line_text" | grep -qE '\b[a-z_][a-zA-Z0-9_]*\s*=\s*\{0\};'; then
continue
fi
warn "$line (potential VLA — verify size is a constant)"
count=$((count + 1))
done < <(grep -rn '\[[a-z_][a-zA-Z0-9_]*\]' "$ROOT/src" 2>/dev/null | grep -v '\.json:' | grep -v 'MAX_' | grep -v 'sizeof' | grep -v '^\s*/\*' | grep -v '//.*\[')
[ "$count" -gt 0 ] && yellow " ($count potential VLA declarations — needs review)"
# ===========================================================================
# Rule 20: All switch statements should have a default case
# Missing default can lead to unhandled enum values silently passing through.
# ===========================================================================
check_header "Rule 20: switch without default case"
count=0
while IFS= read -r line; do
case "$line" in
*cJSON.c*) continue ;;
*cJSON.h*) continue ;;
*".h:"*) continue ;;
esac
# Get the file
f=$(echo "$line" | cut -d: -f1)
ln=$(echo "$line" | cut -d: -f2)
# Check if this switch has a default case within the next 20 lines
context=$(tail -n +"$ln" "$f" | head -20)
if ! echo "$context" | grep -q '\bdefault:'; then
warn "$line (switch without default case)"
count=$((count + 1))
fi
done < <(grep -rn '\bswitch\s*(' "$ROOT/src" 2>/dev/null | grep -v '^\s*//' | head -200)
[ "$count" -gt 0 ] && yellow " ($count switch statements without default — needs review)"
# ===========================================================================
# Rule 21: No longjmp() or setjmp() usage
# Non-local goto complicates cleanup and can skip destructor calls.
# ===========================================================================
check_header "Rule 21: longjmp() / setjmp() usage"
count=0
while IFS= read -r line; do
case "$line" in
*cJSON.c*) continue ;;
*\.json:*) continue ;; # docs, facts
*misra.c*) continue ;; # MISRA documentation table
*\.h:*) continue ;;
esac
# Skip comments
line_text=$(echo "$line" | cut -d: -f3-)
case "$line_text" in
*//*) continue ;; # comment
*/\**) continue ;; # block comment
esac
err "$line"
count=$((count + 1))
done < <(grep -rn '\blongjmp\b\|\bsetjmp\b' "$ROOT/src" 2>/dev/null)
[ "$count" -gt 0 ] && red " ($count longjmp/setjmp calls — must fix)"
# ===========================================================================
# Rule 22: Function bodies over 500 lines (heuristic complexity check)
# ===========================================================================
check_header "Rule 22: Function bodies over 500 lines"
while IFS= read -r line; do
f=$(echo "$line" | cut -d' ' -f2)
len=$(echo "$line" | cut -d' ' -f1)
warn "$f ($len lines)"
done < <(wc -l "$ROOT/src/parser/expr/expr_primary.c" \
"$ROOT/src/parser/expr/expr_prec.c" \
"$ROOT/src/parser/parser_stmt.c" \
"$ROOT/src/codegen/codegen_expr_handlers.c" \
"$ROOT/src/parser/utils/utils_template_replace.c" \
"$ROOT/src/parser/utils/utils_rewrite.c" \
"$ROOT/src/codegen/codegen_stmt_handlers.c" 2>/dev/null | awk '$1 > 500 {print $1 " " $2}')
# ===========================================================================
# Summary
# ===========================================================================
echo ""
if [ "$err_count" -eq 0 ]; then
if [ "$warn_count" -eq 0 ]; then
green "All checks passed."
else
yellow "$warn_count warning(s). $err_count error(s)."
green "All required checks passed."
fi
exit 0
else
red "$err_count violation(s) found (must fix). $warn_count warning(s)."
exit 1
fi