Initial commit
This commit is contained in:
325
commands/documentation-validation/.scripts/example-validator.sh
Executable file
325
commands/documentation-validation/.scripts/example-validator.sh
Executable file
@@ -0,0 +1,325 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# ============================================================================
|
||||
# Example Validator
|
||||
# ============================================================================
|
||||
# Purpose: Validate example quality and detect placeholder patterns
|
||||
# Version: 1.0.0
|
||||
# Usage: ./example-validator.sh <path> [--no-placeholders] [--recursive] [--json]
|
||||
# Returns: 0=success, 1=warning, JSON output to stdout if --json
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Default values
|
||||
NO_PLACEHOLDERS=true
|
||||
RECURSIVE=true
|
||||
JSON_OUTPUT=false
|
||||
EXTENSIONS="md,txt,json,sh,py,js,ts,yaml,yml"
|
||||
|
||||
# Parse arguments
|
||||
TARGET_PATH="${1:-.}"
|
||||
shift || true
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--no-placeholders)
|
||||
NO_PLACEHOLDERS=true
|
||||
shift
|
||||
;;
|
||||
--allow-placeholders)
|
||||
NO_PLACEHOLDERS=false
|
||||
shift
|
||||
;;
|
||||
--recursive)
|
||||
RECURSIVE=true
|
||||
shift
|
||||
;;
|
||||
--non-recursive)
|
||||
RECURSIVE=false
|
||||
shift
|
||||
;;
|
||||
--json)
|
||||
JSON_OUTPUT=true
|
||||
shift
|
||||
;;
|
||||
--extensions)
|
||||
EXTENSIONS="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Initialize counters
|
||||
files_checked=0
|
||||
example_count=0
|
||||
placeholder_count=0
|
||||
todo_count=0
|
||||
declare -a issues=()
|
||||
declare -a files_with_issues=()
|
||||
|
||||
# Build find command based on recursiveness
|
||||
if $RECURSIVE; then
|
||||
FIND_DEPTH=""
|
||||
else
|
||||
FIND_DEPTH="-maxdepth 1"
|
||||
fi
|
||||
|
||||
# Build extension pattern
|
||||
ext_pattern=""
|
||||
IFS=',' read -ra EXT_ARRAY <<< "$EXTENSIONS"
|
||||
for ext in "${EXT_ARRAY[@]}"; do
|
||||
if [[ -z "$ext_pattern" ]]; then
|
||||
ext_pattern="-name '*.${ext}'"
|
||||
else
|
||||
ext_pattern="$ext_pattern -o -name '*.${ext}'"
|
||||
fi
|
||||
done
|
||||
|
||||
# Find files to check
|
||||
mapfile -t files < <(eval "find '$TARGET_PATH' $FIND_DEPTH -type f \( $ext_pattern \) 2>/dev/null" || true)
|
||||
|
||||
# Placeholder patterns to detect
|
||||
declare -a PLACEHOLDER_PATTERNS=(
|
||||
'TODO[:\)]'
|
||||
'FIXME[:\)]'
|
||||
'XXX[:\)]'
|
||||
'HACK[:\)]'
|
||||
'placeholder'
|
||||
'PLACEHOLDER'
|
||||
'your-.*-here'
|
||||
'<your-'
|
||||
'INSERT.?HERE'
|
||||
'YOUR_[A-Z_]+'
|
||||
)
|
||||
|
||||
# Generic dummy value patterns
|
||||
declare -a GENERIC_PATTERNS=(
|
||||
'\bfoo\b'
|
||||
'\bbar\b'
|
||||
'\bbaz\b'
|
||||
'\bdummy\b'
|
||||
)
|
||||
|
||||
# Acceptable patterns (don't count these)
|
||||
declare -a ACCEPTABLE_PATTERNS=(
|
||||
'\{\{[^}]+\}\}' # {{variable}} template syntax
|
||||
'\$\{[^}]+\}' # ${variable} template syntax
|
||||
'\$[A-Z_]+' # $VARIABLE environment variables
|
||||
)
|
||||
|
||||
# Function to check if line contains acceptable pattern
|
||||
is_acceptable_pattern() {
|
||||
local line="$1"
|
||||
|
||||
for pattern in "${ACCEPTABLE_PATTERNS[@]}"; do
|
||||
if echo "$line" | grep -qE "$pattern"; then
|
||||
return 0 # Is acceptable
|
||||
fi
|
||||
done
|
||||
|
||||
return 1 # Not acceptable
|
||||
}
|
||||
|
||||
# Function to count code examples in markdown
|
||||
count_code_examples() {
|
||||
local file="$1"
|
||||
|
||||
if [[ ! "$file" =~ \.md$ ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Count code blocks (```)
|
||||
local count
|
||||
count=$(grep -c '```' "$file" 2>/dev/null || echo "0")
|
||||
|
||||
# Ensure count is numeric and divide by 2 since each code block has opening and closing
|
||||
if [[ "$count" =~ ^[0-9]+$ ]]; then
|
||||
count=$((count / 2))
|
||||
else
|
||||
count=0
|
||||
fi
|
||||
|
||||
echo "$count"
|
||||
}
|
||||
|
||||
# Check each file
|
||||
for file in "${files[@]}"; do
|
||||
((files_checked++)) || true
|
||||
|
||||
# Count examples in markdown files
|
||||
if [[ "$file" =~ \.md$ ]]; then
|
||||
file_examples=$(count_code_examples "$file")
|
||||
((example_count += file_examples)) || true
|
||||
fi
|
||||
|
||||
file_issues=0
|
||||
|
||||
# Check for placeholder patterns
|
||||
for pattern in "${PLACEHOLDER_PATTERNS[@]}"; do
|
||||
while IFS=: read -r line_num line_content; do
|
||||
# Skip if it's an acceptable pattern
|
||||
if is_acceptable_pattern "$line_content"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
((placeholder_count++)) || true
|
||||
((file_issues++)) || true
|
||||
|
||||
issue="$file:$line_num: Placeholder pattern detected"
|
||||
issues+=("$issue")
|
||||
|
||||
# Track TODO/FIXME separately
|
||||
if echo "$pattern" | grep -qE 'TODO|FIXME|XXX'; then
|
||||
((todo_count++)) || true
|
||||
fi
|
||||
done < <(grep -inE "$pattern" "$file" 2>/dev/null || true)
|
||||
done
|
||||
|
||||
# Check for generic dummy values (only in non-test files)
|
||||
if [[ ! "$file" =~ test ]] && [[ ! "$file" =~ example ]] && [[ ! "$file" =~ spec ]]; then
|
||||
for pattern in "${GENERIC_PATTERNS[@]}"; do
|
||||
while IFS=: read -r line_num line_content; do
|
||||
# Skip code comments explaining these terms
|
||||
if echo "$line_content" | grep -qE '(#|//|/\*).*'"$pattern"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Skip if in an acceptable context
|
||||
if is_acceptable_pattern "$line_content"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
((file_issues++)) || true
|
||||
issue="$file:$line_num: Generic placeholder value detected"
|
||||
issues+=("$issue")
|
||||
done < <(grep -inE "$pattern" "$file" 2>/dev/null || true)
|
||||
done
|
||||
fi
|
||||
|
||||
# Track files with issues
|
||||
if ((file_issues > 0)); then
|
||||
files_with_issues+=("$file:$file_issues")
|
||||
fi
|
||||
done
|
||||
|
||||
# Calculate quality score
|
||||
quality_score=100
|
||||
((quality_score -= placeholder_count * 10)) || true
|
||||
((quality_score -= todo_count * 5)) || true
|
||||
|
||||
if ((example_count < 2)); then
|
||||
((quality_score -= 20)) || true
|
||||
fi
|
||||
|
||||
# Ensure score doesn't go negative
|
||||
if ((quality_score < 0)); then
|
||||
quality_score=0
|
||||
fi
|
||||
|
||||
# Determine status
|
||||
status="pass"
|
||||
if ((quality_score < 60)); then
|
||||
status="fail"
|
||||
elif ((quality_score < 80)); then
|
||||
status="warning"
|
||||
fi
|
||||
|
||||
# Output results
|
||||
if $JSON_OUTPUT; then
|
||||
# Build JSON output
|
||||
cat <<EOF
|
||||
{
|
||||
"files_checked": $files_checked,
|
||||
"example_count": $example_count,
|
||||
"placeholder_count": $placeholder_count,
|
||||
"todo_count": $todo_count,
|
||||
"files_with_issues": ${#files_with_issues[@]},
|
||||
"quality_score": $quality_score,
|
||||
"status": "$status",
|
||||
"issues": [
|
||||
$(IFS=; for issue in "${issues[@]:0:20}"; do # Limit to first 20 issues
|
||||
# Escape quotes in issue text
|
||||
escaped_issue="${issue//\"/\\\"}"
|
||||
echo " \"$escaped_issue\","
|
||||
done | sed '$ s/,$//')
|
||||
],
|
||||
"files_with_issues_list": [
|
||||
$(IFS=; for file_info in "${files_with_issues[@]:0:10}"; do # Limit to first 10 files
|
||||
file_path="${file_info%:*}"
|
||||
file_count="${file_info#*:}"
|
||||
echo " {\"file\": \"$file_path\", \"issue_count\": $file_count},"
|
||||
done | sed '$ s/,$//')
|
||||
]
|
||||
}
|
||||
EOF
|
||||
else
|
||||
# Human-readable output
|
||||
echo ""
|
||||
echo "Example Quality Validation"
|
||||
echo "========================================"
|
||||
echo "Files Checked: $files_checked"
|
||||
echo "Code Examples Found: $example_count"
|
||||
echo "Quality Score: $quality_score/100"
|
||||
echo ""
|
||||
|
||||
if ((placeholder_count > 0)) || ((todo_count > 0)); then
|
||||
echo "Issues Detected:"
|
||||
echo " • Placeholder patterns: $placeholder_count"
|
||||
echo " • TODO/FIXME markers: $todo_count"
|
||||
echo " • Files with issues: ${#files_with_issues[@]}"
|
||||
echo ""
|
||||
|
||||
if ((${#files_with_issues[@]} > 0)); then
|
||||
echo "Files with issues:"
|
||||
for file_info in "${files_with_issues[@]:0:5}"; do # Show first 5
|
||||
file_path="${file_info%:*}"
|
||||
file_count="${file_info#*:}"
|
||||
echo " • $file_path ($file_count issues)"
|
||||
done
|
||||
|
||||
if ((${#files_with_issues[@]} > 5)); then
|
||||
echo " ... and $((${#files_with_issues[@]} - 5)) more files"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Sample Issues:"
|
||||
for issue in "${issues[@]:0:5}"; do # Show first 5
|
||||
echo " • $issue"
|
||||
done
|
||||
|
||||
if ((${#issues[@]} > 5)); then
|
||||
echo " ... and $((${#issues[@]} - 5)) more issues"
|
||||
fi
|
||||
else
|
||||
echo "✓ No placeholder patterns detected"
|
||||
fi
|
||||
|
||||
if ((example_count < 2)); then
|
||||
echo ""
|
||||
echo "⚠ Recommendation: Add more code examples (found: $example_count, recommended: 3+)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [[ "$status" == "pass" ]]; then
|
||||
echo "Overall: ✓ PASS"
|
||||
elif [[ "$status" == "warning" ]]; then
|
||||
echo "Overall: ⚠ WARNINGS"
|
||||
else
|
||||
echo "Overall: ✗ FAIL"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Exit with appropriate code
|
||||
if [[ "$status" == "fail" ]]; then
|
||||
exit 1
|
||||
elif [[ "$status" == "warning" ]]; then
|
||||
exit 0 # Warning is not a failure
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
Reference in New Issue
Block a user