""" Project Structure Analyzer Analyzes React project structure against Bulletproof React patterns: - Feature-based organization (src/features/) - Unidirectional dependencies (shared → features → app) - No cross-feature imports - Proper folder hierarchy """ import re from pathlib import Path from typing import Dict, List, Set def analyze(codebase_path: Path, metadata: Dict) -> List[Dict]: """ Analyze project structure for Bulletproof React compliance. Args: codebase_path: Path to React codebase metadata: Project metadata from discovery phase Returns: List of findings with severity and migration guidance """ findings = [] src_dir = codebase_path / 'src' if not src_dir.exists(): findings.append({ 'severity': 'critical', 'category': 'structure', 'title': 'Missing src/ directory', 'current_state': 'No src/ directory found', 'target_state': 'All source code should be in src/ directory', 'migration_steps': [ 'Create src/ directory', 'Move all source files to src/', 'Update import paths', 'Update build configuration' ], 'effort': 'medium', }) return findings # Check for Bulletproof structure findings.extend(check_bulletproof_structure(src_dir)) # Check for cross-feature imports findings.extend(check_cross_feature_imports(src_dir)) # Analyze features/ organization findings.extend(analyze_features_directory(src_dir)) # Check shared code organization findings.extend(check_shared_code_organization(src_dir)) # Check for architectural violations findings.extend(check_architectural_violations(src_dir)) return findings def check_bulletproof_structure(src_dir: Path) -> List[Dict]: """Check for presence of Bulletproof React folder structure.""" findings = [] # Required top-level directories for Bulletproof React bulletproof_dirs = { 'app': 'Application layer (routes, app.tsx, provider.tsx, router.tsx)', 'features': 'Feature modules (80%+ of code should be here)', } # Recommended directories recommended_dirs = { 'components': 'Shared components used across multiple features', 'hooks': 'Shared custom hooks', 'lib': 'Third-party library configurations', 'utils': 'Shared utility functions', 'types': 'Shared TypeScript types', } # Check required directories for dir_name, description in bulletproof_dirs.items(): dir_path = src_dir / dir_name if not dir_path.exists(): findings.append({ 'severity': 'critical' if dir_name == 'features' else 'high', 'category': 'structure', 'title': f'Missing {dir_name}/ directory', 'current_state': f'No {dir_name}/ directory found', 'target_state': f'{dir_name}/ directory should exist: {description}', 'migration_steps': [ f'Create src/{dir_name}/ directory', f'Organize code according to Bulletproof React {dir_name} pattern', 'Update imports to use new structure' ], 'effort': 'high' if dir_name == 'features' else 'medium', }) # Check recommended directories (lower severity) missing_recommended = [] for dir_name, description in recommended_dirs.items(): dir_path = src_dir / dir_name if not dir_path.exists(): missing_recommended.append(f'{dir_name}/ ({description})') if missing_recommended: findings.append({ 'severity': 'medium', 'category': 'structure', 'title': 'Missing recommended directories', 'current_state': f'Missing: {", ".join([d.split("/")[0] for d in missing_recommended])}', 'target_state': 'Bulletproof React recommends these directories for shared code', 'migration_steps': [ 'Create missing directories as needed', 'Move shared code to appropriate directories', 'Ensure proper separation between shared and feature-specific code' ], 'effort': 'low', }) return findings def check_cross_feature_imports(src_dir: Path) -> List[Dict]: """Detect cross-feature imports (architectural violation).""" findings = [] features_dir = src_dir / 'features' if not features_dir.exists(): return findings # Get all feature directories feature_dirs = [d for d in features_dir.iterdir() if d.is_dir() and not d.name.startswith('.')] violations = [] for feature_dir in feature_dirs: # Find all TypeScript/JavaScript files in this feature for file_path in feature_dir.rglob('*.{ts,tsx,js,jsx}'): try: with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() # Check for imports from other features import_pattern = re.compile(r'from\s+[\'"]([^\'\"]+)[\'"]') imports = import_pattern.findall(content) for imp in imports: # Check if importing from another feature if imp.startswith('../') or imp.startswith('@/features/'): # Extract feature name from import path if '@/features/' in imp: imported_feature = imp.split('@/features/')[1].split('/')[0] elif '../' in imp: # Handle relative imports parts = imp.split('/') if 'features' in parts: idx = parts.index('features') if idx + 1 < len(parts): imported_feature = parts[idx + 1] else: continue else: continue else: continue # Check if importing from different feature current_feature = feature_dir.name if imported_feature != current_feature and imported_feature in [f.name for f in feature_dirs]: violations.append({ 'file': str(file_path.relative_to(src_dir)), 'from_feature': current_feature, 'to_feature': imported_feature, 'import': imp }) except: pass if violations: # Group violations by feature grouped = {} for v in violations: key = f"{v['from_feature']} → {v['to_feature']}" if key not in grouped: grouped[key] = [] grouped[key].append(v['file']) for import_path, files in grouped.items(): findings.append({ 'severity': 'high', 'category': 'structure', 'title': f'Cross-feature import: {import_path}', 'current_state': f'{len(files)} file(s) import from another feature', 'target_state': 'Features should be independent. Shared code belongs in src/components/, src/hooks/, or src/utils/', 'migration_steps': [ 'Identify what code is being shared between features', 'Move truly shared code to src/components/, src/hooks/, or src/utils/', 'If code is feature-specific, duplicate it or refactor feature boundaries', 'Update imports to use shared code location' ], 'effort': 'medium', 'affected_files': files[:5], # Show first 5 files }) return findings def analyze_features_directory(src_dir: Path) -> List[Dict]: """Analyze features/ directory structure.""" findings = [] features_dir = src_dir / 'features' if not features_dir.exists(): return findings feature_dirs = [d for d in features_dir.iterdir() if d.is_dir() and not d.name.startswith('.')] if len(feature_dirs) == 0: findings.append({ 'severity': 'high', 'category': 'structure', 'title': 'Empty features/ directory', 'current_state': 'features/ directory exists but contains no features', 'target_state': '80%+ of application code should be organized in feature modules', 'migration_steps': [ 'Identify distinct features in your application', 'Create a directory for each feature in src/features/', 'Move feature-specific code to appropriate feature directories', 'Organize each feature with api/, components/, hooks/, stores/, types/, utils/ as needed' ], 'effort': 'high', }) return findings # Check each feature for proper internal structure for feature_dir in feature_dirs: feature_name = feature_dir.name # Recommended feature subdirectories feature_subdirs = ['api', 'components', 'hooks', 'stores', 'types', 'utils'] has_subdirs = any((feature_dir / subdir).exists() for subdir in feature_subdirs) # Count files in feature root root_files = [f for f in feature_dir.iterdir() if f.is_file() and f.suffix in {'.ts', '.tsx', '.js', '.jsx'}] if len(root_files) > 5 and not has_subdirs: findings.append({ 'severity': 'medium', 'category': 'structure', 'title': f'Feature "{feature_name}" lacks internal organization', 'current_state': f'{len(root_files)} files in feature root without subdirectories', 'target_state': 'Features should be organized with api/, components/, hooks/, stores/, types/, utils/ subdirectories', 'migration_steps': [ f'Create subdirectories in src/features/{feature_name}/', 'Move API calls to api/', 'Move components to components/', 'Move hooks to hooks/', 'Move types to types/', 'Move utilities to utils/' ], 'effort': 'low', }) return findings def check_shared_code_organization(src_dir: Path) -> List[Dict]: """Check if shared code is properly organized.""" findings = [] components_dir = src_dir / 'components' features_dir = src_dir / 'features' if not components_dir.exists(): return findings # Count components shared_components = list(components_dir.rglob('*.{tsx,jsx}')) shared_count = len(shared_components) # Count feature components feature_count = 0 if features_dir.exists(): feature_count = len(list(features_dir.rglob('**/components/**/*.{tsx,jsx}'))) total_components = shared_count + feature_count if total_components > 0: shared_percentage = (shared_count / total_components) * 100 # Bulletproof React recommends 80%+ code in features if shared_percentage > 40: findings.append({ 'severity': 'medium', 'category': 'structure', 'title': 'Too many shared components', 'current_state': f'{shared_percentage:.1f}% of components are in src/components/ (shared)', 'target_state': 'Most components should be feature-specific. Only truly shared components belong in src/components/', 'migration_steps': [ 'Review each component in src/components/', 'Identify components used by only one feature', 'Move feature-specific components to their feature directories', 'Keep only truly shared components in src/components/' ], 'effort': 'medium', }) return findings def check_architectural_violations(src_dir: Path) -> List[Dict]: """Check for common architectural violations.""" findings = [] # Check for business logic in components/ components_dir = src_dir / 'components' if components_dir.exists(): large_components = [] for component_file in components_dir.rglob('*.{tsx,jsx}'): try: with open(component_file, 'r', encoding='utf-8', errors='ignore') as f: lines = len(f.readlines()) if lines > 200: large_components.append((str(component_file.relative_to(src_dir)), lines)) except: pass if large_components: findings.append({ 'severity': 'medium', 'category': 'structure', 'title': 'Large components in shared components/', 'current_state': f'{len(large_components)} component(s) > 200 lines in src/components/', 'target_state': 'Shared components should be simple and reusable. Complex components likely belong in features/', 'migration_steps': [ 'Review large shared components', 'Extract business logic to feature-specific hooks or utilities', 'Consider moving complex components to features/ if feature-specific', 'Keep shared components simple and focused' ], 'effort': 'medium', 'affected_files': [f[0] for f in large_components[:5]], }) # Check for proper app/ structure app_dir = src_dir / 'app' if app_dir.exists(): expected_app_files = ['app.tsx', 'provider.tsx', 'router.tsx'] has_routing = any((app_dir / f).exists() or (app_dir / 'routes').exists() for f in ['router.tsx', 'routes.tsx']) if not has_routing: findings.append({ 'severity': 'low', 'category': 'structure', 'title': 'Missing routing configuration in app/', 'current_state': 'No router.tsx or routes/ found in src/app/', 'target_state': 'Bulletproof React recommends centralizing routing in src/app/router.tsx or src/app/routes/', 'migration_steps': [ 'Create src/app/router.tsx or src/app/routes/', 'Define all application routes in one place', 'Use code splitting for route-level lazy loading' ], 'effort': 'low', }) return findings