Ctrl K
ComunidadGuías…

scripts/check-codebase.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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