Pre-commit hook
#!/bin/bash
# Pre-commit hook: Enforce FAIL-FAST philosophy
# See PROJECT_STANDARDS.md and docs/CODING_PHILOSOPHY.md for coding standards
#
# This hook catches common fallback patterns that hide bugs instead of
# exposing them. When in doubt: CRASH LOUD, CRASH EARLY, CRASH CLEAR.
# Check if we're being run standalone or as a hook
if [ "$1" = "--standalone" ]; then
STANDALONE=1
shift
# Use provided files or all .lua files
if [ -n "$1" ]; then
STAGED_LUA="$@"
else
STAGED_LUA=$(find . -name "*.lua" -not -path "./.git/*")
fi
else
STANDALONE=0
# Find staged .lua files
STAGED_LUA=$(git diff --cached --name-only --diff-filter=ACM | grep '\.lua$')
fi
if [ -z "$STAGED_LUA" ]; then
[ $STANDALONE -eq 0 ] && echo "✓ No Lua files to check"
exit 0
fi
TOTAL_VIOLATIONS=0
# ============================================
# ADDITIONAL CHECKS: Workarounds & Compatibility
# ============================================
# Source additional checks for workarounds, legacy code, etc.
if [ -f ".git/hooks/pre-commit-additions.sh" ]; then
. .git/hooks/pre-commit-additions.sh
fi
# ============================================
# CHECK 1: Forbidden Fallback Patterns
# ============================================
FALLBACK_VIOLATIONS=0
BOOLEAN_LOGIC_WARNINGS=0
for file in $STAGED_LUA; do
# Skip if file doesn't exist (deleted)
if [ ! -f "$file" ]; then
continue
fi
# Find all " or " patterns, excluding ONLY comments
# We want to catch ALL fallbacks: in functions, returns, expressions, etc.
# Note: Not filtering string literals because the pattern can be too broad
ALL_ORS=$(grep -n " or " "$file" | \
grep -v "^\s*--" | \
grep -v " -- .*or" | \
grep -v "\-\- Alternative" | \
grep -v "\-\- Can be:")
if [ -n "$ALL_ORS" ]; then
# Separate boolean logic from fallbacks
FALLBACKS=""
BOOLEAN_LOGIC=""
while IFS= read -r line; do
# Check if this looks like boolean comparison logic:
# Pattern: something == something or something == something
# Pattern: (something) or (something)
# Pattern: if boolVar or boolVar then (combining boolean variables)
# Pattern: return boolVar or boolVar (returning combined boolean)
if echo "$line" | grep -qE '(==|~=|<|>|<=|>=).*or.*(==|~=|<|>|<=|>=)'; then
# Looks like boolean logic - allow but warn
BOOLEAN_LOGIC="${BOOLEAN_LOGIC}${line}\n"
elif echo "$line" | grep -qE '\(.*\)\s+or\s+\(.*\)'; then
# Parenthesized expressions - likely boolean logic
BOOLEAN_LOGIC="${BOOLEAN_LOGIC}${line}\n"
elif echo "$line" | grep -qE '(if|return|while|until)\s+[a-zA-Z_][a-zA-Z0-9_]*\s+or\s+[a-zA-Z_][a-zA-Z0-9_]*\s+(then|$|do)'; then
# Pattern: if varA or varB then / return varA or varB
# Combining boolean variables - legitimate boolean logic
BOOLEAN_LOGIC="${BOOLEAN_LOGIC}${line}\n"
else
# Likely a fallback pattern
FALLBACKS="${FALLBACKS}${line}\n"
fi
done <<< "$ALL_ORS"
# Report fallback violations
if [ -n "$FALLBACKS" ]; then
[ $FALLBACK_VIOLATIONS -eq 0 ] && echo "❌ FALLBACK VIOLATIONS:"
echo " $file:"
echo -e "$FALLBACKS" | grep -v '^$' | sed 's/^/ /'
FALLBACK_VIOLATIONS=$((FALLBACK_VIOLATIONS + 1))
fi
# Boolean logic is allowed - skip warnings
fi
done
[ $FALLBACK_VIOLATIONS -gt 0 ] && TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + FALLBACK_VIOLATIONS))
# ============================================
# CHECK 1B: Hidden Fallback Patterns (if not x then x = y)
# ============================================
HIDDEN_FALLBACK_VIOLATIONS=0
for file in $STAGED_LUA; do
# Skip if file doesn't exist (deleted)
if [ ! -f "$file" ]; then
continue
fi
# Look for "if not <var> then" followed by "<var> = " pattern
# This catches: if not x then x = 0 end
HIDDEN_FALLBACKS=$(grep -n "if not .* then" "$file" | \
grep -v "^\s*--" | \
while read -r line; do
linenum=$(echo "$line" | cut -d: -f1)
varname=$(echo "$line" | sed 's/.*if not \([a-zA-Z_][a-zA-Z0-9_]*\).*/\1/')
# Check if next few lines assign to same variable
nextlines=$(sed -n "${linenum},$((linenum+3))p" "$file" | grep "^\s*${varname}\s*=")
if [ -n "$nextlines" ]; then
echo "$line"
fi
done)
if [ -n "$HIDDEN_FALLBACKS" ]; then
[ $HIDDEN_FALLBACK_VIOLATIONS -eq 0 ] && echo "❌ HIDDEN FALLBACKS:"
echo " $file:"
echo "$HIDDEN_FALLBACKS" | sed 's/^/ /'
HIDDEN_FALLBACK_VIOLATIONS=$((HIDDEN_FALLBACK_VIOLATIONS + 1))
fi
done
[ $HIDDEN_FALLBACK_VIOLATIONS -gt 0 ] && TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + HIDDEN_FALLBACK_VIOLATIONS))
# ============================================
# CHECK 1C: Semantic Fallback Patterns (local x = default; if config.x then x = config.x)
# ============================================
SEMANTIC_FALLBACK_VIOLATIONS=0
for file in $STAGED_LUA; do
# Skip if file doesn't exist (deleted)
if [ ! -f "$file" ]; then
continue
fi
# Use awk to detect semantic fallback patterns:
# Pattern 1: local var = value; if config.var then var = config.var
# Pattern 2: local var = value; if config.var and ... then var = ...
# Both hide defaults from grep while providing fallback behavior
# Skip counters: local count = 0 ... count = count + 1
SEMANTIC_FALLBACKS=$(awk '
/local [a-zA-Z_][a-zA-Z0-9_]* = / {
# Extract variable name and check if it has a default value
# Skip if default is nil (that is a valid feature flag pattern)
if ($0 ~ /= nil/) {
next
}
# Skip if initialized to a number (likely a counter)
if ($0 ~ /= [0-9]+[[:space:]]*$/ || $0 ~ /= [0-9]+\.?[0-9]*[[:space:]]*$/) {
next
}
match($0, /local ([a-zA-Z_][a-zA-Z0-9_]*) = /, arr)
if (arr[1]) {
varname = arr[1]
line_num = NR
stored_line = NR": "$0
# Track we are looking for reassignment of this var
looking_for_var = varname
}
}
looking_for_var && NR > line_num && NR <= line_num + 10 {
# Check if this line reassigns the variable we are tracking
# Pattern: if <something> then ... varname = ...
if ($0 ~ "if .* then" || $0 ~ "elseif .* then") {
in_conditional = 1
conditional_line = NR
}
# Only flag if reassignment happens AFTER the conditional starts
if (in_conditional && NR > conditional_line && $0 ~ "^[[:space:]]*" looking_for_var "[[:space:]]*=") {
# Found reassignment in conditional - this is a semantic fallback
print stored_line
looking_for_var = ""
in_conditional = 0
}
if ($0 ~ "^[[:space:]]*end[[:space:]]*$") {
in_conditional = 0
}
}
# Reset if we move too far from the local declaration
looking_for_var && (NR - line_num > 10) {
looking_for_var = ""
in_conditional = 0
}
' "$file")
if [ -n "$SEMANTIC_FALLBACKS" ]; then
[ $SEMANTIC_FALLBACK_VIOLATIONS -eq 0 ] && echo "❌ SEMANTIC FALLBACKS (local x = default; if config.x then x = config.x):"
echo " $file:"
echo "$SEMANTIC_FALLBACKS" | sed 's/^/ /'
SEMANTIC_FALLBACK_VIOLATIONS=$((SEMANTIC_FALLBACK_VIOLATIONS + 1))
fi
done
[ $SEMANTIC_FALLBACK_VIOLATIONS -gt 0 ] && TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + SEMANTIC_FALLBACK_VIOLATIONS))
# ============================================
# CHECK 1D: Type-check Fallbacks (if type(x) == "table" then...)
# ============================================
TYPE_CHECK_VIOLATIONS=0
for file in $STAGED_LUA; do
if [ ! -f "$file" ]; then
continue
fi
TYPE_CHECKS=$(grep -n 'if type(' "$file" | grep -v "^\s*--")
if [ -n "$TYPE_CHECKS" ]; then
[ $TYPE_CHECK_VIOLATIONS -eq 0 ] && echo "❌ TYPE-CHECK FALLBACKS:"
echo " $file:"
echo "$TYPE_CHECKS" | sed 's/^/ /'
TYPE_CHECK_VIOLATIONS=$((TYPE_CHECK_VIOLATIONS + 1))
fi
done
[ $TYPE_CHECK_VIOLATIONS -gt 0 ] && TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + TYPE_CHECK_VIOLATIONS))
# ============================================
# CHECK 1E: Metatable Fallbacks (__index with defaults)
# ============================================
METATABLE_VIOLATIONS=0
for file in $STAGED_LUA; do
if [ ! -f "$file" ]; then
continue
fi
METATABLES=$(grep -n '__index.*=' "$file" | grep -v "^\s*--")
if [ -n "$METATABLES" ]; then
[ $METATABLE_VIOLATIONS -eq 0 ] && echo "❌ METATABLE FALLBACKS (__index):"
echo " $file:"
echo "$METATABLES" | sed 's/^/ /'
METATABLE_VIOLATIONS=$((METATABLE_VIOLATIONS + 1))
fi
done
[ $METATABLE_VIOLATIONS -gt 0 ] && TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + METATABLE_VIOLATIONS))
# ============================================
# CHECK 1F: Table Access with Fallback (t.key or default)
# ============================================
TABLE_ACCESS_VIOLATIONS=0
for file in $STAGED_LUA; do
if [ ! -f "$file" ]; then
continue
fi
# Look for table.key or default / table[key] or default
# This is typically a fallback pattern
TABLE_FALLBACKS=$(grep -n '\(\..*\|\[.*\]\)\s*or\s*[^=]' "$file" | \
grep -v "^\s*--" | \
grep -v " -- " | \
grep -v "==.*or" | \
grep -v "~=.*or")
if [ -n "$TABLE_FALLBACKS" ]; then
[ $TABLE_ACCESS_VIOLATIONS -eq 0 ] && echo "❌ TABLE ACCESS FALLBACKS (t.key or default):"
echo " $file:"
echo "$TABLE_FALLBACKS" | sed 's/^/ /'
TABLE_ACCESS_VIOLATIONS=$((TABLE_ACCESS_VIOLATIONS + 1))
fi
done
[ $TABLE_ACCESS_VIOLATIONS -gt 0 ] && TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + TABLE_ACCESS_VIOLATIONS))
# ============================================
# CHECK 1G: Function Return Fallbacks (return x or y)
# ============================================
RETURN_FALLBACK_VIOLATIONS=0
for file in $STAGED_LUA; do
if [ ! -f "$file" ]; then
continue
fi
# Look for "return ... or ..." patterns
# Exclude boolean logic (return a == b or c == d)
RETURN_FALLBACKS=$(grep -n 'return.*or' "$file" | \
grep -v "^\s*--" | \
grep -v "==.*or.*==" | \
grep -v "~=.*or.*~=" | \
grep -v "(.*) or (.*)")
if [ -n "$RETURN_FALLBACKS" ]; then
# Filter out cases that are clearly boolean variable combinations
FILTERED_FALLBACKS=""
while IFS= read -r line; do
# Skip: return boolVar or boolVar
if echo "$line" | grep -qE 'return\s+[a-zA-Z_][a-zA-Z0-9_]*\s+or\s+[a-zA-Z_][a-zA-Z0-9_]*\s*$'; then
continue
fi
FILTERED_FALLBACKS="${FILTERED_FALLBACKS}${line}\n"
done <<< "$RETURN_FALLBACKS"
if [ -n "$FILTERED_FALLBACKS" ]; then
[ $RETURN_FALLBACK_VIOLATIONS -eq 0 ] && echo "❌ RETURN FALLBACKS (return x or y):"
echo " $file:"
echo -e "$FILTERED_FALLBACKS" | grep -v '^$' | sed 's/^/ /'
RETURN_FALLBACK_VIOLATIONS=$((RETURN_FALLBACK_VIOLATIONS + 1))
fi
fi
done
[ $RETURN_FALLBACK_VIOLATIONS -gt 0 ] && TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + RETURN_FALLBACK_VIOLATIONS))
# ============================================
# CHECK 1H: Assignment Fallbacks (x = y or z)
# ============================================
ASSIGNMENT_FALLBACK_VIOLATIONS=0
for file in $STAGED_LUA; do
if [ ! -f "$file" ]; then
continue
fi
# Look for "var = ... or ..." patterns
# Exclude boolean logic and variable combinations
ASSIGNMENT_FALLBACKS=$(grep -n '=.*or' "$file" | \
grep -v "^\s*--" | \
grep -v "==.*or" | \
grep -v "~=.*or" | \
grep -v "(.*) or (.*)" | \
grep -v "-- ")
if [ -n "$ASSIGNMENT_FALLBACKS" ]; then
# Filter out likely boolean combinations
FILTERED_FALLBACKS=""
while IFS= read -r line; do
# Skip: var = boolVar or boolVar
if echo "$line" | grep -qE '=\s+[a-zA-Z_][a-zA-Z0-9_]*\s+or\s+[a-zA-Z_][a-zA-Z0-9_]*\s*$'; then
continue
fi
# Skip boolean operators
if echo "$line" | grep -qE '(==|~=|<|>|<=|>=).*or.*(==|~=|<|>|<=|>=)'; then
continue
fi
FILTERED_FALLBACKS="${FILTERED_FALLBACKS}${line}\n"
done <<< "$ASSIGNMENT_FALLBACKS"
if [ -n "$FILTERED_FALLBACKS" ]; then
[ $ASSIGNMENT_FALLBACK_VIOLATIONS -eq 0 ] && echo "❌ ASSIGNMENT FALLBACKS (x = y or z):"
echo " $file:"
echo -e "$FILTERED_FALLBACKS" | grep -v '^$' | sed 's/^/ /'
ASSIGNMENT_FALLBACK_VIOLATIONS=$((ASSIGNMENT_FALLBACK_VIOLATIONS + 1))
fi
fi
done
[ $ASSIGNMENT_FALLBACK_VIOLATIONS -gt 0 ] && TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + ASSIGNMENT_FALLBACK_VIOLATIONS))
# ============================================
# CHECK 1I: Ternary Operator Fallbacks (x and y or z)
# ============================================
TERNARY_FALLBACK_VIOLATIONS=0
for file in $STAGED_LUA; do
if [ ! -f "$file" ]; then
continue
fi
# Look for "x and y or z" patterns (Lua's ternary operator idiom)
# These are often used for fallbacks
TERNARY_PATTERNS=$(grep -n 'and.*or' "$file" | \
grep -v "^\s*--" | \
grep -v " -- ")
if [ -n "$TERNARY_PATTERNS" ]; then
TERNARY_FALLBACKS=""
while IFS= read -r line; do
# Exclude complex boolean logic with multiple comparisons
if echo "$line" | grep -qE '(==|~=|<|>|<=|>=).*and.*(==|~=|<|>|<=|>=).*or.*(==|~=|<|>|<=|>=)'; then
# Looks like complex boolean logic - skip
continue
else
# Likely a ternary fallback pattern
TERNARY_FALLBACKS="${TERNARY_FALLBACKS}${line}\n"
fi
done <<< "$TERNARY_PATTERNS"
if [ -n "$TERNARY_FALLBACKS" ]; then
[ $TERNARY_FALLBACK_VIOLATIONS -eq 0 ] && echo "❌ TERNARY OPERATOR FALLBACKS (x and y or z):"
echo " $file:"
echo -e "$TERNARY_FALLBACKS" | grep -v '^$' | sed 's/^/ /'
TERNARY_FALLBACK_VIOLATIONS=$((TERNARY_FALLBACK_VIOLATIONS + 1))
fi
fi
done
[ $TERNARY_FALLBACK_VIOLATIONS -gt 0 ] && TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + TERNARY_FALLBACK_VIOLATIONS))
# ============================================
# CHECK 1J: Suspicious Comments (Fallback Thinking)
# ============================================
COMMENT_VIOLATIONS=0
for file in $STAGED_LUA; do
# Skip if file doesn't exist (deleted)
if [ ! -f "$file" ]; then
continue
fi
# Look for comments that suggest defensive/fallback thinking
# But exclude the philosophy doc and this hook itself
if [[ "$file" == *"CODING_PHILOSOPHY"* ]] || [[ "$file" == *"pre-commit"* ]]; then
continue
fi
SUSPICIOUS_COMMENTS=$(grep -n -i '\-\-.*\(just in case\|might be missing\|might not exist\|could be nil\|backwards compat\|migration\)' "$file" | \
grep -v "no fallback" | \
grep -v "FAIL FAST")
if [ -n "$SUSPICIOUS_COMMENTS" ]; then
[ $COMMENT_VIOLATIONS -eq 0 ] && echo "⚠️ SUSPICIOUS COMMENTS:"
echo " $file:"
echo "$SUSPICIOUS_COMMENTS" | sed 's/^/ /'
COMMENT_VIOLATIONS=$((COMMENT_VIOLATIONS + 1))
fi
done
[ $COMMENT_VIOLATIONS -gt 0 ] && TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + COMMENT_VIOLATIONS))
# ============================================
# CHECK 1K: TODO and Placeholder Comments
# ============================================
TODO_VIOLATIONS=0
for file in $STAGED_LUA; do
# Skip if file doesn't exist (deleted)
if [ ! -f "$file" ]; then
continue
fi
# Look for TODO, FIXME, HACK, XXX, will implement, placeholder, temporary, not yet implemented, etc.
TODO_COMMENTS=$(grep -n -i '\-\-.*\(TODO\|FIXME\|HACK\|XXX\|will implement\|placeholder\|temporary\|coming soon\|not implemented\|not yet implemented\)' "$file" || true)
if [ -n "$TODO_COMMENTS" ]; then
[ $TODO_VIOLATIONS -eq 0 ] && echo "❌ TODO/PLACEHOLDER COMMENTS:"
echo " $file:"
echo "$TODO_COMMENTS" | sed 's/^/ /'
TODO_VIOLATIONS=$((TODO_VIOLATIONS + 1))
fi
done
[ $TODO_VIOLATIONS -gt 0 ] && TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + TODO_VIOLATIONS))
# ============================================
# CHECK 2: Non-ASCII Characters (Emoji, etc.)
# ============================================
NON_ASCII_VIOLATIONS=0
for file in $STAGED_LUA; do
# Skip if file doesn't exist (deleted)
if [ ! -f "$file" ]; then
continue
fi
# Find lines with non-ASCII characters (characters with codes > 127)
# Exclude comment lines (starting with --)
NON_ASCII=$(grep -n -P '[^\x00-\x7F]' "$file" | grep -v "^\s*[0-9]*:\s*--")
if [ -n "$NON_ASCII" ]; then
[ $NON_ASCII_VIOLATIONS -eq 0 ] && echo "❌ NON-ASCII CHARACTERS:"
echo " $file:"
echo "$NON_ASCII" | sed 's/^/ /'
NON_ASCII_VIOLATIONS=$((NON_ASCII_VIOLATIONS + 1))
fi
done
[ $NON_ASCII_VIOLATIONS -gt 0 ] && TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + NON_ASCII_VIOLATIONS))
# ============================================
# CHECK 3: Forbidden pcall Fallback Patterns
# ============================================
PCALL_VIOLATIONS=0
for file in $STAGED_LUA; do
# Skip if file doesn't exist (deleted)
if [ ! -f "$file" ]; then
continue
fi
# Look for pcall patterns with fallback logic
# Pattern: "pcall(" on a line, likely storing success/result
PCALLS=$(grep -n "pcall(" "$file" | grep -v "^\s*--")
if [ -n "$PCALLS" ]; then
[ $PCALL_VIOLATIONS -eq 0 ] && echo "❌ PCALL (hides errors):"
echo " $file:"
echo "$PCALLS" | sed 's/^/ /'
PCALL_VIOLATIONS=$((PCALL_VIOLATIONS + 1))
fi
done
[ $PCALL_VIOLATIONS -gt 0 ] && TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + PCALL_VIOLATIONS))
# FINAL RESULT
if [ $TOTAL_VIOLATIONS -gt 0 ]; then
echo ""
echo "BLOCKED: $TOTAL_VIOLATIONS violation(s). See PROJECT_STANDARDS.md"
exit 1
else
echo "✓ Pass: No violations found"
exit 0
fi
Comments
Post a Comment