#!/bin/bash # Dependency Checker - Analyze file dependencies # # Purpose: Check for dependencies between files to inform commit ordering # Version: 1.0.0 # Usage: ./dependency-checker.sh [files...] # If no files specified, analyzes all changed files # Returns: # Exit 0: Success # Exit 1: Error # Exit 2: Invalid parameters # # Dependencies: git, bash 4.0+, grep set -euo pipefail VERBOSE=false OUTPUT_FORMAT="text" # Parse options while [[ $# -gt 0 ]]; do case "$1" in --verbose) VERBOSE=true shift ;; --format) OUTPUT_FORMAT="${2:-text}" shift 2 ;; *) break ;; esac done # Logging log() { if [[ "$VERBOSE" == "true" ]]; then echo "[DEBUG] $*" >&2 fi } # Get changed files if not specified FILES=() if [[ $# -eq 0 ]]; then log "Getting changed files from git..." while IFS= read -r file; do [[ -n "$file" ]] && FILES+=("$file") done < <(git diff --cached --name-only; git diff --name-only) else FILES=("$@") fi # Remove duplicates mapfile -t FILES < <(printf '%s\n' "${FILES[@]}" | sort -u) if [[ ${#FILES[@]} -eq 0 ]]; then echo "No files to analyze" >&2 exit 1 fi log "Analyzing ${#FILES[@]} files for dependencies..." # Dependency storage declare -A dependencies declare -A file_types # Detect file type detect_file_type() { local file="$1" case "$file" in *.py) echo "python" ;; *.js|*.jsx) echo "javascript" ;; *.ts|*.tsx) echo "typescript" ;; *.go) echo "go" ;; *.java) echo "java" ;; *.rb) echo "ruby" ;; *.rs) echo "rust" ;; *.md|*.txt|*.rst) echo "docs" ;; *test*|*spec*) echo "test" ;; *) echo "unknown" ;; esac } # Extract imports from Python file extract_python_imports() { local file="$1" local content if [[ -f "$file" ]]; then content=$(cat "$file") else # Try to get from git content=$(git show :"$file" 2>/dev/null || echo "") fi # Match: import module, from module import echo "$content" | grep -E "^(import|from) " | sed -E 's/^(import|from) ([a-zA-Z0-9_.]+).*/\2/' || true } # Extract imports from JavaScript/TypeScript extract_js_imports() { local file="$1" local content if [[ -f "$file" ]]; then content=$(cat "$file") else content=$(git show :"$file" 2>/dev/null || echo "") fi # Match: import from, require() { echo "$content" | grep -oE "import .* from ['\"]([^'\"]+)['\"]" | sed -E "s/.*from ['\"](.*)['\"].*/\1/" || true echo "$content" | grep -oE "require\(['\"]([^'\"]+)['\"]\)" | sed -E "s/.*require\(['\"]([^'\"]+)['\"]\).*/\1/" || true } | sort -u } # Extract imports from Go extract_go_imports() { local file="$1" local content if [[ -f "$file" ]]; then content=$(cat "$file") else content=$(git show :"$file" 2>/dev/null || echo "") fi # Match: import "module" or import ( "module" ) echo "$content" | grep -oE 'import +"[^"]+"' | sed -E 's/import +"([^"]+)".*/\1/' || true echo "$content" | sed -n '/^import (/,/^)/p' | grep -oE '"[^"]+"' | tr -d '"' || true } # Convert import path to file path import_to_file() { local import_path="$1" local file_type="$2" case "$file_type" in python) # module.submodule -> module/submodule.py echo "$import_path" | tr '.' '/' | sed 's|$|.py|' ;; javascript|typescript) # Handle relative imports if [[ "$import_path" == ./* ]] || [[ "$import_path" == ../* ]]; then echo "$import_path" else # node_modules - not a local file echo "" fi ;; go) # Package imports - check if local if [[ "$import_path" == github.com/* ]]; then echo "" else echo "$import_path" fi ;; *) echo "" ;; esac } # Find dependencies for each file log "Extracting imports and dependencies..." for file in "${FILES[@]}"; do file_type=$(detect_file_type "$file") file_types["$file"]="$file_type" log " $file: type=$file_type" case "$file_type" in python) imports=$(extract_python_imports "$file") ;; javascript|typescript) imports=$(extract_js_imports "$file") ;; go) imports=$(extract_go_imports "$file") ;; *) imports="" ;; esac if [[ -n "$imports" ]]; then log " Imports:" while IFS= read -r import_path; do [[ -z "$import_path" ]] && continue log " - $import_path" # Convert import to file path imported_file=$(import_to_file "$import_path" "$file_type") # Check if imported file is in our file list if [[ -n "$imported_file" ]]; then for other_file in "${FILES[@]}"; do if [[ "$other_file" == *"$imported_file"* ]]; then # file depends on other_file if [[ -z "${dependencies[$file]:-}" ]]; then dependencies["$file"]="$other_file" else dependencies["$file"]="${dependencies[$file]},$other_file" fi log " Dependency: $file -> $other_file" fi done fi done <<< "$imports" fi done # Detect test dependencies log "Detecting test dependencies..." for file in "${FILES[@]}"; do if [[ "${file_types[$file]}" == "test" ]]; then # Test file depends on implementation file impl_file="${file//test/}" impl_file="${impl_file//.test/}" impl_file="${impl_file//.spec/}" impl_file="${impl_file//tests\//}" impl_file="${impl_file//spec\//}" for other_file in "${FILES[@]}"; do if [[ "$other_file" == *"$impl_file"* ]] && [[ "$other_file" != "$file" ]]; then if [[ -z "${dependencies[$file]:-}" ]]; then dependencies["$file"]="$other_file" else dependencies["$file"]="${dependencies[$file]},$other_file" fi log " Test dependency: $file -> $other_file" fi done fi done # Output results if [[ "$OUTPUT_FORMAT" == "json" ]]; then echo "{" echo " \"files\": [" local first=true for file in "${FILES[@]}"; do if [[ "$first" == "true" ]]; then first=false else echo "," fi echo -n " {\"file\": \"$file\", \"type\": \"${file_types[$file]}\"" if [[ -n "${dependencies[$file]:-}" ]]; then IFS=',' read -ra deps <<< "${dependencies[$file]}" echo -n ", \"depends_on\": [" local first_dep=true for dep in "${deps[@]}"; do if [[ "$first_dep" == "true" ]]; then first_dep=false else echo -n ", " fi echo -n "\"$dep\"" done echo -n "]" fi echo -n "}" done echo "" echo " ]" echo "}" else echo "=== FILE DEPENDENCIES ===" echo "" echo "Files analyzed: ${#FILES[@]}" echo "" local has_dependencies=false for file in "${FILES[@]}"; do if [[ -n "${dependencies[$file]:-}" ]]; then has_dependencies=true echo "File: $file" echo " Type: ${file_types[$file]}" echo " Depends on:" IFS=',' read -ra deps <<< "${dependencies[$file]}" for dep in "${deps[@]}"; do echo " - $dep" done echo "" fi done if [[ "$has_dependencies" == "false" ]]; then echo "No dependencies detected." echo "All files can be committed independently." else echo "Recommendation:" echo " Commit dependencies before dependent files." echo " Group dependent files in same commit if they form atomic unit." fi fi log "Dependency analysis complete" exit 0