pumuki-ast-hooks 5.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1105 -0
- package/bin/__tests__/auto-fix-violations.spec.js +132 -0
- package/bin/__tests__/auto-restart-guards.spec.js +11 -0
- package/bin/__tests__/check-doc-drift.spec.js +11 -0
- package/bin/__tests__/check-version.spec.js +240 -0
- package/bin/__tests__/cli.spec.js +11 -0
- package/bin/__tests__/guard-auto-manager.spec.js +11 -0
- package/bin/__tests__/guard-supervisor.spec.js +11 -0
- package/bin/__tests__/hook-status.spec.js +11 -0
- package/bin/__tests__/install.spec.js +11 -0
- package/bin/__tests__/nightly-metrics-report.spec.js +94 -0
- package/bin/__tests__/plan-review.spec.js +11 -0
- package/bin/__tests__/predictive-hooks.spec.js +11 -0
- package/bin/__tests__/run-ast-adapter.spec.js +11 -0
- package/bin/__tests__/run-orchestrator.spec.js +11 -0
- package/bin/__tests__/run-playbook.spec.js +11 -0
- package/bin/__tests__/setup-eslint.spec.js +11 -0
- package/bin/__tests__/violations-api.spec.js +11 -0
- package/bin/__tests__/watch-hooks.spec.js +11 -0
- package/bin/ai-commit.sh +5 -0
- package/bin/audit +5 -0
- package/bin/audit-library.js +6 -0
- package/bin/auto-fix-violations.js +19 -0
- package/bin/auto-restart-guards.js +6 -0
- package/bin/check-doc-drift.js +6 -0
- package/bin/check-version.js +19 -0
- package/bin/cleanup-branches.sh +5 -0
- package/bin/cli.js +6 -0
- package/bin/demo-recording.sh +5 -0
- package/bin/demo-violations +5 -0
- package/bin/fix-enforcer +5 -0
- package/bin/fix-gitflow-enforcement.sh +5 -0
- package/bin/generate-progress-report.sh +5 -0
- package/bin/git-analyze-pairs.sh +5 -0
- package/bin/git-leave-branch-check.sh +5 -0
- package/bin/gitflow +5 -0
- package/bin/gitflow-shell-integration.sh +5 -0
- package/bin/guard-auto-manager.js +6 -0
- package/bin/guard-autostart.sh +5 -0
- package/bin/guard-env.sh +5 -0
- package/bin/guard-supervisor.js +6 -0
- package/bin/hook-status.js +6 -0
- package/bin/install-git-wrapper.sh +5 -0
- package/bin/install.js +6 -0
- package/bin/kill-mcp-zombies.sh +5 -0
- package/bin/nightly-metrics-report.js +8 -0
- package/bin/plan-review.js +6 -0
- package/bin/predictive-hooks.js +6 -0
- package/bin/pumuki-audit.js +6 -0
- package/bin/pumuki-init.js +19 -0
- package/bin/pumuki-mcp-server.js +13 -0
- package/bin/pumuki-mcp.js +6 -0
- package/bin/pumuki-rules.js +6 -0
- package/bin/request-no-verify-approval.sh +5 -0
- package/bin/run-ast-adapter.js +6 -0
- package/bin/run-intelligent-audit.sh +5 -0
- package/bin/run-orchestrator.js +6 -0
- package/bin/run-playbook.js +6 -0
- package/bin/session-loader.sh +5 -0
- package/bin/setup-eslint.js +6 -0
- package/bin/start-guards.sh +5 -0
- package/bin/sync-autonomous-orchestrator.sh +5 -0
- package/bin/sync-to-library.sh +5 -0
- package/bin/update-evidence.sh +5 -0
- package/bin/update-session-context.sh +5 -0
- package/bin/verify-no-verify.sh +5 -0
- package/bin/violations +5 -0
- package/bin/violations-api.js +6 -0
- package/bin/watch-hooks.js +6 -0
- package/docs/API_REFERENCE.md +161 -0
- package/docs/ARCHITECTURE.md +236 -0
- package/docs/ARCHITECTURE_DETAILED.md +499 -0
- package/docs/BRANCH_PROTECTION_GUIDE.md +236 -0
- package/docs/CODE_STANDARDS.md +440 -0
- package/docs/CONTRIBUTING.md +246 -0
- package/docs/DEPENDENCIES.md +541 -0
- package/docs/HOW_IT_WORKS.md +716 -0
- package/docs/INSTALLATION.md +784 -0
- package/docs/MCP_SERVERS.md +786 -0
- package/docs/TESTING.md +423 -0
- package/docs/USAGE.md +856 -0
- package/docs/images/ast_intelligence_01.png +0 -0
- package/docs/images/ast_intelligence_02.png +0 -0
- package/docs/images/ast_intelligence_03.png +0 -0
- package/docs/images/ast_intelligence_04.png +0 -0
- package/docs/images/ast_intelligence_05.png +0 -0
- package/hooks/getSkillRulesPath.ts +52 -0
- package/hooks/git-status-monitor.ts +160 -0
- package/hooks/index.js +5 -0
- package/hooks/notify-macos.ts +42 -0
- package/hooks/package.json +16 -0
- package/hooks/post-tool-use-tracker.sh +89 -0
- package/hooks/pre-tool-use-evidence-validator.ts +252 -0
- package/hooks/pre-tool-use-guard.ts +151 -0
- package/hooks/skill-activation-prompt.sh +8 -0
- package/hooks/skill-activation-prompt.ts +307 -0
- package/index.js +49 -0
- package/package.json +117 -0
- package/presentation/cli/audit.sh +24 -0
- package/presentation/cli/autonomous-status.sh +92 -0
- package/presentation/cli/categorize-violations.sh +179 -0
- package/presentation/cli/direct-audit-option2.sh +23 -0
- package/presentation/cli/direct-audit.sh +33 -0
- package/scripts/hooks-system/.AI_TOKEN_STATUS.txt +16 -0
- package/scripts/hooks-system/.audit-reports/auto-recovery.log +1 -0
- package/scripts/hooks-system/.audit-reports/install-wizard.log +4 -0
- package/scripts/hooks-system/.audit-reports/notifications.log +425 -0
- package/scripts/hooks-system/.audit-reports/token-monitor.log +1275 -0
- package/scripts/hooks-system/.audit_tmp/intelligent-report.json +44953 -0
- package/scripts/hooks-system/.audit_tmp/intelligent-report.txt +1338 -0
- package/scripts/hooks-system/.audit_tmp/severity-history.jsonl +1 -0
- package/scripts/hooks-system/.audit_tmp/token-usage.jsonl +1 -0
- package/scripts/hooks-system/.hook-system/config.json +8 -0
- package/scripts/hooks-system/application/CompositionRoot.js +325 -0
- package/scripts/hooks-system/application/__tests__/CompositionRoot.spec.js +84 -0
- package/scripts/hooks-system/application/commands/index.js +64 -0
- package/scripts/hooks-system/application/queries/index.js +60 -0
- package/scripts/hooks-system/application/services/AutonomousOrchestrator.js +130 -0
- package/scripts/hooks-system/application/services/ContextDetectionEngine.js +181 -0
- package/scripts/hooks-system/application/services/DynamicRulesLoader.js +182 -0
- package/scripts/hooks-system/application/services/GitFlowService.js +156 -0
- package/scripts/hooks-system/application/services/GitTreeState.js +140 -0
- package/scripts/hooks-system/application/services/HookSystemScheduler.js +77 -0
- package/scripts/hooks-system/application/services/IntelligentCommitAnalyzer.js +151 -0
- package/scripts/hooks-system/application/services/IntelligentGitTreeMonitor.js +118 -0
- package/scripts/hooks-system/application/services/PlatformAnalysisService.js +173 -0
- package/scripts/hooks-system/application/services/PlatformDetectionService.js +168 -0
- package/scripts/hooks-system/application/services/PlaybookRunner.js +39 -0
- package/scripts/hooks-system/application/services/PredictiveHookAdvisor.js +56 -0
- package/scripts/hooks-system/application/services/RealtimeGuardPlugin.js +62 -0
- package/scripts/hooks-system/application/services/RealtimeGuardService.js +374 -0
- package/scripts/hooks-system/application/services/SmartDirtyTreeAnalyzer.js +63 -0
- package/scripts/hooks-system/application/services/__tests__/AutonomousOrchestrator.spec.js +36 -0
- package/scripts/hooks-system/application/services/__tests__/ContextDetectionEngine.spec.js +33 -0
- package/scripts/hooks-system/application/services/__tests__/DynamicRulesLoader.spec.js +43 -0
- package/scripts/hooks-system/application/services/__tests__/GitTreeState.spec.js +163 -0
- package/scripts/hooks-system/application/services/__tests__/HookSystemScheduler.spec.js +207 -0
- package/scripts/hooks-system/application/services/__tests__/IntelligentCommitAnalyzer.spec.js +365 -0
- package/scripts/hooks-system/application/services/__tests__/IntelligentGitTreeMonitor.spec.js +188 -0
- package/scripts/hooks-system/application/services/__tests__/PlatformDetectionService.spec.js +28 -0
- package/scripts/hooks-system/application/services/__tests__/PlaybookRunner.spec.js +143 -0
- package/scripts/hooks-system/application/services/__tests__/PredictiveHookAdvisor.spec.js +181 -0
- package/scripts/hooks-system/application/services/__tests__/RealtimeGuardPlugin.spec.js +45 -0
- package/scripts/hooks-system/application/services/__tests__/RealtimeGuardService.critical.spec.js +401 -0
- package/scripts/hooks-system/application/services/commit/CommitMessageGenerator.js +34 -0
- package/scripts/hooks-system/application/services/commit/FeatureDetector.js +101 -0
- package/scripts/hooks-system/application/services/evidence/EvidenceContextManager.js +163 -0
- package/scripts/hooks-system/application/services/evidence/__tests__/EvidenceContextManager.spec.js +98 -0
- package/scripts/hooks-system/application/services/guard/GuardAutoManagerService.js +169 -0
- package/scripts/hooks-system/application/services/guard/GuardConfig.js +15 -0
- package/scripts/hooks-system/application/services/guard/GuardEventLogger.js +70 -0
- package/scripts/hooks-system/application/services/guard/GuardHealthReminder.js +54 -0
- package/scripts/hooks-system/application/services/guard/GuardHeartbeatMonitor.js +94 -0
- package/scripts/hooks-system/application/services/guard/GuardLockManager.js +72 -0
- package/scripts/hooks-system/application/services/guard/GuardMonitorLoop.js +29 -0
- package/scripts/hooks-system/application/services/guard/GuardNotificationHandler.js +36 -0
- package/scripts/hooks-system/application/services/guard/GuardProcessManager.js +113 -0
- package/scripts/hooks-system/application/services/guard/GuardRecoveryService.js +90 -0
- package/scripts/hooks-system/application/services/guard/__tests__/GuardAutoManagerService.spec.js +77 -0
- package/scripts/hooks-system/application/services/installation/ConfigurationGeneratorService.js +123 -0
- package/scripts/hooks-system/application/services/installation/FileSystemInstallerService.js +112 -0
- package/scripts/hooks-system/application/services/installation/GitEnvironmentService.js +166 -0
- package/scripts/hooks-system/application/services/installation/HookInstaller.js +197 -0
- package/scripts/hooks-system/application/services/installation/IdeIntegrationService.js +37 -0
- package/scripts/hooks-system/application/services/installation/InstallService.js +130 -0
- package/scripts/hooks-system/application/services/installation/McpConfigurator.js +172 -0
- package/scripts/hooks-system/application/services/installation/PlatformDetectorService.js +36 -0
- package/scripts/hooks-system/application/services/installation/VSCodeTaskConfigurator.js +97 -0
- package/scripts/hooks-system/application/services/logging/UnifiedLogger.js +142 -0
- package/scripts/hooks-system/application/services/logging/__tests__/UnifiedLogger.spec.js +66 -0
- package/scripts/hooks-system/application/services/monitoring/ActivityMonitor.js +80 -0
- package/scripts/hooks-system/application/services/monitoring/AstMonitor.js +140 -0
- package/scripts/hooks-system/application/services/monitoring/DevDocsMonitor.js +85 -0
- package/scripts/hooks-system/application/services/monitoring/EvidenceMonitor.js +103 -0
- package/scripts/hooks-system/application/services/monitoring/EvidenceMonitorService.js +162 -0
- package/scripts/hooks-system/application/services/monitoring/GitTreeMonitor.js +123 -0
- package/scripts/hooks-system/application/services/monitoring/GitTreeMonitorService.js +114 -0
- package/scripts/hooks-system/application/services/monitoring/HealthCheckProviders.js +153 -0
- package/scripts/hooks-system/application/services/monitoring/HealthCheckService.js +118 -0
- package/scripts/hooks-system/application/services/monitoring/HeartbeatMonitorService.js +61 -0
- package/scripts/hooks-system/application/services/monitoring/TokenMonitor.js +60 -0
- package/scripts/hooks-system/application/services/monitoring/__tests__/EvidenceMonitorService.spec.js +107 -0
- package/scripts/hooks-system/application/services/monitoring/__tests__/GitTreeMonitorService.spec.js +27 -0
- package/scripts/hooks-system/application/services/monitoring/__tests__/HealthCheckProviders.spec.js +68 -0
- package/scripts/hooks-system/application/services/monitoring/__tests__/HealthCheckService.spec.js +69 -0
- package/scripts/hooks-system/application/services/monitoring/__tests__/HeartbeatMonitorService.spec.js +35 -0
- package/scripts/hooks-system/application/services/notification/MacNotificationSender.js +106 -0
- package/scripts/hooks-system/application/services/notification/NotificationCenterService.js +221 -0
- package/scripts/hooks-system/application/services/notification/NotificationDispatcher.js +42 -0
- package/scripts/hooks-system/application/services/notification/__tests__/NotificationCenterService.spec.js +40 -0
- package/scripts/hooks-system/application/services/notification/components/NotificationCooldownManager.js +62 -0
- package/scripts/hooks-system/application/services/notification/components/NotificationDeduplicator.js +67 -0
- package/scripts/hooks-system/application/services/notification/components/NotificationQueue.js +36 -0
- package/scripts/hooks-system/application/services/notification/components/NotificationRetryExecutor.js +58 -0
- package/scripts/hooks-system/application/services/platform/PlatformHeuristics.js +144 -0
- package/scripts/hooks-system/application/services/recovery/AutoRecoveryManager.js +137 -0
- package/scripts/hooks-system/application/services/recovery/__tests__/AutoRecoveryManager.spec.js +62 -0
- package/scripts/hooks-system/application/services/smart-commit/CommitMessageSuggester.js +97 -0
- package/scripts/hooks-system/application/services/smart-commit/FileContextGrouper.js +114 -0
- package/scripts/hooks-system/application/services/smart-commit/SmartCommitSummaryBuilder.js +53 -0
- package/scripts/hooks-system/application/services/token/CursorTokenService.js +44 -0
- package/scripts/hooks-system/application/services/token/TokenMetricsService.js +109 -0
- package/scripts/hooks-system/application/services/token/TokenMonitorService.js +160 -0
- package/scripts/hooks-system/application/services/token/TokenStatusReporter.js +56 -0
- package/scripts/hooks-system/application/services/token/__tests__/CursorTokenService.spec.js +69 -0
- package/scripts/hooks-system/application/services/token/__tests__/TokenMonitorService.spec.js +185 -0
- package/scripts/hooks-system/application/state/HookSystemStateMachine.js +59 -0
- package/scripts/hooks-system/application/state/__tests__/HookSystemStateMachine.spec.js +115 -0
- package/scripts/hooks-system/application/use-cases/AnalyzeCodebaseUseCase.js +54 -0
- package/scripts/hooks-system/application/use-cases/AnalyzeStagedFilesUseCase.js +61 -0
- package/scripts/hooks-system/application/use-cases/AutoExecuteAIStartUseCase.js +123 -0
- package/scripts/hooks-system/application/use-cases/BlockCommitUseCase.js +90 -0
- package/scripts/hooks-system/application/use-cases/GenerateAuditReportUseCase.js +184 -0
- package/scripts/hooks-system/application/use-cases/__tests__/AnalyzeCodebaseUseCase.spec.js +156 -0
- package/scripts/hooks-system/application/use-cases/__tests__/AnalyzeStagedFilesUseCase.spec.js +146 -0
- package/scripts/hooks-system/application/use-cases/__tests__/AutoExecuteAIStartUseCase.spec.js +89 -0
- package/scripts/hooks-system/application/use-cases/__tests__/BlockCommitUseCase.spec.js +171 -0
- package/scripts/hooks-system/application/use-cases/__tests__/GenerateAuditReportUseCase.spec.js +207 -0
- package/scripts/hooks-system/bin/__tests__/auto-fix-violations.spec.js +132 -0
- package/scripts/hooks-system/bin/__tests__/auto-restart-guards.spec.js +11 -0
- package/scripts/hooks-system/bin/__tests__/check-doc-drift.spec.js +11 -0
- package/scripts/hooks-system/bin/__tests__/check-version.spec.js +240 -0
- package/scripts/hooks-system/bin/__tests__/cli.spec.js +11 -0
- package/scripts/hooks-system/bin/__tests__/guard-auto-manager.spec.js +11 -0
- package/scripts/hooks-system/bin/__tests__/guard-supervisor.spec.js +11 -0
- package/scripts/hooks-system/bin/__tests__/hook-status.spec.js +11 -0
- package/scripts/hooks-system/bin/__tests__/install.spec.js +11 -0
- package/scripts/hooks-system/bin/__tests__/nightly-metrics-report.spec.js +94 -0
- package/scripts/hooks-system/bin/__tests__/plan-review.spec.js +11 -0
- package/scripts/hooks-system/bin/__tests__/predictive-hooks.spec.js +11 -0
- package/scripts/hooks-system/bin/__tests__/run-ast-adapter.spec.js +11 -0
- package/scripts/hooks-system/bin/__tests__/run-orchestrator.spec.js +11 -0
- package/scripts/hooks-system/bin/__tests__/run-playbook.spec.js +11 -0
- package/scripts/hooks-system/bin/__tests__/setup-eslint.spec.js +11 -0
- package/scripts/hooks-system/bin/__tests__/violations-api.spec.js +11 -0
- package/scripts/hooks-system/bin/__tests__/watch-hooks.spec.js +11 -0
- package/scripts/hooks-system/bin/ai-commit.sh +63 -0
- package/scripts/hooks-system/bin/audit +463 -0
- package/scripts/hooks-system/bin/audit-library.js +54 -0
- package/scripts/hooks-system/bin/auto-fix-violations.js +130 -0
- package/scripts/hooks-system/bin/auto-restart-guards.js +93 -0
- package/scripts/hooks-system/bin/check-doc-drift.js +35 -0
- package/scripts/hooks-system/bin/check-version.js +201 -0
- package/scripts/hooks-system/bin/cleanup-branches.sh +106 -0
- package/scripts/hooks-system/bin/cli.js +208 -0
- package/scripts/hooks-system/bin/demo-recording.sh +57 -0
- package/scripts/hooks-system/bin/demo-violations +44 -0
- package/scripts/hooks-system/bin/fix-enforcer +27 -0
- package/scripts/hooks-system/bin/fix-gitflow-enforcement.sh +68 -0
- package/scripts/hooks-system/bin/generate-progress-report.sh +129 -0
- package/scripts/hooks-system/bin/git-analyze-pairs.sh +0 -0
- package/scripts/hooks-system/bin/git-leave-branch-check.sh +73 -0
- package/scripts/hooks-system/bin/gitflow +17 -0
- package/scripts/hooks-system/bin/gitflow-shell-integration.sh +64 -0
- package/scripts/hooks-system/bin/guard-auto-manager.js +44 -0
- package/scripts/hooks-system/bin/guard-autostart.sh +158 -0
- package/scripts/hooks-system/bin/guard-env.sh +40 -0
- package/scripts/hooks-system/bin/guard-supervisor.js +516 -0
- package/scripts/hooks-system/bin/hook-status.js +41 -0
- package/scripts/hooks-system/bin/install-git-wrapper.sh +53 -0
- package/scripts/hooks-system/bin/install.js +10 -0
- package/scripts/hooks-system/bin/kill-mcp-zombies.sh +48 -0
- package/scripts/hooks-system/bin/nightly-metrics-report.js +138 -0
- package/scripts/hooks-system/bin/plan-review.js +31 -0
- package/scripts/hooks-system/bin/predictive-hooks.js +18 -0
- package/scripts/hooks-system/bin/pumuki-audit.js +113 -0
- package/scripts/hooks-system/bin/pumuki-init.js +104 -0
- package/scripts/hooks-system/bin/pumuki-mcp.js +74 -0
- package/scripts/hooks-system/bin/pumuki-rules.js +74 -0
- package/scripts/hooks-system/bin/request-no-verify-approval.sh +116 -0
- package/scripts/hooks-system/bin/run-ast-adapter.js +86 -0
- package/scripts/hooks-system/bin/run-intelligent-audit.sh +67 -0
- package/scripts/hooks-system/bin/run-orchestrator.js +27 -0
- package/scripts/hooks-system/bin/run-playbook.js +23 -0
- package/scripts/hooks-system/bin/session-loader.sh +264 -0
- package/scripts/hooks-system/bin/setup-eslint.js +110 -0
- package/scripts/hooks-system/bin/start-guards.sh +190 -0
- package/scripts/hooks-system/bin/sync-autonomous-orchestrator.sh +32 -0
- package/scripts/hooks-system/bin/sync-to-library.sh +46 -0
- package/scripts/hooks-system/bin/update-evidence.sh +1167 -0
- package/scripts/hooks-system/bin/update-session-context.sh +261 -0
- package/scripts/hooks-system/bin/verify-no-verify.sh +68 -0
- package/scripts/hooks-system/bin/violations +20 -0
- package/scripts/hooks-system/bin/violations-api.js +345 -0
- package/scripts/hooks-system/bin/watch-hooks.js +20 -0
- package/scripts/hooks-system/config/project.config.json +36 -0
- package/scripts/hooks-system/config/state-map.json +12 -0
- package/scripts/hooks-system/domain/entities/AuditResult.js +139 -0
- package/scripts/hooks-system/domain/entities/Finding.js +116 -0
- package/scripts/hooks-system/domain/entities/SeverityConfig.js +73 -0
- package/scripts/hooks-system/domain/entities/SeverityConfig.ts +90 -0
- package/scripts/hooks-system/domain/entities/__tests__/AuditResult.spec.js +450 -0
- package/scripts/hooks-system/domain/entities/__tests__/Finding.spec.js +335 -0
- package/scripts/hooks-system/domain/entities/__tests__/SeverityConfig.spec.js +240 -0
- package/scripts/hooks-system/domain/entities/__tests__/entities.spec.js +29 -0
- package/scripts/hooks-system/domain/errors/__tests__/DomainErrors.spec.js +59 -0
- package/scripts/hooks-system/domain/errors/index.js +169 -0
- package/scripts/hooks-system/domain/events/__tests__/DomainEvents.spec.js +60 -0
- package/scripts/hooks-system/domain/events/index.js +121 -0
- package/scripts/hooks-system/domain/ports/IAstPort.js +67 -0
- package/scripts/hooks-system/domain/ports/IEvidencePort.js +86 -0
- package/scripts/hooks-system/domain/ports/IGitCommandPort.js +110 -0
- package/scripts/hooks-system/domain/ports/IGitPort.js +114 -0
- package/scripts/hooks-system/domain/ports/IGitQueryPort.js +93 -0
- package/scripts/hooks-system/domain/ports/INotificationPort.js +35 -0
- package/scripts/hooks-system/domain/ports/__tests__/ports.spec.js +36 -0
- package/scripts/hooks-system/domain/ports/index.js +14 -0
- package/scripts/hooks-system/domain/repositories/ICursorTokenRepository.js +13 -0
- package/scripts/hooks-system/domain/repositories/IFindingsRepository.js +30 -0
- package/scripts/hooks-system/domain/repositories/__tests__/IFindingsRepository.spec.js +18 -0
- package/scripts/hooks-system/domain/rules/CommitBlockingRules.js +142 -0
- package/scripts/hooks-system/domain/rules/__tests__/CommitBlockingRules.spec.js +18 -0
- package/scripts/hooks-system/domain/services/AuditAnalyzer.js +103 -0
- package/scripts/hooks-system/domain/services/AuditFilter.js +26 -0
- package/scripts/hooks-system/domain/services/AuditResultSerializer.js +35 -0
- package/scripts/hooks-system/domain/services/AuditScorer.js +38 -0
- package/scripts/hooks-system/domain/values/Severity.js +93 -0
- package/scripts/hooks-system/index.js +49 -0
- package/scripts/hooks-system/infrastructure/adapters/AstAnalyzerAdapter.js +150 -0
- package/scripts/hooks-system/infrastructure/adapters/FileEvidenceAdapter.js +140 -0
- package/scripts/hooks-system/infrastructure/adapters/GitCliAdapter.js +16 -0
- package/scripts/hooks-system/infrastructure/adapters/GitCommandAdapter.js +68 -0
- package/scripts/hooks-system/infrastructure/adapters/GitHubCliAdapter.js +85 -0
- package/scripts/hooks-system/infrastructure/adapters/GitQueryAdapter.js +58 -0
- package/scripts/hooks-system/infrastructure/adapters/LegacyAnalyzerAdapter.js +61 -0
- package/scripts/hooks-system/infrastructure/adapters/MacOSNotificationAdapter.js +99 -0
- package/scripts/hooks-system/infrastructure/adapters/__tests__/AstAnalyzerAdapter.spec.js +32 -0
- package/scripts/hooks-system/infrastructure/adapters/__tests__/FileEvidenceAdapter.spec.js +31 -0
- package/scripts/hooks-system/infrastructure/adapters/__tests__/GitCliAdapter.spec.js +39 -0
- package/scripts/hooks-system/infrastructure/adapters/__tests__/MacOSNotificationAdapter.spec.js +33 -0
- package/scripts/hooks-system/infrastructure/adapters/git/GitCommandRunner.js +78 -0
- package/scripts/hooks-system/infrastructure/adapters/git/GitCommandService.js +67 -0
- package/scripts/hooks-system/infrastructure/adapters/git/GitQueryService.js +50 -0
- package/scripts/hooks-system/infrastructure/adapters/index.js +14 -0
- package/scripts/hooks-system/infrastructure/ast/README.md +198 -0
- package/scripts/hooks-system/infrastructure/ast/__tests__/ast-core.spec.js +160 -0
- package/scripts/hooks-system/infrastructure/ast/__tests__/ast-intelligence.spec.js +20 -0
- package/scripts/hooks-system/infrastructure/ast/android/__tests__/ast-android.spec.js +33 -0
- package/scripts/hooks-system/infrastructure/ast/android/__tests__/clean-architecture-analyzer.spec.js +96 -0
- package/scripts/hooks-system/infrastructure/ast/android/__tests__/ddd-analyzer.spec.js +113 -0
- package/scripts/hooks-system/infrastructure/ast/android/__tests__/detekt-runner.spec.js +36 -0
- package/scripts/hooks-system/infrastructure/ast/android/__tests__/feature-first-analyzer.spec.js +80 -0
- package/scripts/hooks-system/infrastructure/ast/android/__tests__/native-bridge.spec.js +31 -0
- package/scripts/hooks-system/infrastructure/ast/android/analyzers/AndroidASTIntelligentAnalyzer.js +15 -0
- package/scripts/hooks-system/infrastructure/ast/android/analyzers/AndroidASTParser.js +157 -0
- package/scripts/hooks-system/infrastructure/ast/android/analyzers/AndroidAnalysisOrchestrator.js +164 -0
- package/scripts/hooks-system/infrastructure/ast/android/analyzers/AndroidArchitectureDetector.js +334 -0
- package/scripts/hooks-system/infrastructure/ast/android/analyzers/AndroidClassAnalyzer.js +162 -0
- package/scripts/hooks-system/infrastructure/ast/android/analyzers/AndroidForbiddenLiteralsAnalyzer.js +261 -0
- package/scripts/hooks-system/infrastructure/ast/android/analyzers/AndroidSOLIDAnalyzer.js +287 -0
- package/scripts/hooks-system/infrastructure/ast/android/analyzers/__tests__/AndroidForbiddenLiteralsAnalyzer.spec.js +58 -0
- package/scripts/hooks-system/infrastructure/ast/android/analyzers/__tests__/AndroidSOLIDAnalyzer.spec.js +84 -0
- package/scripts/hooks-system/infrastructure/ast/android/ast-android.js +1785 -0
- package/scripts/hooks-system/infrastructure/ast/android/clean-architecture-analyzer.js +115 -0
- package/scripts/hooks-system/infrastructure/ast/android/ddd-analyzer.js +70 -0
- package/scripts/hooks-system/infrastructure/ast/android/detekt-runner.js +81 -0
- package/scripts/hooks-system/infrastructure/ast/android/feature-first-analyzer.js +53 -0
- package/scripts/hooks-system/infrastructure/ast/android/native-bridge.js +119 -0
- package/scripts/hooks-system/infrastructure/ast/archive/README.md +18 -0
- package/scripts/hooks-system/infrastructure/ast/archive/ast-intelligence.ts +276 -0
- package/scripts/hooks-system/infrastructure/ast/archive/ios-rules.js +329 -0
- package/scripts/hooks-system/infrastructure/ast/archive/kotlin-analyzer.js +332 -0
- package/scripts/hooks-system/infrastructure/ast/archive/kotlin-parser.js +303 -0
- package/scripts/hooks-system/infrastructure/ast/archive/swift-analyzer.js +390 -0
- package/scripts/hooks-system/infrastructure/ast/ast-core.js +594 -0
- package/scripts/hooks-system/infrastructure/ast/ast-intelligence.js +617 -0
- package/scripts/hooks-system/infrastructure/ast/backend/__tests__/ast-backend.spec.js +20 -0
- package/scripts/hooks-system/infrastructure/ast/backend/__tests__/clean-architecture-analyzer.spec.js +151 -0
- package/scripts/hooks-system/infrastructure/ast/backend/__tests__/ddd-analyzer.spec.js +124 -0
- package/scripts/hooks-system/infrastructure/ast/backend/__tests__/feature-first-analyzer.spec.js +128 -0
- package/scripts/hooks-system/infrastructure/ast/backend/__tests__/forbidden-literals-analyzer.spec.js +95 -0
- package/scripts/hooks-system/infrastructure/ast/backend/__tests__/nestjs-patterns-analyzer.spec.js +59 -0
- package/scripts/hooks-system/infrastructure/ast/backend/__tests__/solid-analyzer.spec.js +114 -0
- package/scripts/hooks-system/infrastructure/ast/backend/analyzers/BackendArchitectureDetector.js +141 -0
- package/scripts/hooks-system/infrastructure/ast/backend/analyzers/BackendPatternDetector.js +23 -0
- package/scripts/hooks-system/infrastructure/ast/backend/analyzers/__tests__/BackendArchitectureDetector.spec.js +239 -0
- package/scripts/hooks-system/infrastructure/ast/backend/analyzers/__tests__/BackendPatternDetector.spec.js +58 -0
- package/scripts/hooks-system/infrastructure/ast/backend/analyzers/detectors/CQRSDetector.js +41 -0
- package/scripts/hooks-system/infrastructure/ast/backend/analyzers/detectors/CleanArchitectureDetector.js +52 -0
- package/scripts/hooks-system/infrastructure/ast/backend/analyzers/detectors/FeatureFirstCleanDetector.js +74 -0
- package/scripts/hooks-system/infrastructure/ast/backend/analyzers/detectors/LayeredArchitectureDetector.js +25 -0
- package/scripts/hooks-system/infrastructure/ast/backend/analyzers/detectors/MVCDetector.js +32 -0
- package/scripts/hooks-system/infrastructure/ast/backend/analyzers/detectors/OnionArchitectureDetector.js +32 -0
- package/scripts/hooks-system/infrastructure/ast/backend/ast-backend-clean.js +44 -0
- package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +2048 -0
- package/scripts/hooks-system/infrastructure/ast/backend/clean-architecture-analyzer.js +142 -0
- package/scripts/hooks-system/infrastructure/ast/backend/ddd-analyzer.js +256 -0
- package/scripts/hooks-system/infrastructure/ast/backend/feature-first-analyzer.js +70 -0
- package/scripts/hooks-system/infrastructure/ast/backend/forbidden-literals-analyzer.js +236 -0
- package/scripts/hooks-system/infrastructure/ast/backend/nestjs-patterns-analyzer.js +11 -0
- package/scripts/hooks-system/infrastructure/ast/backend/solid-analyzer.js +392 -0
- package/scripts/hooks-system/infrastructure/ast/common/BDDTDDWorkflowRules.js +52 -0
- package/scripts/hooks-system/infrastructure/ast/common/__tests__/BDDTDDWorkflowRules.spec.js +133 -0
- package/scripts/hooks-system/infrastructure/ast/common/__tests__/ast-common.spec.js +20 -0
- package/scripts/hooks-system/infrastructure/ast/common/__tests__/documentation-analyzer.spec.js +120 -0
- package/scripts/hooks-system/infrastructure/ast/common/__tests__/images-backend-analyzer.spec.js +123 -0
- package/scripts/hooks-system/infrastructure/ast/common/__tests__/monorepo-health-analyzer.spec.js +118 -0
- package/scripts/hooks-system/infrastructure/ast/common/__tests__/network-resilience-analyzer.spec.js +180 -0
- package/scripts/hooks-system/infrastructure/ast/common/__tests__/offline-backend-analyzer.spec.js +111 -0
- package/scripts/hooks-system/infrastructure/ast/common/__tests__/push-backend-analyzer.spec.js +124 -0
- package/scripts/hooks-system/infrastructure/ast/common/ast-common.js +345 -0
- package/scripts/hooks-system/infrastructure/ast/common/documentation-analyzer.js +217 -0
- package/scripts/hooks-system/infrastructure/ast/common/images-backend-analyzer.js +36 -0
- package/scripts/hooks-system/infrastructure/ast/common/monorepo-health-analyzer.js +452 -0
- package/scripts/hooks-system/infrastructure/ast/common/network-resilience-analyzer.js +178 -0
- package/scripts/hooks-system/infrastructure/ast/common/offline-backend-analyzer.js +53 -0
- package/scripts/hooks-system/infrastructure/ast/common/push-backend-analyzer.js +42 -0
- package/scripts/hooks-system/infrastructure/ast/common/rules/BDDRules.js +87 -0
- package/scripts/hooks-system/infrastructure/ast/common/rules/ImplementationRules.js +83 -0
- package/scripts/hooks-system/infrastructure/ast/common/rules/TDDRules.js +109 -0
- package/scripts/hooks-system/infrastructure/ast/common/rules/WorkflowRules.js +137 -0
- package/scripts/hooks-system/infrastructure/ast/frontend/__tests__/ast-frontend.spec.js +20 -0
- package/scripts/hooks-system/infrastructure/ast/frontend/analyzers/FrontendArchitectureDetector.js +289 -0
- package/scripts/hooks-system/infrastructure/ast/frontend/analyzers/FrontendForbiddenLiteralsAnalyzer.js +257 -0
- package/scripts/hooks-system/infrastructure/ast/frontend/analyzers/FrontendSOLIDAnalyzer.js +274 -0
- package/scripts/hooks-system/infrastructure/ast/frontend/analyzers/__tests__/FrontendArchitectureDetector.spec.js +151 -0
- package/scripts/hooks-system/infrastructure/ast/frontend/analyzers/__tests__/FrontendForbiddenLiteralsAnalyzer.spec.js +20 -0
- package/scripts/hooks-system/infrastructure/ast/frontend/analyzers/__tests__/FrontendSOLIDAnalyzer.spec.js +108 -0
- package/scripts/hooks-system/infrastructure/ast/frontend/ast-frontend-clean.js +42 -0
- package/scripts/hooks-system/infrastructure/ast/frontend/ast-frontend.js +2094 -0
- package/scripts/hooks-system/infrastructure/ast/frontend/clean-architecture-analyzer.js +88 -0
- package/scripts/hooks-system/infrastructure/ast/frontend/ddd-analyzer.js +94 -0
- package/scripts/hooks-system/infrastructure/ast/frontend/feature-first-analyzer.js +51 -0
- package/scripts/hooks-system/infrastructure/ast/ios/__tests__/ast-ios.spec.js +40 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/__tests__/iOSArchitectureDetector.spec.js +20 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/__tests__/iOSArchitectureRules.spec.js +61 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/__tests__/iOSCICDRules.spec.js +10 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/__tests__/iOSEnterpriseAnalyzer.spec.js +36 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/__tests__/iOSForbiddenLiteralsAnalyzer.spec.js +64 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/__tests__/iOSNetworkingAdvancedRules.spec.js +10 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/__tests__/iOSPerformanceRules.spec.js +34 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/__tests__/iOSSPMRules.spec.js +10 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/__tests__/iOSSwiftUIAdvancedRules.spec.js +10 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js +894 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSArchitectureDetector.js +445 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSArchitectureRules.js +700 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSCICDRules.js +431 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseAnalyzer.js +580 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSForbiddenLiteralsAnalyzer.js +261 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSNetworkingAdvancedRules.js +177 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSPerformanceRules.js +11 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSSPMRules.js +496 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSSwiftUIAdvancedRules.js +333 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSTestingAdvancedRules.js +225 -0
- package/scripts/hooks-system/infrastructure/ast/ios/ast-ios.js +2176 -0
- package/scripts/hooks-system/infrastructure/ast/ios/native-bridge.js +92 -0
- package/scripts/hooks-system/infrastructure/ast/ios/parsers/SourceKittenParser.js +471 -0
- package/scripts/hooks-system/infrastructure/ast/ios/parsers/__tests__/SourceKittenParser.spec.js +41 -0
- package/scripts/hooks-system/infrastructure/ast/text/__tests__/text-scanner.spec.js +20 -0
- package/scripts/hooks-system/infrastructure/ast/text/text-scanner.js +1120 -0
- package/scripts/hooks-system/infrastructure/cache/CacheService.js +160 -0
- package/scripts/hooks-system/infrastructure/cli/__tests__/install-wizard.spec.js +16 -0
- package/scripts/hooks-system/infrastructure/cli/install-wizard.js +74 -0
- package/scripts/hooks-system/infrastructure/core/GitOperations.js +50 -0
- package/scripts/hooks-system/infrastructure/core/GitOperations.ts +112 -0
- package/scripts/hooks-system/infrastructure/core/__tests__/GitOperations.spec.js +146 -0
- package/scripts/hooks-system/infrastructure/eslint/eslint-integration.sh +75 -0
- package/scripts/hooks-system/infrastructure/events/EventListeners.js +143 -0
- package/scripts/hooks-system/infrastructure/events/__tests__/events.spec.js +14 -0
- package/scripts/hooks-system/infrastructure/external-tools/GitOperations.js +54 -0
- package/scripts/hooks-system/infrastructure/external-tools/eslint/backend.config.template.mjs +58 -0
- package/scripts/hooks-system/infrastructure/git-hooks/pre-push +35 -0
- package/scripts/hooks-system/infrastructure/git-server/pre-receive-hook +253 -0
- package/scripts/hooks-system/infrastructure/guards/git-wrapper.sh +32 -0
- package/scripts/hooks-system/infrastructure/guards/master-validator.sh +247 -0
- package/scripts/hooks-system/infrastructure/guards/prevent-no-verify.sh +34 -0
- package/scripts/hooks-system/infrastructure/hooks/__tests__/skill-activation-prompt.spec.js +11 -0
- package/scripts/hooks-system/infrastructure/hooks/pre-tool-use-intelligent-enforcer.sh +489 -0
- package/scripts/hooks-system/infrastructure/hooks/skill-activation-prompt.js +244 -0
- package/scripts/hooks-system/infrastructure/logging/UnifiedLoggerFactory.js +40 -0
- package/scripts/hooks-system/infrastructure/logging/__tests__/logging.spec.js +9 -0
- package/scripts/hooks-system/infrastructure/mcp/README.md +116 -0
- package/scripts/hooks-system/infrastructure/mcp/__tests__/ast-intelligence-automation.spec.js +38 -0
- package/scripts/hooks-system/infrastructure/mcp/__tests__/evidence-watcher.spec.js +38 -0
- package/scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js +1097 -0
- package/scripts/hooks-system/infrastructure/mcp/evidence-watcher.js +128 -0
- package/scripts/hooks-system/infrastructure/mcp/package.json +17 -0
- package/scripts/hooks-system/infrastructure/mcp/services/EvidenceService.js +87 -0
- package/scripts/hooks-system/infrastructure/mcp/services/McpProtocolHandler.js +166 -0
- package/scripts/hooks-system/infrastructure/orchestration/__tests__/intelligent-audit.spec.js +11 -0
- package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +353 -0
- package/scripts/hooks-system/infrastructure/patterns/pattern-checks.sh +98 -0
- package/scripts/hooks-system/infrastructure/reporting/ReportImpactAnalyzer.js +109 -0
- package/scripts/hooks-system/infrastructure/reporting/ReportMetricsCalculator.js +114 -0
- package/scripts/hooks-system/infrastructure/reporting/ReportPresenter.js +86 -0
- package/scripts/hooks-system/infrastructure/reporting/__tests__/reporting.spec.js +15 -0
- package/scripts/hooks-system/infrastructure/reporting/report-generator.js +130 -0
- package/scripts/hooks-system/infrastructure/reporting/severity-tracker.js +105 -0
- package/scripts/hooks-system/infrastructure/repositories/CursorTokenRepository.js +76 -0
- package/scripts/hooks-system/infrastructure/repositories/FileFindingsRepository.js +88 -0
- package/scripts/hooks-system/infrastructure/repositories/__tests__/repositories.spec.js +20 -0
- package/scripts/hooks-system/infrastructure/repositories/datasources/CursorApiDataSource.js +73 -0
- package/scripts/hooks-system/infrastructure/repositories/datasources/CursorFileDataSource.js +55 -0
- package/scripts/hooks-system/infrastructure/severity/__tests__/severity-evaluator.spec.js +18 -0
- package/scripts/hooks-system/infrastructure/severity/analyzers/__tests__/maintainability-analyzer.spec.js +170 -0
- package/scripts/hooks-system/infrastructure/severity/analyzers/__tests__/performance-analyzer.spec.js +186 -0
- package/scripts/hooks-system/infrastructure/severity/analyzers/__tests__/security-analyzer.spec.js +151 -0
- package/scripts/hooks-system/infrastructure/severity/analyzers/__tests__/stability-analyzer.spec.js +143 -0
- package/scripts/hooks-system/infrastructure/severity/analyzers/maintainability-analyzer.js +100 -0
- package/scripts/hooks-system/infrastructure/severity/analyzers/performance-analyzer.js +109 -0
- package/scripts/hooks-system/infrastructure/severity/analyzers/security-analyzer.js +104 -0
- package/scripts/hooks-system/infrastructure/severity/analyzers/stability-analyzer.js +85 -0
- package/scripts/hooks-system/infrastructure/severity/context/analyzers/CodeClassificationAnalyzer.js +71 -0
- package/scripts/hooks-system/infrastructure/severity/context/analyzers/DataAnalyzer.js +64 -0
- package/scripts/hooks-system/infrastructure/severity/context/analyzers/ImpactAnalyzer.js +68 -0
- package/scripts/hooks-system/infrastructure/severity/context/analyzers/SafetyAnalyzer.js +82 -0
- package/scripts/hooks-system/infrastructure/severity/context/context-builder.js +88 -0
- package/scripts/hooks-system/infrastructure/severity/generators/RecommendationGenerator.js +153 -0
- package/scripts/hooks-system/infrastructure/severity/mappers/SeverityMapper.js +10 -0
- package/scripts/hooks-system/infrastructure/severity/policies/gate-policies.js +136 -0
- package/scripts/hooks-system/infrastructure/severity/policies/severity-policies.json +206 -0
- package/scripts/hooks-system/infrastructure/severity/scorers/ContextMultiplier.js +49 -0
- package/scripts/hooks-system/infrastructure/severity/severity-evaluator.js +117 -0
- package/scripts/hooks-system/infrastructure/shell/core/constants.sh +26 -0
- package/scripts/hooks-system/infrastructure/shell/core/utils.sh +45 -0
- package/scripts/hooks-system/infrastructure/shell/gitflow/git-wrapper.sh +646 -0
- package/scripts/hooks-system/infrastructure/shell/gitflow/gitflow-enforcer.sh +620 -0
- package/scripts/hooks-system/infrastructure/shell/gitflow/gitflow-state-manager.sh +235 -0
- package/scripts/hooks-system/infrastructure/shell/gitflow-state-manager.sh +225 -0
- package/scripts/hooks-system/infrastructure/shell/orchestrators/audit-orchestrator.sh +1106 -0
- package/scripts/hooks-system/infrastructure/shell/security/detect-secrets.sh +26 -0
- package/scripts/hooks-system/infrastructure/shell/security/detect_secrets.py +182 -0
- package/scripts/hooks-system/infrastructure/shell/validate-clean-architecture.sh +254 -0
- package/scripts/hooks-system/infrastructure/shell/validators/check-doc-structure.sh +62 -0
- package/scripts/hooks-system/infrastructure/shell/validators/ensure-critical-docs.sh +26 -0
- package/scripts/hooks-system/infrastructure/shell/validators/validate-ai-protocol.sh +474 -0
- package/scripts/hooks-system/infrastructure/shell/validators/validate-clean-architecture.sh +303 -0
- package/scripts/hooks-system/infrastructure/shell/validators/validate-conventional-commit.sh +42 -0
- package/scripts/hooks-system/infrastructure/storage/file-operations.sh +31 -0
- package/scripts/hooks-system/infrastructure/telemetry/TelemetryService.js +165 -0
- package/scripts/hooks-system/infrastructure/telemetry/__tests__/telemetry.spec.js +15 -0
- package/scripts/hooks-system/infrastructure/telemetry/metrics-logger.js +66 -0
- package/scripts/hooks-system/infrastructure/telemetry/metrics-server.js +61 -0
- package/scripts/hooks-system/infrastructure/utils/__tests__/utils.spec.js +8 -0
- package/scripts/hooks-system/infrastructure/utils/error-utils.js +28 -0
- package/scripts/hooks-system/infrastructure/utils/timestamp-helper.sh +106 -0
- package/scripts/hooks-system/infrastructure/utils/token-manager.js +121 -0
- package/scripts/hooks-system/infrastructure/validators/__tests__/detect-commit-language.spec.js +16 -0
- package/scripts/hooks-system/infrastructure/validators/__tests__/enforce-english-literals.spec.js +67 -0
- package/scripts/hooks-system/infrastructure/validators/detect-commit-language.js +145 -0
- package/scripts/hooks-system/infrastructure/validators/enforce-english-literals.js +202 -0
- package/scripts/hooks-system/infrastructure/watchdog/__tests__/.audit-reports/token-monitor.log +18 -0
- package/scripts/hooks-system/infrastructure/watchdog/__tests__/auto-recovery.spec.js +14 -0
- package/scripts/hooks-system/infrastructure/watchdog/__tests__/token-monitor.spec.js +67 -0
- package/scripts/hooks-system/infrastructure/watchdog/__tests__/watchdog.spec.js +22 -0
- package/scripts/hooks-system/infrastructure/watchdog/ai-watchdog.sh +278 -0
- package/scripts/hooks-system/infrastructure/watchdog/auto-recovery.js +32 -0
- package/scripts/hooks-system/infrastructure/watchdog/health-check.js +58 -0
- package/scripts/hooks-system/infrastructure/watchdog/token-monitor-loop.sh +20 -0
- package/scripts/hooks-system/infrastructure/watchdog/token-monitor.js +69 -0
- package/scripts/hooks-system/infrastructure/watchdog/token-tracker.sh +208 -0
- package/scripts/hooks-system/presentation/cli/audit.sh +32 -0
- package/scripts/hooks-system/presentation/cli/autonomous-status.sh +92 -0
- package/scripts/hooks-system/presentation/cli/categorize-violations.sh +179 -0
- package/scripts/hooks-system/presentation/cli/direct-audit-option2.sh +23 -0
- package/scripts/hooks-system/presentation/cli/direct-audit.sh +33 -0
- package/skills/android-guidelines/SKILL.md +475 -0
- package/skills/android-guidelines/resources/advanced-topics.md +44 -0
- package/skills/android-guidelines/resources/architecture-overview.md +44 -0
- package/skills/backend-guidelines/SKILL.md +335 -0
- package/skills/backend-guidelines/resources/architecture-overview.md +48 -0
- package/skills/frontend-guidelines/SKILL.md +367 -0
- package/skills/frontend-guidelines/resources/architecture-overview.md +44 -0
- package/skills/ios-guidelines/SKILL.md +406 -0
- package/skills/ios-guidelines/resources/architecture-overview.md +47 -0
- package/skills/skill-rules.json +334 -0
|
@@ -0,0 +1,2094 @@
|
|
|
1
|
+
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { pushFinding, mapToLevel, SyntaxKind, isTestFile, platformOf, getRepoRoot } = require(path.join(__dirname, '../ast-core'));
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { FrontendArchitectureDetector } = require(path.join(__dirname, 'analyzers/FrontendArchitectureDetector'));
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Run Frontend-specific AST intelligence analysis
|
|
9
|
+
* @param {Project} project - TypeScript morph project
|
|
10
|
+
* @param {Array} findings - Findings array to populate
|
|
11
|
+
* @param {string} platform - Platform identifier
|
|
12
|
+
*/
|
|
13
|
+
function runFrontendIntelligence(project, findings, platform) {
|
|
14
|
+
try {
|
|
15
|
+
const root = getRepoRoot();
|
|
16
|
+
const architectureDetector = new FrontendArchitectureDetector(root);
|
|
17
|
+
const detectedPattern = architectureDetector.detect();
|
|
18
|
+
const detectionSummary = architectureDetector.getDetectionSummary();
|
|
19
|
+
|
|
20
|
+
console.log(`[Frontend Architecture] Pattern detected: ${detectedPattern} (confidence: ${detectionSummary.confidence}%)`);
|
|
21
|
+
|
|
22
|
+
if (detectionSummary.warnings.length > 0) {
|
|
23
|
+
detectionSummary.warnings.forEach(warning => {
|
|
24
|
+
pushFinding('frontend.architecture.detection_warning', warning.severity.toLowerCase(), null, null, warning.message + '\n\n' + warning.recommendation, findings);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error('[Frontend Architecture] Error during architecture detection:', error.message);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const godComponentBaseline = (() => {
|
|
32
|
+
const quantile = (values, p) => {
|
|
33
|
+
if (!values || values.length === 0) return 0;
|
|
34
|
+
const sorted = [...values].filter((v) => Number.isFinite(v)).sort((a, b) => a - b);
|
|
35
|
+
if (sorted.length === 0) return 0;
|
|
36
|
+
const idx = Math.max(0, Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1));
|
|
37
|
+
return sorted[idx];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const median = (values) => {
|
|
41
|
+
if (!values || values.length === 0) return 0;
|
|
42
|
+
const sorted = [...values].filter((v) => Number.isFinite(v)).sort((a, b) => a - b);
|
|
43
|
+
if (sorted.length === 0) return 0;
|
|
44
|
+
const mid = Math.floor(sorted.length / 2);
|
|
45
|
+
if (sorted.length % 2 === 0) return (sorted[mid - 1] + sorted[mid]) / 2;
|
|
46
|
+
return sorted[mid];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const mad = (values) => {
|
|
50
|
+
const med = median(values);
|
|
51
|
+
const deviations = (values || []).map((v) => Math.abs(v - med));
|
|
52
|
+
return median(deviations);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const robustZ = (x, med, madValue) => {
|
|
56
|
+
if (!Number.isFinite(x) || !Number.isFinite(med) || !Number.isFinite(madValue) || madValue === 0) return 0;
|
|
57
|
+
return 0.6745 * (x - med) / madValue;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const pOutlier = Number(process.env.AST_GODCLASS_P_OUTLIER || 90);
|
|
61
|
+
const pExtreme = Number(process.env.AST_GODCLASS_P_EXTREME || 97);
|
|
62
|
+
|
|
63
|
+
const componentPattern = /^(export\s+)?(?:const|function)\s+([A-Z]\w+)\s*[=:].*(?:React\.FC|JSX\.Element|\(\)\s*=>|function)/gm;
|
|
64
|
+
const metrics = [];
|
|
65
|
+
|
|
66
|
+
project.getSourceFiles().forEach((sf) => {
|
|
67
|
+
if (!sf || typeof sf.getFilePath !== 'function') return;
|
|
68
|
+
const filePath = sf.getFilePath();
|
|
69
|
+
if (platformOf(filePath) !== 'frontend') return;
|
|
70
|
+
if (/\/ast-[^/]+\.js$/.test(filePath)) return;
|
|
71
|
+
if (isTestFile(filePath)) return;
|
|
72
|
+
|
|
73
|
+
const content = sf.getFullText();
|
|
74
|
+
const components = Array.from(content.matchAll(componentPattern));
|
|
75
|
+
if (components.length === 0) return;
|
|
76
|
+
|
|
77
|
+
components.forEach((compMatch) => {
|
|
78
|
+
const componentStart = compMatch.index || 0;
|
|
79
|
+
const componentEnd = content.indexOf('\n}\n', componentStart) || content.length;
|
|
80
|
+
const componentBody = content.substring(componentStart, componentEnd);
|
|
81
|
+
const hookCount = (componentBody.match(/\buse[A-Z]\w+\(/g) || []).length;
|
|
82
|
+
const functionCount = (componentBody.match(/(?:const|let)\s+\w+\s*=\s*(?:async\s*)?\(/g) || []).length;
|
|
83
|
+
const totalComplexity = hookCount + functionCount;
|
|
84
|
+
const bodyLines = componentBody.split('\n').length;
|
|
85
|
+
metrics.push({ totalComplexity, bodyLines });
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (metrics.length === 0) return null;
|
|
90
|
+
|
|
91
|
+
const complexities = metrics.map(m => m.totalComplexity);
|
|
92
|
+
const sizes = metrics.map(m => m.bodyLines);
|
|
93
|
+
|
|
94
|
+
const med = {
|
|
95
|
+
totalComplexity: median(complexities),
|
|
96
|
+
bodyLines: median(sizes)
|
|
97
|
+
};
|
|
98
|
+
const madValue = {
|
|
99
|
+
totalComplexity: mad(complexities),
|
|
100
|
+
bodyLines: mad(sizes)
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const z = {
|
|
104
|
+
totalComplexity: complexities.map(v => robustZ(v, med.totalComplexity, madValue.totalComplexity)),
|
|
105
|
+
bodyLines: sizes.map(v => robustZ(v, med.bodyLines, madValue.bodyLines))
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
thresholds: {
|
|
110
|
+
outlier: {
|
|
111
|
+
totalComplexityZ: quantile(z.totalComplexity, pOutlier),
|
|
112
|
+
bodyLinesZ: quantile(z.bodyLines, pOutlier)
|
|
113
|
+
},
|
|
114
|
+
extreme: {
|
|
115
|
+
totalComplexityZ: quantile(z.totalComplexity, pExtreme),
|
|
116
|
+
bodyLinesZ: quantile(z.bodyLines, pExtreme)
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
med,
|
|
120
|
+
mad: madValue,
|
|
121
|
+
robustZ
|
|
122
|
+
};
|
|
123
|
+
})();
|
|
124
|
+
|
|
125
|
+
project.getSourceFiles().forEach((sf) => {
|
|
126
|
+
if (!sf || typeof sf.getFilePath !== 'function') return;
|
|
127
|
+
const filePath = sf.getFilePath();
|
|
128
|
+
const isInfrastructure = /\/infrastructure\/|\/lib\/api\/|\/services\//.test(filePath);
|
|
129
|
+
const isComponent = /\/(components|app|presentation)\//.test(filePath) && !isInfrastructure;
|
|
130
|
+
|
|
131
|
+
if (platformOf(filePath) !== "frontend") return;
|
|
132
|
+
|
|
133
|
+
if (/\/ast-[^/]+\.js$/.test(filePath)) return;
|
|
134
|
+
if (/\/app\/middleware\.ts$|\/middleware\.ts$|\/app\/headers\.ts$/.test(filePath)) {
|
|
135
|
+
if (!/Content\-Security\-Policy/i.test(sf.getFullText())) {
|
|
136
|
+
pushFinding("frontend.security.missing_csp", "high", sf, sf, "CSP header not set in middleware/headers", findings);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const hookCalls = sf.getDescendantsOfKind(SyntaxKind.CallExpression).filter((call) => {
|
|
141
|
+
const expr = call.getExpression().getText();
|
|
142
|
+
return /^(use[A-Z]|useState|useEffect|useCallback|useMemo|useContext|useReducer|useRef)\b/.test(expr);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
hookCalls.forEach((hookCall) => {
|
|
146
|
+
let parent = hookCall.getParent();
|
|
147
|
+
let depth = 0;
|
|
148
|
+
const maxDepth = 10; // Limit traversal to avoid false positives in deeply nested code
|
|
149
|
+
|
|
150
|
+
while (parent && depth < maxDepth) {
|
|
151
|
+
const kind = parent.getKind();
|
|
152
|
+
|
|
153
|
+
if (kind === SyntaxKind.FunctionDeclaration ||
|
|
154
|
+
kind === SyntaxKind.FunctionExpression ||
|
|
155
|
+
kind === SyntaxKind.ArrowFunction) {
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (kind === SyntaxKind.IfStatement || kind === SyntaxKind.ConditionalExpression) {
|
|
160
|
+
pushFinding("frontend.hooks.conditional", "error", sf, hookCall, "Hook called conditionally", findings);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
parent = parent.getParent();
|
|
165
|
+
depth++;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
sf.getDescendantsOfKind(SyntaxKind.FunctionDeclaration).forEach((fn) => {
|
|
170
|
+
const name = fn.getName();
|
|
171
|
+
if (name && /^[A-Z]/.test(name)) {
|
|
172
|
+
const params = fn.getParameters();
|
|
173
|
+
if (params.length > 0 && params[0].getTypeNode() === undefined) {
|
|
174
|
+
pushFinding("frontend.props.missing_types", "warning", sf, fn, `Component ${name} missing prop types`, findings);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
sf.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((call) => {
|
|
180
|
+
const expr = call.getExpression().getText();
|
|
181
|
+
if (/useQuery|useMutation/.test(expr)) {
|
|
182
|
+
const parent = call.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
|
|
183
|
+
if (parent) {
|
|
184
|
+
const text = parent.getText();
|
|
185
|
+
if (!/error|Error/.test(text)) {
|
|
186
|
+
pushFinding("frontend.react_query.missing_error", "warning", sf, call, "React Query hook without error handling", findings);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
sf.getDescendantsOfKind(SyntaxKind.FunctionDeclaration).forEach((fn) => {
|
|
193
|
+
const name = fn.getName();
|
|
194
|
+
if (name && /^[A-Z]/.test(name)) {
|
|
195
|
+
const params = fn.getParameters();
|
|
196
|
+
if (params.length > 0) {
|
|
197
|
+
const firstParam = params[0];
|
|
198
|
+
const typeNode = firstParam.getTypeNode();
|
|
199
|
+
if (typeNode) {
|
|
200
|
+
const typeText = typeNode.getText();
|
|
201
|
+
const propMatches = typeText.match(/[:;]/g);
|
|
202
|
+
if (propMatches && propMatches.length > 7) {
|
|
203
|
+
pushFinding("frontend.component.too_many_props", "warning", sf, fn, `Component ${name} has ${propMatches.length} props`, findings);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const isDomUtilityFile = /\/(scripts|utils|helpers|lib|core)\//i.test(filePath);
|
|
211
|
+
const isCustomHook = /\/hooks\//i.test(filePath) || /^use[A-Z]/.test(filePath.split('/').pop() || '');
|
|
212
|
+
const isChartMapComponent = /\/(charts|maps|visualization)\//i.test(filePath) || /(Map|Chart|Graph|Plot)\.tsx?$/.test(filePath);
|
|
213
|
+
const isDomTestFile = /\.(spec|test)\.(ts|tsx|js|jsx)$/.test(filePath) || /\/(tests?|__tests__)\//i.test(filePath);
|
|
214
|
+
const isDomConfigFile = /(config|setup|polyfill|main\.tsx?)$/i.test(filePath) || /\/config\//i.test(filePath);
|
|
215
|
+
const isE2ETest = /\/(e2e|playwright|cypress)\//i.test(filePath);
|
|
216
|
+
const isThirdPartyIntegration = /(google|leaflet|mapbox|d3|chart)/i.test(sf.getFullText().substring(0, 500));
|
|
217
|
+
const isLegitimateContext = isDomUtilityFile || isCustomHook || isChartMapComponent || isDomTestFile || isDomConfigFile || isE2ETest || isThirdPartyIntegration;
|
|
218
|
+
|
|
219
|
+
if (!isLegitimateContext) {
|
|
220
|
+
sf.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((call) => {
|
|
221
|
+
const expr = call.getExpression().getText();
|
|
222
|
+
if (/^(document\.|window\.(scroll|location|URL)|getElementById|getElementsBy|querySelector)/.test(expr)) {
|
|
223
|
+
const fullCallText = call.getText();
|
|
224
|
+
const functionName = call.getFirstAncestorByKind(SyntaxKind.FunctionDeclaration)?.getName() || '';
|
|
225
|
+
const isUtilityFunction = /^(setup|init|configure|attach|mount|render)/i.test(functionName);
|
|
226
|
+
|
|
227
|
+
const isReactEntry = /createRoot\s*\(\s*document\.getElementById/.test(fullCallText); // React 18 entry
|
|
228
|
+
const isLangAttribute = /document\.documentElement\.(setAttribute|lang)/.test(fullCallText); // i18n
|
|
229
|
+
const isHeadManipulation = /document\.(head|body)\.append/.test(fullCallText); // Script injection (Maps, etc)
|
|
230
|
+
const isMetaTag = /document\.querySelector\(['"]meta\[name=/.test(fullCallText); // Meta tags (SEO, theme)
|
|
231
|
+
const isFileDownload = /document\.createElement\(['"]a['"]\)/.test(fullCallText) && /\.download\s*=/.test(sf.getFullText().substring(call.getStart() - 100, call.getEnd() + 100)); // File export
|
|
232
|
+
const isBlobURL = /URL\.createObjectURL|URL\.revokeObjectURL/.test(fullCallText); // Blob downloads
|
|
233
|
+
const isWindowReload = /window\.location\.reload/.test(fullCallText); // Page reload (legitimate retry)
|
|
234
|
+
const isScrollRestoration = /scrollY|scrollTop|scrollPosition|scrollTo/.test(sf.getFullText().substring(call.getStart() - 200, call.getEnd() + 50)) && /sessionStorage|localStorage/.test(sf.getFullText().substring(call.getStart() - 200, call.getEnd() + 50)); // Scroll save/restore
|
|
235
|
+
const isModalPattern = /Modal|Dialog|Drawer/.test(filePath) && (/body\.style\.overflow|addEventListener\(['"]keydown/.test(sf.getFullText().substring(call.getStart() - 100, call.getEnd() + 100))); // Modal escape key + body scroll lock
|
|
236
|
+
|
|
237
|
+
if (!isUtilityFunction && !isReactEntry && !isLangAttribute && !isHeadManipulation && !isMetaTag && !isFileDownload && !isBlobURL && !isWindowReload && !isScrollRestoration && !isModalPattern) {
|
|
238
|
+
pushFinding("frontend.dom.direct", "error", sf, call, "Direct DOM manipulation detected", findings);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
sf.getDescendantsOfKind(SyntaxKind.JsxElement).forEach((jsx) => {
|
|
245
|
+
const tag = jsx.getOpeningElement()?.getTagNameNode()?.getText();
|
|
246
|
+
if (tag && /^[A-Z]/.test(tag)) {
|
|
247
|
+
const parent = jsx.getParent();
|
|
248
|
+
if (parent && parent.getKind && parent.getKind() === SyntaxKind.ArrayLiteralExpression) {
|
|
249
|
+
const attrs = jsx.getOpeningElement()?.getAttributes();
|
|
250
|
+
const hasKey = attrs?.some((a) => {
|
|
251
|
+
const name = a.getNameNode?.();
|
|
252
|
+
return name && name.getText() === "key";
|
|
253
|
+
});
|
|
254
|
+
if (!hasKey) {
|
|
255
|
+
pushFinding("frontend.list.missing_key", "error", sf, jsx, "Missing key prop in list item", findings);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
sf.getDescendantsOfKind(SyntaxKind.FunctionDeclaration).forEach((fn) => {
|
|
262
|
+
const name = fn.getName();
|
|
263
|
+
if (!name || !/^[A-Z]/.test(name)) return;
|
|
264
|
+
const params = fn.getParameters();
|
|
265
|
+
if (params.length === 0) return;
|
|
266
|
+
const p = params[0];
|
|
267
|
+
const typeNode = p.getTypeNode();
|
|
268
|
+
const paramName = p.getName();
|
|
269
|
+
const body = fn.getBody();
|
|
270
|
+
if (!body) return;
|
|
271
|
+
const propNames = new Set();
|
|
272
|
+
if (typeNode && typeNode.getKind && typeNode.getKind() !== undefined) {
|
|
273
|
+
const typeText = typeNode.getText();
|
|
274
|
+
(typeText.match(/\b(\w+)\s*:/g) || []).forEach((m) => {
|
|
275
|
+
const k = m.replace(/[:\s]/g, "");
|
|
276
|
+
if (k) propNames.add(k);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
let forwards = 0;
|
|
280
|
+
body.getDescendantsOfKind(SyntaxKind.JsxAttribute).forEach((attr) => {
|
|
281
|
+
const n = attr.getNameNode();
|
|
282
|
+
const init = attr.getInitializer();
|
|
283
|
+
if (!n || !init) return;
|
|
284
|
+
const attrName = n.getText();
|
|
285
|
+
const initText = init.getText().replace(/[{}\s]/g, "");
|
|
286
|
+
if (propNames.has(attrName) && initText === attrName) {
|
|
287
|
+
forwards += 1;
|
|
288
|
+
}
|
|
289
|
+
if (attr.getKind && attr.getKind() === SyntaxKind.JsxSpreadAttribute) {
|
|
290
|
+
const t = attr.getText();
|
|
291
|
+
if (t.includes(paramName)) forwards += 1;
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
if (forwards >= 6) {
|
|
295
|
+
pushFinding("frontend.props.prop_drilling", "warning", sf, fn, `Possible prop drilling detected in ${name} (forwards=${forwards})`, findings);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
sf.getDescendantsOfKind(SyntaxKind.FunctionDeclaration).forEach((fn) => {
|
|
300
|
+
const name = fn.getName();
|
|
301
|
+
if (!name || !/^[A-Z]/.test(name)) return;
|
|
302
|
+
const body = fn.getBody();
|
|
303
|
+
if (!body) return;
|
|
304
|
+
const t = body.getText();
|
|
305
|
+
const branches = (t.match(/\b(if|for|while|case |catch|\?:|&&|\|\|)\b/g) || []).length;
|
|
306
|
+
const complexity = 1 + branches;
|
|
307
|
+
if (complexity > 20) {
|
|
308
|
+
pushFinding("frontend.component.cyclomatic_complexity", "warning", sf, fn, `High cyclomatic complexity in ${name} (=${complexity})`, findings);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
sf.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
|
|
313
|
+
const name = cls.getName();
|
|
314
|
+
if (name && /^[A-Z]/.test(name)) {
|
|
315
|
+
const heritage = cls.getHeritageClauses();
|
|
316
|
+
const isReactComponent = heritage.some((h) =>
|
|
317
|
+
h.getText().includes("extends") &&
|
|
318
|
+
h.getTypeNodes().some((t) => /Component|PureComponent/.test(t.getText()))
|
|
319
|
+
);
|
|
320
|
+
const hasRender = cls.getMembers().some((m) =>
|
|
321
|
+
m.getKind() === SyntaxKind.MethodDeclaration &&
|
|
322
|
+
m.getName() === "render"
|
|
323
|
+
);
|
|
324
|
+
if (isReactComponent || hasRender) {
|
|
325
|
+
pushFinding("frontend.react.class_components", "error", sf, cls, `Class component ${name} detected (use functional components only)`, findings);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
sf.getDescendantsOfKind(SyntaxKind.JsxElement).forEach((jsx) => {
|
|
331
|
+
const tag = jsx.getOpeningElement()?.getTagNameNode()?.getText();
|
|
332
|
+
if (tag && /^[A-Z]/.test(tag)) {
|
|
333
|
+
const parent = jsx.getParent();
|
|
334
|
+
if (parent && parent.getKind && parent.getKind() === SyntaxKind.ArrayLiteralExpression) {
|
|
335
|
+
const attrs = jsx.getOpeningElement()?.getAttributes();
|
|
336
|
+
const keyAttr = attrs?.find((a) => a.getNameNode && a.getNameNode()?.getText() === "key");
|
|
337
|
+
if (keyAttr) {
|
|
338
|
+
const init = keyAttr.getInitializer();
|
|
339
|
+
const keyText = init?.getText();
|
|
340
|
+
if (keyText && /\bindex\b|\bi\b/.test(keyText)) {
|
|
341
|
+
pushFinding("frontend.react.index_as_key", "warning", sf, jsx, "Using index as key in list (use stable unique identifier)", findings);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
sf.getDescendantsOfKind(SyntaxKind.FunctionDeclaration).forEach((fn) => {
|
|
349
|
+
const name = fn.getName();
|
|
350
|
+
if (!name || !/^[A-Z]/.test(name)) return;
|
|
351
|
+
const start = fn.getStart();
|
|
352
|
+
const end = fn.getEnd();
|
|
353
|
+
const lineCount = sf.getLineAndColumnAtPos(end).line - sf.getLineAndColumnAtPos(start).line + 1;
|
|
354
|
+
const body = fn.getBody();
|
|
355
|
+
if (!body) return;
|
|
356
|
+
const jsxElements = body.getDescendantsOfKind(SyntaxKind.JsxElement);
|
|
357
|
+
const jsxSelfClosing = body.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement);
|
|
358
|
+
const totalJsx = jsxElements.length + jsxSelfClosing.length;
|
|
359
|
+
if (lineCount > 50 && totalJsx > 10) {
|
|
360
|
+
pushFinding("frontend.react.missing_composition", "high", sf, fn, `Large component ${name} (${lineCount} lines, ${totalJsx} JSX elements) violates SRP (Single Responsibility Principle)`, findings);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
sf.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach((el) => {
|
|
365
|
+
const tag = el.getTagNameNode()?.getText();
|
|
366
|
+
if (tag === "img") {
|
|
367
|
+
const attrs = el.getAttributes();
|
|
368
|
+
const hasAlt = attrs.some((a) => a.getNameNode && a.getNameNode()?.getText() === "alt");
|
|
369
|
+
if (!hasAlt) {
|
|
370
|
+
pushFinding("frontend.a11y.img_missing_alt", "error", sf, el, "<img> without alt attribute", findings);
|
|
371
|
+
}
|
|
372
|
+
const hasLazy = attrs.some((a) => a.getNameNode && a.getNameNode()?.getText() === "loading" && /lazy/.test(a.getInitializer()?.getText() || ""));
|
|
373
|
+
if (!hasLazy) {
|
|
374
|
+
pushFinding("frontend.performance.img_missing_lazy", "warning", sf, el, "<img> without loading=\"lazy\"", findings);
|
|
375
|
+
}
|
|
376
|
+
pushFinding("frontend.next.image_not_used", "warning", sf, el, "<img> tag used - prefer next/image", findings);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
sf.getDescendantsOfKind(SyntaxKind.JsxElement).forEach((el) => {
|
|
380
|
+
const tag = el.getOpeningElement()?.getTagNameNode()?.getText();
|
|
381
|
+
if (tag === "img") {
|
|
382
|
+
const attrs = el.getOpeningElement()?.getAttributes() || [];
|
|
383
|
+
const hasAlt = attrs.some((a) => a.getNameNode && a.getNameNode()?.getText() === "alt");
|
|
384
|
+
if (!hasAlt) {
|
|
385
|
+
pushFinding("frontend.a11y.img_missing_alt", "error", sf, el, "<img> without alt attribute", findings);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const fileContent = sf.getFullText();
|
|
391
|
+
const isChartFile = /(recharts|chart\.js|d3|victory|nivo|visx)/i.test(fileContent.substring(0, 500));
|
|
392
|
+
|
|
393
|
+
sf.getDescendantsOfKind(SyntaxKind.JsxOpeningElement).forEach((open) => {
|
|
394
|
+
const tag = open.getTagNameNode()?.getText();
|
|
395
|
+
const attrs = open.getAttributes();
|
|
396
|
+
const hasOnClick = attrs.some((a) => a.getNameNode && a.getNameNode()?.getText() === "onClick");
|
|
397
|
+
|
|
398
|
+
const isSemanticNative = tag === "button" || tag === "a" || tag === "input" || tag === "select" || tag === "textarea";
|
|
399
|
+
const isSemanticComponent = /^(Button|Link|IconButton|MenuItem|Tab|Chip|Card|Badge)$/i.test(tag || '');
|
|
400
|
+
const isThirdPartyLibComponent = /^(Recharts|Chart|Leaflet|Map|Dialog|Popover|Dropdown|Select|Combobox)/i.test(tag || '');
|
|
401
|
+
const isChartComponent = isChartFile && /^(Pie|Bar|Line|Area|Scatter|Radar|Legend|Tooltip|Cell)$/i.test(tag || '');
|
|
402
|
+
|
|
403
|
+
if (hasOnClick && !isSemanticNative && !isSemanticComponent && !isThirdPartyLibComponent && !isChartComponent) {
|
|
404
|
+
const hasRole = attrs.some((a) => a.getNameNode && a.getNameNode()?.getText() === "role");
|
|
405
|
+
const hasAria = attrs.some((a) => a.getNameNode && /^aria-/.test(a.getNameNode()?.getText() || ""));
|
|
406
|
+
const hasLabel = attrs.some((a) => a.getNameNode && a.getNameNode()?.getText() === "aria-label");
|
|
407
|
+
if (!hasRole && !(hasAria || hasLabel)) {
|
|
408
|
+
pushFinding("frontend.a11y.interactive_missing_aria", "error", sf, open, "Interactive element without role/aria-label", findings);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
sf.getDescendantsOfKind(SyntaxKind.JsxAttribute).forEach((attr) => {
|
|
413
|
+
const name = attr.getNameNode()?.getText();
|
|
414
|
+
if (name !== "tabIndex") return;
|
|
415
|
+
const val = attr.getInitializer()?.getText() || "";
|
|
416
|
+
if (/\b[1-9][0-9]*\b/.test(val)) {
|
|
417
|
+
pushFinding("frontend.a11y.tabindex_positive", "warning", sf, attr, "Positive tabIndex detected", findings);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const imports = sf.getImportDeclarations();
|
|
422
|
+
const hasNextImage = imports.some((imp) => imp.getModuleSpecifierValue() === "next/image");
|
|
423
|
+
const usesLink = imports.some((imp) => imp.getModuleSpecifierValue() === "next/link");
|
|
424
|
+
|
|
425
|
+
sf.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach((el) => {
|
|
426
|
+
const tag = el.getTagNameNode()?.getText();
|
|
427
|
+
if (tag === "Image" && hasNextImage) {
|
|
428
|
+
const attrs = el.getAttributes();
|
|
429
|
+
const hasAlt = attrs.some((a) => a.getNameNode && a.getNameNode()?.getText() === "alt");
|
|
430
|
+
const hasFill = attrs.some((a) => a.getNameNode && a.getNameNode()?.getText() === "fill");
|
|
431
|
+
const hasWidth = attrs.some((a) => a.getNameNode && a.getNameNode()?.getText() === "width");
|
|
432
|
+
const hasHeight = attrs.some((a) => a.getNameNode && a.getNameNode()?.getText() === "height");
|
|
433
|
+
if (!hasAlt) {
|
|
434
|
+
pushFinding("frontend.next.image_missing_alt", "error", sf, el, "next/image without alt", findings);
|
|
435
|
+
}
|
|
436
|
+
if (!(hasFill || (hasWidth && hasHeight))) {
|
|
437
|
+
pushFinding("frontend.next.image_missing_size", "warning", sf, el, "next/image without width/height or fill", findings);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
sf.getDescendantsOfKind(SyntaxKind.JsxOpeningElement).forEach((open) => {
|
|
443
|
+
const tag = open.getTagNameNode()?.getText();
|
|
444
|
+
if (tag === "a") {
|
|
445
|
+
const attrs = open.getAttributes();
|
|
446
|
+
const hrefAttr = attrs.find((a) => a.getNameNode && a.getNameNode()?.getText() === "href");
|
|
447
|
+
const hrefText = hrefAttr?.getInitializer()?.getText()?.replace(/['"`]/g, "");
|
|
448
|
+
if (hrefText && hrefText.startsWith("/") && !usesLink) {
|
|
449
|
+
pushFinding("frontend.next.link_mandatory", "warning", sf, open, "Internal link should use next/link", findings);
|
|
450
|
+
}
|
|
451
|
+
if (hrefText && /^(http:|https:)/.test(hrefText) && usesLink) {
|
|
452
|
+
pushFinding("frontend.next.link_external_wrong", "warning", sf, open, "External URL should not use next/link", findings);
|
|
453
|
+
}
|
|
454
|
+
if (!hrefAttr) {
|
|
455
|
+
pushFinding("frontend.a11y.anchor_missing_href", "warning", sf, open, "Anchor without href attribute", findings);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
sf.getDescendantsOfKind(SyntaxKind.JsxAttribute).forEach((attr) => {
|
|
461
|
+
const name = attr.getNameNode()?.getText();
|
|
462
|
+
if (name === "dangerouslySetInnerHTML") {
|
|
463
|
+
const jsxAttributes = attr.getParent();
|
|
464
|
+
const jsxOpening = jsxAttributes?.getParent();
|
|
465
|
+
const tagName = jsxOpening?.getTagNameNode()?.getText() || '';
|
|
466
|
+
const isStyleTag = tagName === 'style';
|
|
467
|
+
const isCSSGeneration = isStyleTag || /__html.*THEMES|__html.*colorConfig|css`|styled\./i.test(sf.getFullText().substring(attr.getStart() - 100, attr.getEnd() + 200));
|
|
468
|
+
|
|
469
|
+
const sanitized = imports.some((imp) => /dompurify|sanitize-html/.test(imp.getModuleSpecifierValue())) || sf.getFullText().includes("sanitize(");
|
|
470
|
+
if (!sanitized && !isCSSGeneration) {
|
|
471
|
+
pushFinding("frontend.security.dangerous_html_unsanitized", "critical", sf, attr, "dangerouslySetInnerHTML without sanitizer", findings);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
sf.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((call) => {
|
|
477
|
+
const expr = call.getExpression().getText();
|
|
478
|
+
if (expr === "eval" || expr === "Function") {
|
|
479
|
+
pushFinding("frontend.security.eval", "critical", sf, call, "eval/new Function usage detected", findings);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
sf.getDescendantsOfKind(SyntaxKind.JsxAttribute).forEach((attr) => {
|
|
484
|
+
const name = attr.getNameNode()?.getText();
|
|
485
|
+
if (name === "key") {
|
|
486
|
+
const initText = attr.getInitializer()?.getText() || "";
|
|
487
|
+
if (/Math\.random\(\)|Date\.now\(\)/.test(initText)) {
|
|
488
|
+
pushFinding("frontend.react.random_key", "warning", sf, attr, "Random key used in list", findings);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
sf.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((mapCall) => {
|
|
494
|
+
const e = mapCall.getExpression().getText();
|
|
495
|
+
if (/\.map$/.test(e) || /\.map\(/.test(e)) {
|
|
496
|
+
const arrow = mapCall.getArguments()[0];
|
|
497
|
+
if (!arrow || !arrow.getBody) return;
|
|
498
|
+
const body = arrow.getBody();
|
|
499
|
+
const jsx = body && (body.getKind && (body.getKind() === SyntaxKind.JsxElement || body.getKind() === SyntaxKind.JsxSelfClosingElement)) ? body : null;
|
|
500
|
+
if (jsx) {
|
|
501
|
+
const open = jsx.getKind() === SyntaxKind.JsxElement ? jsx.getOpeningElement() : jsx;
|
|
502
|
+
const attrs = open.getAttributes ? open.getAttributes() : [];
|
|
503
|
+
const hasKey = attrs && attrs.some((a) => a.getNameNode && a.getNameNode()?.getText() === "key");
|
|
504
|
+
if (!hasKey) {
|
|
505
|
+
pushFinding("frontend.list.map_missing_key", "error", sf, jsx, "JSX returned from map without key", findings);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
if (filePath.includes("/pages/")) {
|
|
512
|
+
pushFinding("frontend.nextjs.pages_directory", "warning", sf, sf, "Using legacy pages/ directory (use app/ directory for Next.js 13+)", findings);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const hasUseClient = sf.getFullText().includes('"use client"') || sf.getFullText().includes("'use client'");
|
|
516
|
+
if (hasUseClient) {
|
|
517
|
+
const hasInteractiveCode = sf.getDescendantsOfKind(SyntaxKind.CallExpression).some((call) => {
|
|
518
|
+
const expr = call.getExpression().getText();
|
|
519
|
+
return /^(useState|useEffect|useCallback|useMemo|useRef|useContext|useReducer)\b/.test(expr);
|
|
520
|
+
});
|
|
521
|
+
const hasEventHandlers = sf.getDescendantsOfKind(SyntaxKind.JsxAttribute).some((attr) => {
|
|
522
|
+
const name = attr.getNameNode()?.getText();
|
|
523
|
+
return name && /^on[A-Z]/.test(name);
|
|
524
|
+
});
|
|
525
|
+
const hasFormElements = sf.getDescendantsOfKind(SyntaxKind.JsxOpeningElement).some((el) => {
|
|
526
|
+
const tag = el.getTagNameNode()?.getText();
|
|
527
|
+
return tag && /^(input|textarea|select|form|button)$/.test(tag);
|
|
528
|
+
});
|
|
529
|
+
if (!hasInteractiveCode && !hasEventHandlers && !hasFormElements) {
|
|
530
|
+
pushFinding("frontend.nextjs.unnecessary_client", "warning", sf, sf, '"use client" directive may be unnecessary (component could be Server Component)', findings);
|
|
531
|
+
}
|
|
532
|
+
sf.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((c) => {
|
|
533
|
+
const expr = c.getExpression().getText();
|
|
534
|
+
if (expr !== "fetch") return;
|
|
535
|
+
const args = c.getArguments();
|
|
536
|
+
const opt = args[1] ? args[1].getText() : "";
|
|
537
|
+
const ok = /cache\s*:\s*"no-store"|next\s*:\s*\{[^}]*revalidate/.test(opt);
|
|
538
|
+
if (!ok) {
|
|
539
|
+
pushFinding("frontend.next.client_fetch_without_cache", "warning", sf, c, "Client fetch without cache/no-store or next.revalidate", findings);
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
if (sf.getFullText().includes("window.location") || sf.getFullText().includes("location.href")) {
|
|
543
|
+
pushFinding("frontend.next.navigation_missing_useRouter", "info", sf, sf, "Navigation via window.location in client component (prefer next/navigation)", findings);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const isNextTestFile = /\.(spec|test)\.(ts|tsx|js|jsx)$/.test(filePath);
|
|
548
|
+
const isServerComponent = !sf.getFullText().includes('"use client"') && !sf.getFullText().includes("'use client'");
|
|
549
|
+
|
|
550
|
+
if (isServerComponent && (filePath.includes("/app/") || filePath.includes("/app\\")) && !isNextTestFile) {
|
|
551
|
+
const hasDomAccess = sf.getDescendantsOfKind(SyntaxKind.Identifier).some((id) => {
|
|
552
|
+
const t = id.getText();
|
|
553
|
+
return t === "window" || t === "document" || t === "localStorage";
|
|
554
|
+
});
|
|
555
|
+
if (hasDomAccess) {
|
|
556
|
+
pushFinding("frontend.next.server_component_dom_access", "error", sf, sf, "Server Component accessing window/document/localStorage", findings);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const usesNextHead = sf.getImportDeclarations().some((imp) => imp.getModuleSpecifierValue() === "next/head");
|
|
561
|
+
if (usesNextHead && (filePath.includes("/app/") || filePath.includes("/app\\"))) {
|
|
562
|
+
pushFinding("frontend.next.head_legacy", "warning", sf, sf, "next/head used in App Router - prefer Metadata API", findings);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (/page\.(t|j)sx?$/.test(filePath) && (filePath.includes("/app/") || filePath.includes("/app\\"))) {
|
|
566
|
+
const hasMetadata = /export\s+const\s+metadata\s*=/.test(sf.getFullText());
|
|
567
|
+
if (!hasMetadata) {
|
|
568
|
+
pushFinding("frontend.next.missing_metadata", "warning", sf, sf, "page.tsx without exported metadata", findings);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const heavyLibs = ["lodash", "moment", "chart.js", "highcharts", "firebase", "@mui/material"];
|
|
573
|
+
const heavyImported = sf.getImportDeclarations().filter((imp) => heavyLibs.some((l) => imp.getModuleSpecifierValue() === l));
|
|
574
|
+
if (heavyImported.length > 0) {
|
|
575
|
+
const hasDynamic = sf.getFullText().includes("next/dynamic") || sf.getDescendantsOfKind(SyntaxKind.CallExpression).some((c) => c.getExpression().getText() === "import");
|
|
576
|
+
if (!hasDynamic) {
|
|
577
|
+
pushFinding("frontend.performance.heavy_no_dynamic", "warning", sf, sf, "Heavy library imported without code splitting", findings);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
sf.getDescendantsOfKind(SyntaxKind.JsxOpeningElement).forEach((open) => {
|
|
582
|
+
const tag = open.getTagNameNode()?.getText();
|
|
583
|
+
if (tag !== "div" && tag !== "span") return;
|
|
584
|
+
const attrs = open.getAttributes();
|
|
585
|
+
const hasOnClick = attrs.some((a) => a.getNameNode && a.getNameNode()?.getText() === "onClick");
|
|
586
|
+
if (!hasOnClick) return;
|
|
587
|
+
const hasRole = attrs.some((a) => a.getNameNode && a.getNameNode()?.getText() === "role");
|
|
588
|
+
const hasTabIndex = attrs.some((a) => a.getNameNode && a.getNameNode()?.getText() === "tabIndex");
|
|
589
|
+
if (!hasRole || !hasTabIndex) {
|
|
590
|
+
pushFinding("frontend.a11y.clickable_without_role", "error", sf, open, "Clickable non-interactive element without role and tabIndex", findings);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
sf.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((call) => {
|
|
595
|
+
const expr = call.getExpression().getText();
|
|
596
|
+
if (expr === "useEffect") {
|
|
597
|
+
const args = call.getArguments();
|
|
598
|
+
if (args.length < 2 || args[1].getKind && args[1].getKind() !== SyntaxKind.ArrayLiteralExpression) {
|
|
599
|
+
pushFinding("frontend.react.useeffect_missing_deps", "warning", sf, call, "useEffect without dependency array", findings);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
sf.getDescendantsOfKind(SyntaxKind.JsxAttribute).forEach((attr) => {
|
|
605
|
+
const name = attr.getNameNode()?.getText();
|
|
606
|
+
if (!name || !/^on[A-Z]/.test(name)) return;
|
|
607
|
+
const init = attr.getInitializer();
|
|
608
|
+
const txt = init ? init.getText() : "";
|
|
609
|
+
if (/=>|function\s*\(/.test(txt)) {
|
|
610
|
+
pushFinding("frontend.react.inline_handler", "info", sf, attr, "Inline function in JSX handler - consider useCallback", findings);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
const usesI18n = sf.getImportDeclarations().some((imp) => /i18n|react-i18next|next-i18next/.test(imp.getModuleSpecifierValue())) || sf.getFullText().includes("useTranslation");
|
|
615
|
+
if (!usesI18n) {
|
|
616
|
+
const jsxTexts = sf.getDescendantsOfKind(SyntaxKind.JsxText).filter((t) => {
|
|
617
|
+
const v = t.getText().trim();
|
|
618
|
+
return v.length > 3 && /[a-zA-Z]/.test(v) && !/^\{|\}/.test(v);
|
|
619
|
+
});
|
|
620
|
+
if (jsxTexts.length > 0) {
|
|
621
|
+
pushFinding("frontend.i18n.hardcoded_jsx_text", "warning", sf, sf, `Hardcoded JSX text detected (${jsxTexts.length})`, findings);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const usesLegacyRouter = sf.getImportDeclarations().some((imp) => imp.getModuleSpecifierValue() === "next/router");
|
|
626
|
+
if (usesLegacyRouter) {
|
|
627
|
+
pushFinding("frontend.next.router_legacy", "warning", sf, sf, "Using next/router legacy API - prefer next/navigation", findings);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
sf.getDescendantsOfKind(SyntaxKind.JsxOpeningElement).forEach((open) => {
|
|
631
|
+
const tag = open.getTagNameNode()?.getText();
|
|
632
|
+
if (tag !== "a") return;
|
|
633
|
+
const attrs = open.getAttributes();
|
|
634
|
+
const hasBlank = attrs.some((a) => a.getNameNode && a.getNameNode()?.getText() === "target" && /_blank/.test(a.getInitializer()?.getText() || ""));
|
|
635
|
+
if (!hasBlank) return;
|
|
636
|
+
const hasRel = attrs.some((a) => a.getNameNode && a.getNameNode()?.getText() === "rel" && /(noopener|noreferrer)/.test(a.getInitializer()?.getText() || ""));
|
|
637
|
+
if (!hasRel) {
|
|
638
|
+
pushFinding("frontend.security.target_blank", "high", sf, open, "target=_blank without rel=\"noopener noreferrer\"", findings);
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
const usesLazy = sf.getDescendantsOfKind(SyntaxKind.CallExpression).some((call) => call.getExpression().getText() === "lazy" || call.getExpression().getText() === "React.lazy");
|
|
643
|
+
if (usesLazy) {
|
|
644
|
+
const hasSuspense = sf.getFullText().includes("<Suspense") || sf.getFullText().includes("React.Suspense");
|
|
645
|
+
if (!hasSuspense) {
|
|
646
|
+
pushFinding("frontend.performance.lazy_without_suspense", "warning", sf, sf, "React.lazy used without Suspense boundary", findings);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const isInnerHTMLTestFile = /\.(spec|test)\.(ts|tsx|js|jsx)$/.test(filePath) || /\/(tests?|__tests__|helpers)\/.*dom\./i.test(filePath);
|
|
651
|
+
const isPluginLoader = /(leaflet|mapbox|google.*maps|chart.*plugin)/i.test(sf.getFullText().substring(0, 500));
|
|
652
|
+
|
|
653
|
+
if (!isInnerHTMLTestFile && !isPluginLoader) {
|
|
654
|
+
sf.getDescendantsOfKind(SyntaxKind.BinaryExpression).forEach((bin) => {
|
|
655
|
+
const left = bin.getLeft().getText();
|
|
656
|
+
if (/\.innerHTML\b/.test(left)) {
|
|
657
|
+
pushFinding("frontend.security.innerhtml_assignment", "error", sf, bin, "Assignment to innerHTML detected", findings);
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
sf.getDescendantsOfKind(SyntaxKind.FunctionDeclaration).forEach((fn) => {
|
|
662
|
+
const name = fn.getName();
|
|
663
|
+
if (!name || !/^[A-Z]/.test(name)) return;
|
|
664
|
+
const props = fn.getParameters();
|
|
665
|
+
if (props.length === 0) return;
|
|
666
|
+
const hasProps = props[0].getTypeNode();
|
|
667
|
+
if (!hasProps) return;
|
|
668
|
+
|
|
669
|
+
const isMemoized = sf.getImportDeclarations().some((imp) =>
|
|
670
|
+
imp.getModuleSpecifierValue() === "react" &&
|
|
671
|
+
imp.getNamedImports().some((n) => n.getName() === "memo")
|
|
672
|
+
);
|
|
673
|
+
const isWrappedInMemo = sf.getDescendantsOfKind(SyntaxKind.CallExpression).some((call) => {
|
|
674
|
+
const expr = call.getExpression().getText();
|
|
675
|
+
return expr === "memo" || expr === "React.memo";
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
if (!isMemoized && !isWrappedInMemo) {
|
|
679
|
+
pushFinding("frontend.react.missing_memo", "info", sf, fn, `Component ${name} with props should consider React.memo for performance`, findings);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
const useCallbackCalls = sf.getDescendantsOfKind(SyntaxKind.CallExpression).filter((call) => {
|
|
684
|
+
const expr = call.getExpression().getText();
|
|
685
|
+
return expr === "useCallback";
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
sf.getDescendantsOfKind(SyntaxKind.ArrowFunction).forEach((arrow) => {
|
|
689
|
+
const parent = arrow.getParent();
|
|
690
|
+
if (parent && parent.getKind && parent.getKind() === SyntaxKind.VariableDeclaration) {
|
|
691
|
+
const varDecl = parent;
|
|
692
|
+
const name = varDecl.getName();
|
|
693
|
+
if (name && /^on[A-Z]/.test(name)) { // Probablemente un event handler
|
|
694
|
+
const isCallback = useCallbackCalls.some((callback) =>
|
|
695
|
+
callback.getAncestors().some((anc) => anc === varDecl)
|
|
696
|
+
);
|
|
697
|
+
if (!isCallback) {
|
|
698
|
+
pushFinding("frontend.react.missing_usecallback", "info", sf, varDecl, `Event handler ${name} should use useCallback to prevent re-renders`, findings);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
if (isComponent) {
|
|
705
|
+
sf.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((call) => {
|
|
706
|
+
const expr = call.getExpression().getText();
|
|
707
|
+
if (/\.(filter|map|reduce|sort|find|some|every|includes)\(/.test(expr)) {
|
|
708
|
+
const parent = call.getParent();
|
|
709
|
+
if (parent && parent.getKind && parent.getKind() === SyntaxKind.VariableDeclaration) {
|
|
710
|
+
const varDecl = parent;
|
|
711
|
+
const isMemoized = sf.getDescendantsOfKind(SyntaxKind.CallExpression).some((memoCall) => {
|
|
712
|
+
const memoExpr = memoCall.getExpression().getText();
|
|
713
|
+
return memoExpr === "useMemo" && memoCall.getAncestors().some((anc) => anc === varDecl);
|
|
714
|
+
});
|
|
715
|
+
if (!isMemoized) {
|
|
716
|
+
pushFinding("frontend.react.missing_usememo", "info", sf, call, `Expensive computation should use useMemo`, findings);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// i18n: strings hardcodeados sin useTranslation
|
|
724
|
+
const hasUseTranslation = sf.getDescendantsOfKind(SyntaxKind.CallExpression).some((call) => {
|
|
725
|
+
const expr = call.getExpression().getText();
|
|
726
|
+
return /^useTranslation|translate\(|t\(/.test(expr);
|
|
727
|
+
});
|
|
728
|
+
if (hasUseTranslation) {
|
|
729
|
+
const stringLiterals = sf.getDescendantsOfKind(SyntaxKind.StringLiteral).filter((str) => {
|
|
730
|
+
const text = str.getLiteralValue();
|
|
731
|
+
const parent = str.getParent();
|
|
732
|
+
const isInTypeDefinition = parent && parent.getKind && (
|
|
733
|
+
parent.getKind() === SyntaxKind.UnionType ||
|
|
734
|
+
parent.getKind() === SyntaxKind.TypeLiteral
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
const isCssClass = /^(bg-|text-|border-|shadow-|rounded|flex|grid|hover:|focus:|dark:|animate-|transition|space-|gap-|p-|m-|w-|h-|opacity-|cursor-|overflow-|absolute|relative|fixed|sticky|top-|left-|right-|bottom-|z-|from-|to-|via-)/.test(text);
|
|
738
|
+
const isTestId = /^(data-testid|aria-|role)/.test(text) || text.includes('-banner') || text.includes('-button') || text.includes('-modal');
|
|
739
|
+
const isStorageKey = text.endsWith('Position') || text.endsWith('State') || text.endsWith('Cache') || text.endsWith('Token');
|
|
740
|
+
const isTerminalCommand = /^(cd |npm |git |yarn |pnpm |node |npx |bun )/.test(text);
|
|
741
|
+
const isErrorMatching = text.includes('server is not running') || text.includes('Backend') || text.includes('Network error');
|
|
742
|
+
|
|
743
|
+
return text.length > 10 &&
|
|
744
|
+
!isInTypeDefinition &&
|
|
745
|
+
!isCssClass &&
|
|
746
|
+
!isTestId &&
|
|
747
|
+
!isStorageKey &&
|
|
748
|
+
!isTerminalCommand &&
|
|
749
|
+
!isErrorMatching &&
|
|
750
|
+
!text.includes("http") &&
|
|
751
|
+
!text.includes("/") &&
|
|
752
|
+
!text.includes("px") &&
|
|
753
|
+
!text.includes("#") &&
|
|
754
|
+
!text.includes("use client") &&
|
|
755
|
+
!text.includes("use server") &&
|
|
756
|
+
!/^[A-Z_]+$/.test(text) &&
|
|
757
|
+
!/^[a-z]+\.[a-z.]+$/i.test(text) &&
|
|
758
|
+
!/^[a-z]{2}-[A-Z]{2}$/.test(text) &&
|
|
759
|
+
/\b[a-z]/.test(text) &&
|
|
760
|
+
/\s/.test(text);
|
|
761
|
+
});
|
|
762
|
+
if (stringLiterals.length > 5) {
|
|
763
|
+
pushFinding("frontend.i18n.hardcoded_strings", "warning", sf, sf, `Possible hardcoded strings detected (${stringLiterals.length}) - consider using useTranslation`, findings);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// i18n namespaces (skip if custom implementation doesn't support namespaces)
|
|
768
|
+
const hasCustomI18n = sf.getImportDeclarations().some((imp) =>
|
|
769
|
+
imp.getModuleSpecifierValue().includes('@/i18n') ||
|
|
770
|
+
imp.getModuleSpecifierValue().includes('i18n/index')
|
|
771
|
+
);
|
|
772
|
+
if (!hasCustomI18n && /useTranslation\(/.test(sf.getFullText()) && /useTranslation\(\)/.test(sf.getFullText())) {
|
|
773
|
+
pushFinding("frontend.i18n.missing_namespaces", "warning", sf, sf, "useTranslation without namespace argument", findings);
|
|
774
|
+
}
|
|
775
|
+
if (!isInfrastructure && sf.getFullText().match(/new\s+Date\(|Date\.now\(/) && !/Intl\.|date\-fns|dayjs/.test(sf.getFullText())) {
|
|
776
|
+
pushFinding("frontend.i18n.missing_formatting", "warning", sf, sf, "Date used in UI without localized formatting", findings);
|
|
777
|
+
}
|
|
778
|
+
const projectRoot = getRepoRoot();
|
|
779
|
+
const hasConfig =
|
|
780
|
+
fs.existsSync(path.join(projectRoot, 'next-i18next.config.js')) ||
|
|
781
|
+
fs.existsSync(path.join(projectRoot, 'i18n.ts')) ||
|
|
782
|
+
fs.existsSync(path.join(projectRoot, 'apps/admin-dashboard/i18n.ts')) ||
|
|
783
|
+
fs.existsSync(path.join(projectRoot, 'apps/admin-dashboard/src/infrastructure/config/i18n.config.ts')) ||
|
|
784
|
+
fs.existsSync(path.join(projectRoot, 'src/infrastructure/config/i18n.config.ts'));
|
|
785
|
+
if (!hasConfig && /\/app\//.test(filePath)) {
|
|
786
|
+
pushFinding("frontend.i18n.from_day_one", "info", sf, sf, "i18n config not found (heuristic)", findings);
|
|
787
|
+
pushFinding("frontend.i18n.fallback_locale", "info", sf, sf, "Fallback locale not configured (heuristic)", findings);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const hasZustand = sf.getImportDeclarations().some((imp) =>
|
|
791
|
+
imp.getModuleSpecifierValue().includes("zustand") ||
|
|
792
|
+
imp.getModuleSpecifierValue().includes("zustand/")
|
|
793
|
+
);
|
|
794
|
+
const hasRedux = sf.getImportDeclarations().some((imp) =>
|
|
795
|
+
imp.getModuleSpecifierValue().includes("redux") ||
|
|
796
|
+
imp.getModuleSpecifierValue().includes("react-redux")
|
|
797
|
+
);
|
|
798
|
+
const hasContext = sf.getDescendantsOfKind(SyntaxKind.CallExpression).some((call) =>
|
|
799
|
+
call.getExpression().getText() === "createContext" ||
|
|
800
|
+
call.getExpression().getText() === "React.createContext"
|
|
801
|
+
);
|
|
802
|
+
|
|
803
|
+
if ((hasRedux || hasContext) && !hasZustand) {
|
|
804
|
+
pushFinding("frontend.state.missing_zustand", "info", sf, sf, "Consider using Zustand for global state management (simpler than Redux/Context)", findings);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const hasReactQuery = sf.getImportDeclarations().some((imp) =>
|
|
808
|
+
imp.getModuleSpecifierValue().includes("@tanstack/react-query") ||
|
|
809
|
+
imp.getModuleSpecifierValue().includes("react-query")
|
|
810
|
+
);
|
|
811
|
+
const hasFetchCalls = sf.getDescendantsOfKind(SyntaxKind.CallExpression).some((call) => {
|
|
812
|
+
const expr = call.getExpression().getText();
|
|
813
|
+
return expr === "fetch" || expr.includes("axios") || expr.includes("api");
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
if (hasFetchCalls && !hasReactQuery && isComponent) {
|
|
817
|
+
pushFinding("frontend.state.missing_react_query", "warning", sf, sf, "Server state management without React Query - consider using for caching and synchronization", findings);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const hasTailwindImport = sf.getImportDeclarations().some((imp) =>
|
|
821
|
+
imp.getModuleSpecifierValue().includes("tailwind") ||
|
|
822
|
+
imp.getModuleSpecifierValue().includes("clsx") ||
|
|
823
|
+
imp.getModuleSpecifierValue().includes("cn")
|
|
824
|
+
);
|
|
825
|
+
const hasClassNameUsage = sf.getDescendantsOfKind(SyntaxKind.JsxAttribute).some((attr) => {
|
|
826
|
+
const name = attr.getNameNode()?.getText();
|
|
827
|
+
return name === "className";
|
|
828
|
+
});
|
|
829
|
+
const hasTailwind = hasTailwindImport || hasClassNameUsage;
|
|
830
|
+
|
|
831
|
+
const hasInlineStyles = sf.getDescendantsOfKind(SyntaxKind.JsxAttribute).some((attr) => {
|
|
832
|
+
const name = attr.getNameNode()?.getText();
|
|
833
|
+
return name === "style";
|
|
834
|
+
});
|
|
835
|
+
const hasCssModules = sf.getImportDeclarations().some((imp) =>
|
|
836
|
+
imp.getModuleSpecifierValue().endsWith(".module.css") ||
|
|
837
|
+
imp.getModuleSpecifierValue().endsWith(".module.scss")
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
if (hasInlineStyles && !hasTailwind && !hasCssModules) {
|
|
841
|
+
pushFinding("frontend.styling.missing_tailwind", "info", sf, sf, "Consider using Tailwind CSS for consistent utility-first styling", findings);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
sf.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((call) => {
|
|
845
|
+
const expr = call.getExpression().getText();
|
|
846
|
+
if (expr === "fetch" || expr.includes("axios") || expr.includes("useQuery")) {
|
|
847
|
+
const hasLoadingState = sf.getFullText().includes("loading") || sf.getFullText().includes("isLoading") || sf.getFullText().includes("pending");
|
|
848
|
+
if (!hasLoadingState) {
|
|
849
|
+
pushFinding("frontend.api.loading_states", "info", sf, call, "API call without loading state - consider showing skeleton screens or spinners", findings);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
sf.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((call) => {
|
|
855
|
+
const expr = call.getExpression().getText();
|
|
856
|
+
if (expr === "fetch" || expr.includes("axios")) {
|
|
857
|
+
const hasRetry = sf.getFullText().includes("retry") || sf.getFullText().includes("retryOnError");
|
|
858
|
+
if (!hasRetry) {
|
|
859
|
+
pushFinding("frontend.api.retry_logic", "info", sf, call, "API call without retry logic - consider exponential backoff for failed requests", findings);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
if (/\/app\/api\//.test(filePath) || /\/pages\/api\//.test(filePath)) {
|
|
864
|
+
const txt = sf.getFullText();
|
|
865
|
+
const usesResponse = /NextResponse|Response/.test(txt);
|
|
866
|
+
const hasCache = /Cache-Control/i.test(txt);
|
|
867
|
+
if (usesResponse && !hasCache) {
|
|
868
|
+
pushFinding("frontend.api.missing_cache_headers", "warning", sf, sf, "API route without Cache-Control header", findings);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const hasAppRouter = sf.getFullText().includes("app/") || sf.getFullText().includes("layout.tsx") || sf.getFullText().includes("page.tsx");
|
|
873
|
+
if (hasAppRouter) {
|
|
874
|
+
pushFinding("frontend.nextjs.app_router", "info", sf, sf, "Using Next.js App Router - good for modern React Server Components", findings);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (/\/app\/api\//.test(filePath) && filePath.endsWith('.ts')) {
|
|
878
|
+
const hasHandler = /export\s+const\s+(GET|POST|PUT|DELETE|PATCH)\s*=/.test(sf.getFullText());
|
|
879
|
+
if (!hasHandler) {
|
|
880
|
+
pushFinding("frontend.nextjs.route_handlers", "warning", sf, sf, "Route inside app/api without exported HTTP handlers", findings);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if (/\/app\//.test(filePath) && filePath.endsWith('.tsx')) {
|
|
884
|
+
const fileContent = sf.getFullText();
|
|
885
|
+
const isClientComponent = /^['"]use client['"];?\s*$/m.test(fileContent);
|
|
886
|
+
|
|
887
|
+
if (!isClientComponent && /fetch\(/.test(fileContent) && !/cache\s*:\s*"no\-store"|next\s*:\s*\{\s*revalidate\s*:/.test(fileContent)) {
|
|
888
|
+
pushFinding("frontend.nextjs.data_fetching", "warning", sf, sf, "Server fetch without cache/revalidate options", findings);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const hasStrict = sf.getFullText().includes('"strict": true') || sf.getFullText().includes("'strict': true");
|
|
893
|
+
if (!hasStrict && sf.getFullText().includes("tsconfig.json")) {
|
|
894
|
+
pushFinding("frontend.typescript.strict_mode", "warning", sf, sf, "TypeScript strict mode not enabled - consider enabling for better type safety", findings);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
sf.getDescendantsOfKind(SyntaxKind.AnyKeyword).forEach((anyKeyword) => {
|
|
898
|
+
pushFinding("frontend.typescript.any_usage", "warning", sf, anyKeyword, "Usage of 'any' type - prefer specific types for better type safety", findings);
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
const normalizedFilePath = String(filePath || '').replace(/\\/g, '/').toLowerCase();
|
|
902
|
+
const looksLikeFrontendAppCode =
|
|
903
|
+
normalizedFilePath.includes('/src/') ||
|
|
904
|
+
normalizedFilePath.includes('/app/') ||
|
|
905
|
+
normalizedFilePath.includes('/pages/') ||
|
|
906
|
+
normalizedFilePath.includes('/components/') ||
|
|
907
|
+
normalizedFilePath.includes('/features/');
|
|
908
|
+
const shouldCheckImplicitAny = looksLikeFrontendAppCode && (normalizedFilePath.endsWith('.ts') || normalizedFilePath.endsWith('.tsx'));
|
|
909
|
+
sf.getDescendantsOfKind(SyntaxKind.Parameter).forEach((param) => {
|
|
910
|
+
if (shouldCheckImplicitAny && !param.getTypeNode()) {
|
|
911
|
+
pushFinding("frontend.typescript.implicit_any", "warning", sf, param, "Parameter without explicit type - add type annotation", findings);
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
if (filePath.endsWith('.ts') && !filePath.endsWith('.d.ts')) {
|
|
916
|
+
sf.getDescendantsOfKind(SyntaxKind.CatchClause).forEach((catchClause) => {
|
|
917
|
+
const varDecl = catchClause.getVariableDeclaration();
|
|
918
|
+
if (varDecl) {
|
|
919
|
+
const typeNode = varDecl.getTypeNode();
|
|
920
|
+
if (!typeNode) {
|
|
921
|
+
pushFinding(
|
|
922
|
+
"frontend.error_handling.untyped_catch",
|
|
923
|
+
"high",
|
|
924
|
+
sf,
|
|
925
|
+
catchClause,
|
|
926
|
+
"Catch parameter MUST be typed as ': unknown' - use type guards (error instanceof ApiError)",
|
|
927
|
+
findings
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
sf.getDescendantsOfKind(SyntaxKind.ExpressionStatement).forEach((stmt) => {
|
|
935
|
+
const text = stmt.getText().trim();
|
|
936
|
+
if (/^void\s+(err|error)\s*;?\s*$/.test(text)) {
|
|
937
|
+
pushFinding(
|
|
938
|
+
"frontend.error_handling.void_error",
|
|
939
|
+
"high",
|
|
940
|
+
sf,
|
|
941
|
+
stmt,
|
|
942
|
+
"NEVER use 'void err' - handle errors with type guards (ApiError/Error) and error state",
|
|
943
|
+
findings
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
sf.getDescendantsOfKind(SyntaxKind.VariableDeclaration).forEach((varDecl) => {
|
|
949
|
+
const typeNode = varDecl.getTypeNode();
|
|
950
|
+
if (typeNode && typeNode.getText() === 'unknown') {
|
|
951
|
+
const parent = varDecl.getParent();
|
|
952
|
+
const scope = varDecl.getFirstAncestorByKind(SyntaxKind.Block);
|
|
953
|
+
if (scope) {
|
|
954
|
+
const scopeText = scope.getText();
|
|
955
|
+
const varName = varDecl.getName();
|
|
956
|
+
const hasTypeGuard = new RegExp(`${varName}\\s+instanceof|typeof\\s+${varName}|${varName}\\.constructor`).test(scopeText);
|
|
957
|
+
if (!hasTypeGuard) {
|
|
958
|
+
pushFinding(
|
|
959
|
+
"frontend.typescript.unknown_without_guard",
|
|
960
|
+
"high",
|
|
961
|
+
sf,
|
|
962
|
+
varDecl,
|
|
963
|
+
`Variable '${varName}: unknown' used without type guards - add instanceof/typeof checks`,
|
|
964
|
+
findings
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
sf.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((call) => {
|
|
972
|
+
const expr = call.getExpression();
|
|
973
|
+
if (expr.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
974
|
+
const text = expr.getText();
|
|
975
|
+
if (text === 'console.error') {
|
|
976
|
+
const catchBlock = call.getFirstAncestor((node) => node.getKind() === SyntaxKind.CatchClause);
|
|
977
|
+
if (catchBlock) {
|
|
978
|
+
pushFinding(
|
|
979
|
+
"frontend.error_handling.console_in_catch",
|
|
980
|
+
"medium",
|
|
981
|
+
sf,
|
|
982
|
+
call,
|
|
983
|
+
"Avoid console.error in catch - use error state management (setError, setChartError)",
|
|
984
|
+
findings
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
const hasReactHookForm = sf.getImportDeclarations().some((imp) =>
|
|
992
|
+
imp.getModuleSpecifierValue().includes("react-hook-form")
|
|
993
|
+
);
|
|
994
|
+
const hasFormElements = sf.getDescendantsOfKind(SyntaxKind.JsxOpeningElement).some((el) => {
|
|
995
|
+
const tag = el.getTagNameNode()?.getText();
|
|
996
|
+
return tag === "form" || tag === "input" || tag === "textarea" || tag === "select";
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
if (hasFormElements && !hasReactHookForm) {
|
|
1000
|
+
pushFinding("frontend.forms.missing_react_hook_form", "info", sf, sf, "Forms without React Hook Form - consider using for better form management and validation", findings);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const hasZod = sf.getImportDeclarations().some((imp) =>
|
|
1004
|
+
imp.getModuleSpecifierValue().includes("zod")
|
|
1005
|
+
);
|
|
1006
|
+
if (hasReactHookForm && !hasZod) {
|
|
1007
|
+
pushFinding("frontend.forms.missing_zod", "info", sf, sf, "React Hook Form without Zod - consider using Zod for type-safe form validation", findings);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const hasDynamicImport = sf.getDescendantsOfKind(SyntaxKind.CallExpression).some((call) => {
|
|
1011
|
+
const expr = call.getExpression().getText();
|
|
1012
|
+
return expr === "import" || expr.includes("next/dynamic");
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
if (!hasDynamicImport && sf.getFullText().length > 10000) { // Large file
|
|
1016
|
+
pushFinding("frontend.performance.code_splitting", "info", sf, sf, "Large component without code splitting - consider lazy loading with React.lazy or next/dynamic", findings);
|
|
1017
|
+
pushFinding("frontend.performance.missing_code_splitting", "info", sf, sf, "Large component without code splitting - consider lazy loading with React.lazy or next/dynamic", findings);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
sf.getDescendantsOfKind(SyntaxKind.JsxElement).forEach((jsx) => {
|
|
1021
|
+
const tag = jsx.getOpeningElement()?.getTagNameNode()?.getText();
|
|
1022
|
+
if (tag && (tag.includes("List") || tag.includes("Grid") || tag === "ul" || tag === "ol")) {
|
|
1023
|
+
const hasVirtualization = sf.getImportDeclarations().some((imp) =>
|
|
1024
|
+
imp.getModuleSpecifierValue().includes("react-window") ||
|
|
1025
|
+
imp.getModuleSpecifierValue().includes("react-virtualized")
|
|
1026
|
+
);
|
|
1027
|
+
if (!hasVirtualization) {
|
|
1028
|
+
pushFinding("frontend.performance.virtualization", "info", sf, jsx, "Large list without virtualization - consider react-window for better performance", findings);
|
|
1029
|
+
pushFinding("frontend.performance.missing_virtual_scrolling", "info", sf, jsx, "Large list without virtualization - consider react-window for better performance", findings);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
const totalLines = sf.getText().split(/\r?\n/).length;
|
|
1035
|
+
if (totalLines > 500) {
|
|
1036
|
+
pushFinding("frontend.file.too_large", "info", sf, sf, `Large TSX file (${totalLines} lines)`, findings);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
sf.getDescendantsOfKind(SyntaxKind.JsxAttribute).forEach((attr) => {
|
|
1040
|
+
const name = attr.getNameNode()?.getText();
|
|
1041
|
+
if (name === "dangerouslySetInnerHTML") {
|
|
1042
|
+
pushFinding("frontend.security.dangerous_html", "warning", sf, attr, "dangerouslySetInnerHTML usage - ensure HTML is sanitized to prevent XSS attacks", findings);
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
sf.getDescendantsOfKind(SyntaxKind.JsxAttribute).forEach((attr) => {
|
|
1046
|
+
const n = attr.getNameNode()?.getText();
|
|
1047
|
+
if (n === "style") {
|
|
1048
|
+
const parent = attr.getParent();
|
|
1049
|
+
const elementName = parent?.getFirstChildByKind(SyntaxKind.Identifier)?.getText() || '';
|
|
1050
|
+
const isChartComponent = /Chart|Legend/.test(filePath);
|
|
1051
|
+
const isSvgElement = ['Label', 'text', 'circle', 'path', 'rect', 'line'].includes(elementName);
|
|
1052
|
+
|
|
1053
|
+
const styleValue = attr.getInitializer()?.getText() || '';
|
|
1054
|
+
const hasDynamicColorProp = /\{\s*(backgroundColor|color|fill)[\s:}]/.test(styleValue);
|
|
1055
|
+
const isChartFile = /Chart|Legend|chart/.test(filePath);
|
|
1056
|
+
const isDynamicColor = hasDynamicColorProp && isChartFile;
|
|
1057
|
+
|
|
1058
|
+
if (!isSvgElement && !isDynamicColor) {
|
|
1059
|
+
pushFinding("frontend.styling.inline_style", "warning", sf, attr, "Inline style detected - prefer className with Tailwind/CSS Modules", findings);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
sf.getDescendantsOfKind(SyntaxKind.JsxAttribute).forEach((attr) => {
|
|
1065
|
+
const name = attr.getNameNode()?.getText();
|
|
1066
|
+
const value = attr.getInitializer()?.getText();
|
|
1067
|
+
if (name && /^on[A-Z]/.test(name) && value && value.includes("javascript:")) {
|
|
1068
|
+
pushFinding("frontend.security.inline_handlers", "error", sf, attr, "Inline JavaScript in event handler - potential XSS vulnerability", findings);
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
const isConfigFile = /\.(config|spec|test)\.(ts|tsx|js|jsx)$/.test(filePath);
|
|
1073
|
+
sf.getDescendantsOfKind(SyntaxKind.StringLiteral).forEach((str) => {
|
|
1074
|
+
const text = str.getLiteralValue();
|
|
1075
|
+
if (text.startsWith("http://")) {
|
|
1076
|
+
const isLocalhost = /^http:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|::1)(:|\/|$)/i.test(text);
|
|
1077
|
+
const isEnvVar = /process\.env\.|NEXT_PUBLIC_|API_URL/.test(sf.getFullText());
|
|
1078
|
+
const isDevOnly = isConfigFile || /development|test|local/i.test(sf.getFullText());
|
|
1079
|
+
const isXMLNamespace = /xmlns=|http:\/\/(www\.)?w3\.org\/(1999|2000)\/(svg|xhtml)/i.test(text);
|
|
1080
|
+
|
|
1081
|
+
if (!isLocalhost && !isDevOnly && !isXMLNamespace) {
|
|
1082
|
+
pushFinding("frontend.security.https_always", "error", sf, str, "HTTP URL detected - always use HTTPS in production", findings);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
const isTest = isTestFile(filePath);
|
|
1088
|
+
if (!isTest && sf.getDescendantsOfKind(SyntaxKind.FunctionDeclaration).some((fn) =>
|
|
1089
|
+
fn.getName() && /^[A-Z]/.test(fn.getName())
|
|
1090
|
+
)) {
|
|
1091
|
+
const testFilePath = filePath.replace(/\.tsx?$/, `.test.$&`);
|
|
1092
|
+
if (!fs.existsSync(testFilePath)) {
|
|
1093
|
+
pushFinding("frontend.testing.missing_tests", "info", sf, sf, `Missing test file: ${testFilePath}`, findings);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (isTest) {
|
|
1098
|
+
const hasRTL = sf.getImportDeclarations().some((imp) =>
|
|
1099
|
+
imp.getModuleSpecifierValue().includes("@testing-library/react")
|
|
1100
|
+
);
|
|
1101
|
+
const hasEnzyme = sf.getImportDeclarations().some((imp) =>
|
|
1102
|
+
imp.getModuleSpecifierValue().includes("enzyme")
|
|
1103
|
+
);
|
|
1104
|
+
|
|
1105
|
+
if (hasEnzyme && !hasRTL) {
|
|
1106
|
+
pushFinding("frontend.testing.missing_rtl", "warning", sf, sf, "Using Enzyme instead of React Testing Library - prefer RTL for user-centric testing", findings);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
if (isTest) {
|
|
1111
|
+
const hasMSW = sf.getImportDeclarations().some((imp) =>
|
|
1112
|
+
imp.getModuleSpecifierValue().includes("msw")
|
|
1113
|
+
);
|
|
1114
|
+
const hasFetch = sf.getDescendantsOfKind(SyntaxKind.CallExpression).some((call) =>
|
|
1115
|
+
call.getExpression().getText() === "fetch"
|
|
1116
|
+
);
|
|
1117
|
+
|
|
1118
|
+
if (hasFetch && !hasMSW) {
|
|
1119
|
+
pushFinding("frontend.testing.missing_msw", "info", sf, sf, "API calls in tests without MSW - consider using Mock Service Worker for API mocking", findings);
|
|
1120
|
+
pushFinding("frontend.testing.msw", "info", sf, sf, "API calls in tests without MSW - consider using Mock Service Worker for API mocking", findings);
|
|
1121
|
+
}
|
|
1122
|
+
const usesRTL = sf.getImportDeclarations().some((imp) => imp.getModuleSpecifierValue().includes("@testing-library/react"));
|
|
1123
|
+
const usesUserEvent = sf.getImportDeclarations().some((imp) => imp.getModuleSpecifierValue().includes("@testing-library/user-event"));
|
|
1124
|
+
if (usesRTL && !usesUserEvent && /fireEvent\./.test(sf.getFullText())) {
|
|
1125
|
+
pushFinding("frontend.testing.missing_userevent", "warning", sf, sf, "fireEvent used without userEvent – prefer userEvent for realistic interactions", findings);
|
|
1126
|
+
}
|
|
1127
|
+
if (/toMatchSnapshot\(/.test(sf.getFullText())) {
|
|
1128
|
+
pushFinding("frontend.testing.snapshot_moderation", "warning", sf, sf, "Snapshot assertions detected – use sparingly on stable components", findings);
|
|
1129
|
+
}
|
|
1130
|
+
if (/page\.tsx$/.test(filePath)) {
|
|
1131
|
+
const candidate = filePath.replace(/\/app\//, '/e2e/').replace(/\.tsx$/, '.spec.ts');
|
|
1132
|
+
try {
|
|
1133
|
+
if (!fs.existsSync(candidate)) {
|
|
1134
|
+
pushFinding("frontend.testing.missing_e2e", "info", sf, sf, "No E2E spec found for this page (heuristic)", findings);
|
|
1135
|
+
}
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
if (process.env.DEBUG) {
|
|
1138
|
+
console.debug(`[frontend-ast] Failed to check E2E spec file: ${error.message}`);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// ==========================================
|
|
1145
|
+
// ==========================================
|
|
1146
|
+
|
|
1147
|
+
const content = sf.getFullText();
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
const componentPattern = /^(export\s+)?(?:const|function)\s+([A-Z]\w+)\s*[=:].*(?:React\.FC|JSX\.Element|\(\)\s*=>|function)/gm;
|
|
1151
|
+
const components = Array.from(content.matchAll(componentPattern));
|
|
1152
|
+
if (components.length > 3 && !filePath.includes('.stories.') && !filePath.includes('index.tsx')) {
|
|
1153
|
+
pushFinding(
|
|
1154
|
+
"frontend.solid.srp_multiple_components",
|
|
1155
|
+
"high",
|
|
1156
|
+
sf,
|
|
1157
|
+
sf,
|
|
1158
|
+
`File defines ${components.length} components - split into separate files (SRP: one component per file)`,
|
|
1159
|
+
findings
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
if (components.length >= 1) {
|
|
1164
|
+
components.forEach((compMatch) => {
|
|
1165
|
+
const componentName = compMatch[2];
|
|
1166
|
+
const componentStart = compMatch.index || 0;
|
|
1167
|
+
const componentEnd = content.indexOf('\n}\n', componentStart) || content.length;
|
|
1168
|
+
const componentBody = content.substring(componentStart, componentEnd);
|
|
1169
|
+
|
|
1170
|
+
const hookCount = (componentBody.match(/\buse[A-Z]\w+\(/g) || []).length;
|
|
1171
|
+
const functionCount = (componentBody.match(/(?:const|let)\s+\w+\s*=\s*(?:async\s*)?\(/g) || []).length;
|
|
1172
|
+
const totalComplexity = hookCount + functionCount;
|
|
1173
|
+
|
|
1174
|
+
if (godComponentBaseline) {
|
|
1175
|
+
const totalComplexityZ = godComponentBaseline.robustZ(totalComplexity, godComponentBaseline.med.totalComplexity, godComponentBaseline.mad.totalComplexity);
|
|
1176
|
+
const bodyLines = componentBody.split('\n').length;
|
|
1177
|
+
const bodyLinesZ = godComponentBaseline.robustZ(bodyLines, godComponentBaseline.med.bodyLines, godComponentBaseline.mad.bodyLines);
|
|
1178
|
+
|
|
1179
|
+
const complexityOutlier = totalComplexityZ >= godComponentBaseline.thresholds.outlier.totalComplexityZ;
|
|
1180
|
+
const sizeOutlier = bodyLinesZ >= godComponentBaseline.thresholds.outlier.bodyLinesZ;
|
|
1181
|
+
const extreme = totalComplexityZ >= godComponentBaseline.thresholds.extreme.totalComplexityZ || bodyLinesZ >= godComponentBaseline.thresholds.extreme.bodyLinesZ;
|
|
1182
|
+
|
|
1183
|
+
if (extreme || (complexityOutlier && sizeOutlier)) {
|
|
1184
|
+
pushFinding(
|
|
1185
|
+
"frontend.solid.srp_god_component",
|
|
1186
|
+
"critical",
|
|
1187
|
+
sf,
|
|
1188
|
+
sf,
|
|
1189
|
+
`Component '${componentName}' has ${hookCount} hooks + ${functionCount} functions = ${totalComplexity} (z=${totalComplexityZ.toFixed(2)}), ${bodyLines} lines (z=${bodyLinesZ.toFixed(2)}) - split responsibilities (SRP)`,
|
|
1190
|
+
findings
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
const switchPattern = /switch\s*\([^)]+\)\s*\{[\s\S]{300,}?\}/g;
|
|
1199
|
+
let switchMatch;
|
|
1200
|
+
while ((switchMatch = switchPattern.exec(content)) !== null) {
|
|
1201
|
+
const caseCount = (switchMatch[0].match(/case\s+/g) || []).length;
|
|
1202
|
+
if (caseCount > 5) {
|
|
1203
|
+
const lineNumber = content.substring(0, switchMatch.index).split('\n').length;
|
|
1204
|
+
pushFinding(
|
|
1205
|
+
"frontend.solid.ocp_switch_polymorphism",
|
|
1206
|
+
"high",
|
|
1207
|
+
sf,
|
|
1208
|
+
sf,
|
|
1209
|
+
`Line ${lineNumber}: Large switch (${caseCount} cases) - use lookup table/strategy pattern (OCP: open for extension)`,
|
|
1210
|
+
findings
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const ifElseRenderPattern = /if\s*\([^)]+\)\s*\{[\s\S]{50,}?return\s+<[\s\S]{50,}?else\s+if\s*\([^)]+\)\s*\{[\s\S]{50,}?return\s+</g;
|
|
1216
|
+
if (ifElseRenderPattern.test(content)) {
|
|
1217
|
+
pushFinding(
|
|
1218
|
+
"frontend.solid.ocp_conditional_render",
|
|
1219
|
+
"medium",
|
|
1220
|
+
sf,
|
|
1221
|
+
sf,
|
|
1222
|
+
'Multiple if-else for rendering - use component mapping/strategies (OCP: polymorphic components)',
|
|
1223
|
+
findings
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
const extendsPattern = /(?:interface|type)\s+(\w+Props)\s+extends\s+(\w+Props)/g;
|
|
1229
|
+
let extendsMatch;
|
|
1230
|
+
while ((extendsMatch = extendsPattern.exec(content)) !== null) {
|
|
1231
|
+
const childProps = extendsMatch[1];
|
|
1232
|
+
const parentProps = extendsMatch[2];
|
|
1233
|
+
|
|
1234
|
+
const propsDefStart = content.indexOf(extendsMatch[0]);
|
|
1235
|
+
const propsDefEnd = content.indexOf('}', propsDefStart);
|
|
1236
|
+
const propsDef = content.substring(propsDefStart, propsDefEnd);
|
|
1237
|
+
|
|
1238
|
+
const requiredPropsCount = (propsDef.match(/\w+\s*:\s*[^?]/g) || []).length;
|
|
1239
|
+
if (requiredPropsCount > 3) {
|
|
1240
|
+
const lineNumber = content.substring(0, propsDefStart).split('\n').length;
|
|
1241
|
+
pushFinding(
|
|
1242
|
+
"frontend.solid.lsp_props_narrowing",
|
|
1243
|
+
"high",
|
|
1244
|
+
sf,
|
|
1245
|
+
sf,
|
|
1246
|
+
`Line ${lineNumber}: ${childProps} extends ${parentProps} but adds ${requiredPropsCount} required props - violates LSP (subtypes should be substitutable)`,
|
|
1247
|
+
findings
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
const propsInterfacePattern = /(?:interface|type)\s+(\w+Props)\s*\{([^}]{200,})\}/g;
|
|
1254
|
+
let propsMatch;
|
|
1255
|
+
while ((propsMatch = propsInterfacePattern.exec(content)) !== null) {
|
|
1256
|
+
const propsName = propsMatch[1];
|
|
1257
|
+
const propsBody = propsMatch[2];
|
|
1258
|
+
const propsCount = (propsBody.match(/\w+\s*[?:]?\s*:/g) || []).length;
|
|
1259
|
+
|
|
1260
|
+
if (propsCount > 10) {
|
|
1261
|
+
const lineNumber = content.substring(0, propsMatch.index).split('\n').length;
|
|
1262
|
+
pushFinding(
|
|
1263
|
+
"frontend.solid.isp_fat_props",
|
|
1264
|
+
"high",
|
|
1265
|
+
sf,
|
|
1266
|
+
sf,
|
|
1267
|
+
`Line ${lineNumber}: ${propsName} has ${propsCount} properties - split into smaller interfaces (ISP: clients shouldn't depend on unused props)`,
|
|
1268
|
+
findings
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
if (filePath.includes('/components/') || filePath.includes('/presentation/')) {
|
|
1275
|
+
const concreteImports = ['axios', 'fetch', 'localStorage', 'sessionStorage', '@supabase/supabase-js'];
|
|
1276
|
+
concreteImports.forEach((concrete) => {
|
|
1277
|
+
if (content.includes(`import`) && content.includes(concrete) && !filePath.includes('infrastructure/')) {
|
|
1278
|
+
const lineNumber = content.indexOf(concrete) > -1 ? content.substring(0, content.indexOf(concrete)).split('\n').length : 1;
|
|
1279
|
+
pushFinding(
|
|
1280
|
+
"frontend.solid.dip_concrete_dependency",
|
|
1281
|
+
"critical",
|
|
1282
|
+
sf,
|
|
1283
|
+
sf,
|
|
1284
|
+
`Line ${lineNumber}: Component/View depends on concrete ${concrete} - inject via props/context (DIP: depend on abstractions)`,
|
|
1285
|
+
findings
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
if (filePath.includes('/hooks/') || /^use[A-Z]/.test(path.basename(filePath))) {
|
|
1292
|
+
if (content.includes('new ') && (content.includes('Service(') || content.includes('Client(') || content.includes('Api('))) {
|
|
1293
|
+
pushFinding(
|
|
1294
|
+
"frontend.solid.dip_hook_instantiation",
|
|
1295
|
+
"high",
|
|
1296
|
+
sf,
|
|
1297
|
+
sf,
|
|
1298
|
+
'Hook instantiates service directly - receive as parameter (DIP: high-level shouldn\'t know low-level construction)',
|
|
1299
|
+
findings
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// ==========================================
|
|
1305
|
+
// ==========================================
|
|
1306
|
+
|
|
1307
|
+
|
|
1308
|
+
if (filePath.includes('/presentation/') || filePath.includes('/components/')) {
|
|
1309
|
+
const forbiddenImports = ['axios', 'supabase', 'prisma', 'mongoose', 'fetch'];
|
|
1310
|
+
forbiddenImports.forEach((forbidden) => {
|
|
1311
|
+
if (content.includes(`from '${forbidden}'`) || content.includes(`from "${forbidden}"`)) {
|
|
1312
|
+
pushFinding(
|
|
1313
|
+
"frontend.clean_arch.presentation_infrastructure",
|
|
1314
|
+
"critical",
|
|
1315
|
+
sf,
|
|
1316
|
+
sf,
|
|
1317
|
+
`Presentation layer imports ${forbidden} - use repository abstraction (Clean Arch: dependencies point inward)`,
|
|
1318
|
+
findings
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
if (filePath.includes('/components/') || filePath.includes('/pages/') || filePath.includes('/app/') && filePath.endsWith('.tsx')) {
|
|
1325
|
+
const businessPatterns = [
|
|
1326
|
+
/fetch\s*\(/,
|
|
1327
|
+
/axios\./,
|
|
1328
|
+
/\.\s*post\s*\(/,
|
|
1329
|
+
/\.\s*put\s*\(/,
|
|
1330
|
+
/\.\s*delete\s*\(/,
|
|
1331
|
+
/localStorage\.setItem/,
|
|
1332
|
+
/sessionStorage\.setItem/,
|
|
1333
|
+
/new\s+Date\(\).*format/
|
|
1334
|
+
];
|
|
1335
|
+
|
|
1336
|
+
businessPatterns.forEach((pattern) => {
|
|
1337
|
+
if (pattern.test(content)) {
|
|
1338
|
+
const match = content.match(pattern);
|
|
1339
|
+
if (match && match.index !== undefined) {
|
|
1340
|
+
const lineNumber = content.substring(0, match.index).split('\n').length;
|
|
1341
|
+
pushFinding(
|
|
1342
|
+
"frontend.clean_arch.business_logic_in_ui",
|
|
1343
|
+
"high",
|
|
1344
|
+
sf,
|
|
1345
|
+
sf,
|
|
1346
|
+
`Line ${lineNumber}: Business logic in UI component - move to use-case/service (Clean Arch: UI coordinates, doesn't decide)`,
|
|
1347
|
+
findings
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
if (filePath.includes('/utils/') || filePath.includes('/helpers/') || filePath.includes('/lib/')) {
|
|
1355
|
+
if (!filePath.includes('node_modules') && filePath.endsWith('.ts') || filePath.endsWith('.tsx')) {
|
|
1356
|
+
pushFinding(
|
|
1357
|
+
"frontend.clean_arch.forbidden_directory",
|
|
1358
|
+
"critical",
|
|
1359
|
+
sf,
|
|
1360
|
+
sf,
|
|
1361
|
+
'utils/helpers/lib directories forbidden - move to infrastructure/ or application/ (Clean Arch: proper layer organization)',
|
|
1362
|
+
findings
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if ((content.includes('Repository') || content.includes('Api')) &&
|
|
1368
|
+
content.includes('class ') &&
|
|
1369
|
+
!filePath.includes('/infrastructure/')) {
|
|
1370
|
+
pushFinding(
|
|
1371
|
+
"frontend.clean_arch.repository_location",
|
|
1372
|
+
"high",
|
|
1373
|
+
sf,
|
|
1374
|
+
sf,
|
|
1375
|
+
'Repository/API implementation outside infrastructure/ - move to infrastructure/repositories/',
|
|
1376
|
+
findings
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// ==========================================
|
|
1381
|
+
// ==========================================
|
|
1382
|
+
|
|
1383
|
+
if (isTestFile(filePath)) {
|
|
1384
|
+
const testPattern = /(?:it|test)\s*\(\s*['"`]([^'"`]+)['"`]/g;
|
|
1385
|
+
let testMatch;
|
|
1386
|
+
const testNames = [];
|
|
1387
|
+
|
|
1388
|
+
while ((testMatch = testPattern.exec(content)) !== null) {
|
|
1389
|
+
const testName = testMatch[1];
|
|
1390
|
+
testNames.push({ name: testName, index: testMatch.index });
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
testNames.forEach((test) => {
|
|
1394
|
+
const hasBDDStructure = /given|when|then|should/i.test(test.name);
|
|
1395
|
+
const isDescriptive = test.name.length > 20;
|
|
1396
|
+
|
|
1397
|
+
if (!hasBDDStructure && isDescriptive) {
|
|
1398
|
+
const lineNumber = content.substring(0, test.index).split('\n').length;
|
|
1399
|
+
pushFinding(
|
|
1400
|
+
"frontend.bdd.test_naming",
|
|
1401
|
+
"medium",
|
|
1402
|
+
sf,
|
|
1403
|
+
sf,
|
|
1404
|
+
`Line ${lineNumber}: Test '${test.name.substring(0, 40)}...' - use BDD naming: 'should X when Y' or 'given X, when Y, then Z'`,
|
|
1405
|
+
findings
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
if (!content.includes('makeSUT') && !content.includes('createSUT') && !content.includes('setup')) {
|
|
1411
|
+
const hasMultipleTests = testNames.length > 3;
|
|
1412
|
+
if (hasMultipleTests) {
|
|
1413
|
+
pushFinding(
|
|
1414
|
+
"frontend.bdd.missing_make_sut",
|
|
1415
|
+
"medium",
|
|
1416
|
+
sf,
|
|
1417
|
+
sf,
|
|
1418
|
+
'Test file with multiple tests - extract SUT creation to makeSUT factory (BDD: reusable test setup)',
|
|
1419
|
+
findings
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
if (content.includes('jest.mock') && !content.includes('jest.spyOn')) {
|
|
1425
|
+
pushFinding(
|
|
1426
|
+
"frontend.bdd.prefer_spies",
|
|
1427
|
+
"low",
|
|
1428
|
+
sf,
|
|
1429
|
+
sf,
|
|
1430
|
+
'Test uses jest.mock - prefer jest.spyOn (BDD: spy on real behavior, don\'t replace entirely)',
|
|
1431
|
+
findings
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
if (!isTestFile(filePath) && (content.includes('Mock') || content.includes('Stub') || content.includes('Fake')) && content.includes('export')) {
|
|
1436
|
+
pushFinding(
|
|
1437
|
+
"frontend.testing.mock_in_production",
|
|
1438
|
+
"critical",
|
|
1439
|
+
sf,
|
|
1440
|
+
sf,
|
|
1441
|
+
'Mock/Stub/Fake exported from production code - move to test files',
|
|
1442
|
+
findings
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// ==========================================
|
|
1448
|
+
// ==========================================
|
|
1449
|
+
|
|
1450
|
+
const commentPattern = /\/\/(?!\s*TODO:)(?!\s*FIXME:)(?!\s*eslint-)(?!\s*@ts-)(?!\s*prettier-)[\s]*\w{3,}[^\n]{15,}/g;
|
|
1451
|
+
let commentMatch;
|
|
1452
|
+
let commentCount = 0;
|
|
1453
|
+
|
|
1454
|
+
while ((commentMatch = commentPattern.exec(content)) !== null && commentCount < 5) {
|
|
1455
|
+
const lineNumber = content.substring(0, commentMatch.index).split('\n').length;
|
|
1456
|
+
const commentText = commentMatch[0].substring(0, 50);
|
|
1457
|
+
pushFinding(
|
|
1458
|
+
"frontend.code_quality.comment",
|
|
1459
|
+
"medium",
|
|
1460
|
+
sf,
|
|
1461
|
+
sf,
|
|
1462
|
+
`Line ${lineNumber}: Comment '${commentText}...' - refactor to self-descriptive code (No comments rule)`,
|
|
1463
|
+
findings
|
|
1464
|
+
);
|
|
1465
|
+
commentCount++;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
const nestedIfPattern = /if\s*\([^)]+\)\s*\{[^}]*if\s*\([^)]+\)\s*\{[^}]*if\s*\([^)]+\)\s*\{/g;
|
|
1469
|
+
if (nestedIfPattern.test(content)) {
|
|
1470
|
+
pushFinding(
|
|
1471
|
+
"frontend.code_quality.nested_conditionals",
|
|
1472
|
+
"high",
|
|
1473
|
+
sf,
|
|
1474
|
+
sf,
|
|
1475
|
+
'Deeply nested if statements - use early returns/guard clauses (Clean Code: reduce cognitive complexity)',
|
|
1476
|
+
findings
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// Magic numbers
|
|
1481
|
+
const magicNumberPattern = /[^a-zA-Z0-9_]\d{3,}(?!\s*px|ms|rem|em|%|vh|vw)/g;
|
|
1482
|
+
let magicCount = 0;
|
|
1483
|
+
let magicMatch;
|
|
1484
|
+
|
|
1485
|
+
const isJSXFile = /\.(tsx|jsx)$/i.test(filePath);
|
|
1486
|
+
const isJSXLikeContent = /<\s*[A-Za-z][A-Za-z0-9-]*/.test(content) && (content.includes('return') || content.includes('React'));
|
|
1487
|
+
const isReactUIFile = isJSXFile || isJSXLikeContent;
|
|
1488
|
+
const isInternalAstToolingFile = filePath.includes('/infrastructure/ast/') || filePath.includes('/scripts/hooks-system/');
|
|
1489
|
+
|
|
1490
|
+
while ((magicMatch = magicNumberPattern.exec(content)) !== null && magicCount < 5) {
|
|
1491
|
+
const number = magicMatch[0].trim();
|
|
1492
|
+
const lineNumber = content.substring(0, magicMatch.index).split('\n').length;
|
|
1493
|
+
|
|
1494
|
+
if (!['1000', '2000', '3000', '5000', '10000'].includes(number.trim()) || content.substring(magicMatch.index - 20, magicMatch.index).includes('Date')) {
|
|
1495
|
+
pushFinding(
|
|
1496
|
+
"frontend.code_quality.magic_number",
|
|
1497
|
+
"low",
|
|
1498
|
+
sf,
|
|
1499
|
+
sf,
|
|
1500
|
+
`Line ${lineNumber}: Magic number ${number} - use named constant`,
|
|
1501
|
+
findings
|
|
1502
|
+
);
|
|
1503
|
+
magicCount++;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
const callbackPattern = /\([^)]*\)\s*=>\s*\{[^}]*\([^)]*\)\s*=>\s*\{[^}]*\([^)]*\)\s*=>\s*\{/g;
|
|
1508
|
+
if (isReactUIFile && !isInternalAstToolingFile && callbackPattern.test(content)) {
|
|
1509
|
+
pushFinding(
|
|
1510
|
+
"frontend.code_quality.callback_hell",
|
|
1511
|
+
"high",
|
|
1512
|
+
sf,
|
|
1513
|
+
sf,
|
|
1514
|
+
'Deeply nested callbacks - use async/await or extract functions (Clean Code: flatten structure)',
|
|
1515
|
+
findings
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// ==========================================
|
|
1520
|
+
// ==========================================
|
|
1521
|
+
|
|
1522
|
+
if ((filePath.includes('/models/') || filePath.includes('/views/') || filePath.includes('/controllers/')) &&
|
|
1523
|
+
!filePath.includes('/features/') && !filePath.includes('/domain/')) {
|
|
1524
|
+
pushFinding(
|
|
1525
|
+
"frontend.ddd.technical_grouping",
|
|
1526
|
+
"low",
|
|
1527
|
+
sf,
|
|
1528
|
+
sf,
|
|
1529
|
+
'Technical grouping (models/views/controllers) - consider feature-first organization (DDD: organize by domain/feature)',
|
|
1530
|
+
findings
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
if (filePath.includes('/domain/') || filePath.includes('/entities/')) {
|
|
1535
|
+
const hasClass = content.includes('class ');
|
|
1536
|
+
const hasInterface = content.includes('interface ') || content.includes('type ');
|
|
1537
|
+
const hasMethods = content.includes('() {') || content.includes('function');
|
|
1538
|
+
|
|
1539
|
+
if ((hasClass || hasInterface) && !hasMethods) {
|
|
1540
|
+
pushFinding(
|
|
1541
|
+
"frontend.ddd.anemic_model",
|
|
1542
|
+
"medium",
|
|
1543
|
+
sf,
|
|
1544
|
+
sf,
|
|
1545
|
+
'Entity/Domain model with no behavior - add domain logic methods (DDD: rich domain models)',
|
|
1546
|
+
findings
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// ==========================================
|
|
1552
|
+
// ==========================================
|
|
1553
|
+
|
|
1554
|
+
if (content.includes('dangerouslySetInnerHTML')) {
|
|
1555
|
+
const hasDOMPurify = content.includes('DOMPurify') || content.includes('sanitize');
|
|
1556
|
+
|
|
1557
|
+
if (!hasDOMPurify) {
|
|
1558
|
+
pushFinding(
|
|
1559
|
+
"frontend.security.xss_danger",
|
|
1560
|
+
"critical",
|
|
1561
|
+
sf,
|
|
1562
|
+
sf,
|
|
1563
|
+
'🚨 CRITICAL XSS Risk: dangerouslySetInnerHTML without DOMPurify sanitization. Install DOMPurify: npm i dompurify; Use: dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(html)}}. Prevents: Script injection, data theft, account hijacking',
|
|
1564
|
+
findings
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
sf.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((call) => {
|
|
1570
|
+
const callText = call.getText();
|
|
1571
|
+
|
|
1572
|
+
if (callText.startsWith('useEffect(')) {
|
|
1573
|
+
const effectBody = call.getArguments()[0]?.getText() || '';
|
|
1574
|
+
|
|
1575
|
+
const hasSubscription = effectBody.includes('.subscribe') ||
|
|
1576
|
+
effectBody.includes('addEventListener') ||
|
|
1577
|
+
effectBody.includes('setInterval') ||
|
|
1578
|
+
effectBody.includes('setTimeout') ||
|
|
1579
|
+
effectBody.includes('WebSocket') ||
|
|
1580
|
+
effectBody.includes('.on(');
|
|
1581
|
+
|
|
1582
|
+
const hasCleanup = effectBody.includes('return () =>') ||
|
|
1583
|
+
effectBody.includes('return function');
|
|
1584
|
+
|
|
1585
|
+
if (hasSubscription && !hasCleanup) {
|
|
1586
|
+
pushFinding(
|
|
1587
|
+
"frontend.hooks.useeffect_cleanup",
|
|
1588
|
+
"critical",
|
|
1589
|
+
sf,
|
|
1590
|
+
call,
|
|
1591
|
+
'🚨 CRITICAL Memory Leak: useEffect with subscription/listener missing cleanup function. Add: return () => { subscription.unsubscribe(); removeEventListener(); clearInterval(); }. Prevents: Memory leaks, zombie listeners, multiple subscriptions',
|
|
1592
|
+
findings
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
// 3. any Type Forbidden
|
|
1599
|
+
sf.getDescendantsOfKind(SyntaxKind.AnyKeyword).forEach((anyNode) => {
|
|
1600
|
+
const parent = anyNode.getParent();
|
|
1601
|
+
const parentText = parent?.getText() || '';
|
|
1602
|
+
|
|
1603
|
+
const isThirdPartyType = parentText.includes('@types/') || parentText.includes('declare module');
|
|
1604
|
+
|
|
1605
|
+
if (!isThirdPartyType) {
|
|
1606
|
+
pushFinding(
|
|
1607
|
+
"frontend.typescript.any_type_forbidden",
|
|
1608
|
+
"critical",
|
|
1609
|
+
sf,
|
|
1610
|
+
anyNode,
|
|
1611
|
+
'🚨 CRITICAL Type Safety: any type forbidden. Use unknown + type guard, or specific union type. Example: function process(data: unknown) { if (isValidData(data)) { /* typed */ } }. Prevents: Runtime errors, loss of IntelliSense, bugs in production',
|
|
1612
|
+
findings
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
sf.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((call) => {
|
|
1618
|
+
const callText = call.getText();
|
|
1619
|
+
|
|
1620
|
+
if (callText.startsWith('useCallback(')) {
|
|
1621
|
+
const args = call.getArguments();
|
|
1622
|
+
const callback = args[0]?.getText() || '';
|
|
1623
|
+
const depsArray = args[1]?.getText() || '';
|
|
1624
|
+
|
|
1625
|
+
const usedVariables = callback.match(/\b[a-z]\w+/gi) || [];
|
|
1626
|
+
const declaredDeps = depsArray.match(/\b[a-z]\w+/gi) || [];
|
|
1627
|
+
|
|
1628
|
+
const suspiciousMissing = usedVariables.filter(v =>
|
|
1629
|
+
!['console', 'window', 'document', 'Math', 'Date', 'JSON', 'Promise', 'Array', 'Object', 'String', 'Number', 'Boolean'].includes(v) &&
|
|
1630
|
+
!declaredDeps.includes(v) &&
|
|
1631
|
+
v.length > 2
|
|
1632
|
+
);
|
|
1633
|
+
|
|
1634
|
+
if (suspiciousMissing.length > 0 && !depsArray.includes('eslint-disable')) {
|
|
1635
|
+
pushFinding(
|
|
1636
|
+
"frontend.hooks.usecallback_deps",
|
|
1637
|
+
"critical",
|
|
1638
|
+
sf,
|
|
1639
|
+
call,
|
|
1640
|
+
`🚨 CRITICAL Stale Closure: useCallback possibly missing dependencies: [${suspiciousMissing.join(', ')}]. Add to dependency array or verify. Prevents: Stale closures, incorrect behavior, hard-to-debug bugs. See: https://react.dev/reference/react/useCallback#my-callback-runs-too-often`,
|
|
1641
|
+
findings
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
|
|
1647
|
+
const functionComponents = sf.getDescendantsOfKind(SyntaxKind.FunctionDeclaration)
|
|
1648
|
+
.concat(sf.getDescendantsOfKind(SyntaxKind.ArrowFunction))
|
|
1649
|
+
.filter(fn => {
|
|
1650
|
+
const name = fn.getName?.() || fn.getParent()?.getText() || '';
|
|
1651
|
+
return /^[A-Z]/.test(name) || fn.getText().includes('use');
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1654
|
+
functionComponents.forEach(comp => {
|
|
1655
|
+
const statements = comp.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
1656
|
+
let foundConditional = false;
|
|
1657
|
+
let foundHookAfterConditional = false;
|
|
1658
|
+
|
|
1659
|
+
statements.forEach(stmt => {
|
|
1660
|
+
const stmtText = stmt.getText();
|
|
1661
|
+
const isConditional = stmt.getParent()?.getKind() === SyntaxKind.IfStatement ||
|
|
1662
|
+
stmt.getParent()?.getKind() === SyntaxKind.ConditionalExpression;
|
|
1663
|
+
|
|
1664
|
+
const isHook = /^use[A-Z]/.test(stmtText);
|
|
1665
|
+
|
|
1666
|
+
if (isConditional) foundConditional = true;
|
|
1667
|
+
if (foundConditional && isHook) foundHookAfterConditional = true;
|
|
1668
|
+
});
|
|
1669
|
+
|
|
1670
|
+
if (foundHookAfterConditional) {
|
|
1671
|
+
pushFinding(
|
|
1672
|
+
"frontend.hooks.hook_call_order",
|
|
1673
|
+
"critical",
|
|
1674
|
+
sf,
|
|
1675
|
+
comp,
|
|
1676
|
+
'🚨 CRITICAL React Rules Violation: Hook called after conditional/loop. Hooks MUST be at top level. Move hooks before any conditionals. Prevents: Rules of Hooks violation, inconsistent renders, React crash. See: https://react.dev/reference/rules/rules-of-hooks',
|
|
1677
|
+
findings
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
const tokenInURLPattern = /(\?|&)(token|auth|key|apikey|api_key|access_token)=[a-zA-Z0-9_\-\.]+/gi;
|
|
1683
|
+
let tokenMatch;
|
|
1684
|
+
|
|
1685
|
+
while ((tokenMatch = tokenInURLPattern.exec(content)) !== null) {
|
|
1686
|
+
const lineNumber = content.substring(0, tokenMatch.index).split('\n').length;
|
|
1687
|
+
|
|
1688
|
+
pushFinding(
|
|
1689
|
+
"frontend.security.token_in_url",
|
|
1690
|
+
"critical",
|
|
1691
|
+
sf,
|
|
1692
|
+
sf,
|
|
1693
|
+
`🚨 CRITICAL Security: Auth token in URL query params (line ${lineNumber}). URLs are logged, cached, shared. Use Authorization header instead: headers: { 'Authorization': \`Bearer \${token}\` }. Prevents: Token theft, session hijacking, compliance violations`,
|
|
1694
|
+
findings
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
if (filePath.includes('/app/layout') || filePath.includes('/middleware')) {
|
|
1699
|
+
const hasCSP = content.includes('Content-Security-Policy') ||
|
|
1700
|
+
content.includes('contentSecurityPolicy');
|
|
1701
|
+
|
|
1702
|
+
if (!hasCSP && !content.includes('TODO: CSP')) {
|
|
1703
|
+
pushFinding(
|
|
1704
|
+
"frontend.security.missing_csp_headers",
|
|
1705
|
+
"critical",
|
|
1706
|
+
sf,
|
|
1707
|
+
sf,
|
|
1708
|
+
'🚨 CRITICAL Security: Missing Content-Security-Policy headers. Add to Next.js config or middleware. Example: "Content-Security-Policy": "default-src \'self\'; script-src \'self\' \'unsafe-inline\'; style-src \'self\' \'unsafe-inline\'". Prevents: XSS, clickjacking, code injection, data exfiltration',
|
|
1709
|
+
findings
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// ==========================================
|
|
1715
|
+
// ==========================================
|
|
1716
|
+
|
|
1717
|
+
const hasUseClientDirective = content.includes("'use client'") || content.includes('"use client"');
|
|
1718
|
+
const hasHooks = content.includes('useState') || content.includes('useEffect');
|
|
1719
|
+
const hasEventHandlers = content.includes('onClick') || content.includes('onChange');
|
|
1720
|
+
|
|
1721
|
+
if (hasUseClientDirective && !hasHooks && !hasEventHandlers && !filePath.includes('/components/ui/')) {
|
|
1722
|
+
pushFinding(
|
|
1723
|
+
"frontend.nextjs.unnecessary_use_client",
|
|
1724
|
+
"high",
|
|
1725
|
+
sf,
|
|
1726
|
+
sf,
|
|
1727
|
+
'🚨 HIGH: Unnecessary "use client" directive. Server Components by default (Next.js 15). Only add "use client" when using: useState, useEffect, onClick, browser APIs. Benefit: Smaller bundle, faster load, better SEO',
|
|
1728
|
+
findings
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
if (content.includes('React.memo(') || content.includes('memo(')) {
|
|
1733
|
+
const hasMemoJustification = content.includes('// memo:') || content.includes('/* memo:');
|
|
1734
|
+
|
|
1735
|
+
if (!hasMemoJustification) {
|
|
1736
|
+
pushFinding(
|
|
1737
|
+
"frontend.performance.memo_without_justification",
|
|
1738
|
+
"high",
|
|
1739
|
+
sf,
|
|
1740
|
+
sf,
|
|
1741
|
+
'🚨 HIGH: React.memo without justification. Premature optimization. Add comment: // memo: Prevents re-render when X changes. Only use if profiled performance issue. Most components don\'t need memo.',
|
|
1742
|
+
findings
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
if ((content.includes('<img ') || content.includes('<img>')) && !content.includes('<Image')) {
|
|
1748
|
+
pushFinding(
|
|
1749
|
+
"frontend.nextjs.missing_next_image",
|
|
1750
|
+
"high",
|
|
1751
|
+
sf,
|
|
1752
|
+
sf,
|
|
1753
|
+
'🚨 HIGH: Use Next/Image instead of <img>. Import: import Image from \'next/image\'. Benefits: Automatic optimization, lazy loading, WebP format, responsive sizes, CLS prevention. Performance: 3x faster load',
|
|
1754
|
+
findings
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
const divSoupPattern = /<div[^>]*>\\s*<div[^>]*>\\s*<div[^>]*>\\s*<div/g;
|
|
1759
|
+
if (divSoupPattern.test(content)) {
|
|
1760
|
+
pushFinding(
|
|
1761
|
+
"frontend.accessibility.semantic_html",
|
|
1762
|
+
"high",
|
|
1763
|
+
sf,
|
|
1764
|
+
sf,
|
|
1765
|
+
'🚨 HIGH: Div soup detected (4+ nested divs). Use semantic HTML: <header>, <nav>, <main>, <section>, <article>, <aside>, <footer>. Benefits: SEO, accessibility, screen readers',
|
|
1766
|
+
findings
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
const inputWithoutLabelPattern = /<input[^>]*(?!aria-label|aria-labelledby)[^>]*>/g;
|
|
1771
|
+
let inputMatch;
|
|
1772
|
+
let inputCount = 0;
|
|
1773
|
+
|
|
1774
|
+
while ((inputMatch = inputWithoutLabelPattern.exec(content)) !== null && inputCount < 3) {
|
|
1775
|
+
const lineNumber = content.substring(0, inputMatch.index).split('\\n').length;
|
|
1776
|
+
pushFinding(
|
|
1777
|
+
"frontend.accessibility.input_label",
|
|
1778
|
+
"high",
|
|
1779
|
+
sf,
|
|
1780
|
+
sf,
|
|
1781
|
+
`🚨 HIGH: Input without label (line ${lineNumber}). Add aria-label or associated <label>. Screen readers need text description. Example: <input aria-label="Email address" /> or <label htmlFor="email">Email</label><input id="email" />`,
|
|
1782
|
+
findings
|
|
1783
|
+
);
|
|
1784
|
+
inputCount++;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
if (content.includes('onClick') && !content.includes('onKeyDown') && !content.includes('onKeyPress')) {
|
|
1788
|
+
pushFinding(
|
|
1789
|
+
"frontend.accessibility.keyboard_navigation",
|
|
1790
|
+
"high",
|
|
1791
|
+
sf,
|
|
1792
|
+
sf,
|
|
1793
|
+
'🚨 HIGH: onClick without keyboard handler. Add onKeyDown={(e) => e.key === \'Enter\' && handleClick()}. Keyboard-only users need access. Or use <button> (has keyboard support).',
|
|
1794
|
+
findings
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
const largeListPattern = /\{.*\.map\(.*=>.*\).*\}/s;
|
|
1799
|
+
if (isReactUIFile && !isInternalAstToolingFile && largeListPattern.test(content) && content.includes('.map(') && !content.includes('react-window') && !content.includes('react-virtualized')) {
|
|
1800
|
+
const mapCount = (content.match(/\.map\(/g) || []).length;
|
|
1801
|
+
|
|
1802
|
+
if (mapCount > 2) {
|
|
1803
|
+
pushFinding(
|
|
1804
|
+
"frontend.performance.missing_virtualization",
|
|
1805
|
+
"high",
|
|
1806
|
+
sf,
|
|
1807
|
+
sf,
|
|
1808
|
+
'🚨 HIGH: Large list without virtualization. For 100+ items use react-window or react-virtualized. Example: <FixedSizeList itemCount={items.length} itemSize={50} height={600} />. Performance: Renders only visible items.',
|
|
1809
|
+
findings
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
if (isReactUIFile && !isInternalAstToolingFile && (content.includes('modal') || content.includes('Modal') || content.includes('dialog')) && !content.includes('focus') && !content.includes('ref')) {
|
|
1815
|
+
pushFinding(
|
|
1816
|
+
"frontend.accessibility.focus_trap",
|
|
1817
|
+
"high",
|
|
1818
|
+
sf,
|
|
1819
|
+
sf,
|
|
1820
|
+
'🚨 HIGH: Modal without focus management. Install: npm i focus-trap-react. Trap focus inside modal: <FocusTrap><Modal>...</Modal></FocusTrap>. Prevents keyboard users escaping modal.',
|
|
1821
|
+
findings
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
const useStateCallsInComponent = sf.getDescendantsOfKind(SyntaxKind.CallExpression)
|
|
1826
|
+
.filter(call => call.getExpression().getText() === 'useState');
|
|
1827
|
+
|
|
1828
|
+
sf.getDescendantsOfKind(SyntaxKind.FunctionDeclaration).forEach(fn => {
|
|
1829
|
+
const name = fn.getName();
|
|
1830
|
+
if (name && /^[A-Z]/.test(name)) {
|
|
1831
|
+
const useStatesInFn = useStateCallsInComponent.filter(call =>
|
|
1832
|
+
fn.getDescendants().includes(call)
|
|
1833
|
+
);
|
|
1834
|
+
|
|
1835
|
+
if (useStatesInFn.length >= 4) {
|
|
1836
|
+
pushFinding(
|
|
1837
|
+
"frontend.hooks.useState_overuse",
|
|
1838
|
+
"medium",
|
|
1839
|
+
sf,
|
|
1840
|
+
fn,
|
|
1841
|
+
`Component ${name} has ${useStatesInFn.length}+ useState. Consider useReducer for complex state. Benefits: Single state object, predictable updates, easier testing.`,
|
|
1842
|
+
findings
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
});
|
|
1847
|
+
|
|
1848
|
+
const contextCreations = sf.getDescendantsOfKind(SyntaxKind.CallExpression)
|
|
1849
|
+
.filter(call => call.getExpression().getText() === 'createContext');
|
|
1850
|
+
|
|
1851
|
+
if (contextCreations.length >= 3 && !sf.getFilePath().includes('context')) {
|
|
1852
|
+
pushFinding(
|
|
1853
|
+
"frontend.state.context_overuse",
|
|
1854
|
+
"medium",
|
|
1855
|
+
sf,
|
|
1856
|
+
sf,
|
|
1857
|
+
`Multiple Context creations (${contextCreations.length}). Consider Zustand for global state. Context rerenders all consumers. Zustand: Selective subscriptions, better performance.`,
|
|
1858
|
+
findings
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
sf.getDescendantsOfKind(SyntaxKind.FunctionDeclaration).forEach(fn => {
|
|
1863
|
+
const name = fn.getName();
|
|
1864
|
+
const body = fn.getBody()?.getText() || '';
|
|
1865
|
+
const hasHookCall = /use[A-Z]/.test(body);
|
|
1866
|
+
|
|
1867
|
+
if (name && hasHookCall && !/^use[A-Z]/.test(name)) {
|
|
1868
|
+
pushFinding(
|
|
1869
|
+
"frontend.hooks.naming_convention",
|
|
1870
|
+
"medium",
|
|
1871
|
+
sf,
|
|
1872
|
+
fn,
|
|
1873
|
+
`Function ${name} uses hooks but doesn't start with 'use'. Rename to use${name[0].toUpperCase()}${name.slice(1)}. React ESLint requires 'use' prefix for hook functions.`,
|
|
1874
|
+
findings
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
const fullText = sf.getFullText();
|
|
1880
|
+
const hasLargeImports = fullText.includes('import * as') ||
|
|
1881
|
+
fullText.match(/import\s+\{[^}]{200,}\}/);
|
|
1882
|
+
|
|
1883
|
+
if (hasLargeImports) {
|
|
1884
|
+
pushFinding(
|
|
1885
|
+
"frontend.performance.large_imports",
|
|
1886
|
+
"medium",
|
|
1887
|
+
sf,
|
|
1888
|
+
sf,
|
|
1889
|
+
'Large import detected. Use tree-shaking: import { Button } from \'@mui/material/Button\' (not from \'@mui/material\'). Reduces bundle size by importing only needed components.',
|
|
1890
|
+
findings
|
|
1891
|
+
);
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
if (content.includes('class') && content.includes('extends Component') &&
|
|
1895
|
+
!content.includes('componentDidCatch') && !content.includes('getDerivedStateFromError')) {
|
|
1896
|
+
|
|
1897
|
+
const hasChildrenRender = content.includes('render()') && content.includes('children');
|
|
1898
|
+
|
|
1899
|
+
if (hasChildrenRender) {
|
|
1900
|
+
pushFinding(
|
|
1901
|
+
"frontend.error_handling.missing_error_boundary",
|
|
1902
|
+
"medium",
|
|
1903
|
+
sf,
|
|
1904
|
+
sf,
|
|
1905
|
+
'Component renders children without error boundary. Add: componentDidCatch(error, errorInfo) { logError(error); }. Prevents whole app crash from single component error.',
|
|
1906
|
+
findings
|
|
1907
|
+
);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
const hasRoutes = content.includes('Route') || content.includes('router');
|
|
1912
|
+
const hasLazy = content.includes('React.lazy') || content.includes('dynamic(');
|
|
1913
|
+
|
|
1914
|
+
if (hasRoutes && !hasLazy && sf.getFilePath().includes('app')) {
|
|
1915
|
+
pushFinding(
|
|
1916
|
+
"frontend.performance.missing_lazy_loading",
|
|
1917
|
+
"medium",
|
|
1918
|
+
sf,
|
|
1919
|
+
sf,
|
|
1920
|
+
'Routes without lazy loading. Use: const Dashboard = lazy(() => import(\'./Dashboard\')). Wrap with <Suspense>. Reduces initial bundle, faster First Contentful Paint.',
|
|
1921
|
+
findings
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
|
|
1926
|
+
if (sf.getFilePath().includes('layout.tsx') || sf.getFilePath().includes('page.tsx')) {
|
|
1927
|
+
const hasMetadata = content.includes('metadata') || content.includes('generateMetadata');
|
|
1928
|
+
|
|
1929
|
+
if (!hasMetadata) {
|
|
1930
|
+
pushFinding(
|
|
1931
|
+
"frontend.seo.missing_metadata",
|
|
1932
|
+
"low",
|
|
1933
|
+
sf,
|
|
1934
|
+
sf,
|
|
1935
|
+
'Missing SEO metadata. Export: export const metadata = { title, description, openGraph }. Improves Google ranking, social media previews.',
|
|
1936
|
+
findings
|
|
1937
|
+
);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
if (sf.getFilePath().includes('sitemap')) {
|
|
1942
|
+
const hasDynamicRoutes = content.includes('fetch') || content.includes('database');
|
|
1943
|
+
|
|
1944
|
+
if (!hasDynamicRoutes && content.length < 200) {
|
|
1945
|
+
pushFinding(
|
|
1946
|
+
"frontend.seo.static_sitemap",
|
|
1947
|
+
"low",
|
|
1948
|
+
sf,
|
|
1949
|
+
sf,
|
|
1950
|
+
'Static sitemap. Generate dynamically: fetch all routes from API/DB. Update automatically when content changes. Better SEO indexing.',
|
|
1951
|
+
findings
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
if (sf.getFilePath().includes('robots')) {
|
|
1957
|
+
const hasDisallow = content.includes('Disallow');
|
|
1958
|
+
|
|
1959
|
+
if (!hasDisallow) {
|
|
1960
|
+
pushFinding(
|
|
1961
|
+
"frontend.seo.robots_config",
|
|
1962
|
+
"low",
|
|
1963
|
+
sf,
|
|
1964
|
+
sf,
|
|
1965
|
+
'Robots.txt incomplete. Add Disallow rules for: /api, /admin, /_next. Prevents search engines indexing private routes.',
|
|
1966
|
+
findings
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
if (sf.getFilePath().includes('manifest')) {
|
|
1972
|
+
const hasIcons = content.includes('icons');
|
|
1973
|
+
const hasThemeColor = content.includes('theme_color');
|
|
1974
|
+
|
|
1975
|
+
if (!hasIcons || !hasThemeColor) {
|
|
1976
|
+
pushFinding(
|
|
1977
|
+
"frontend.pwa.manifest_incomplete",
|
|
1978
|
+
"low",
|
|
1979
|
+
sf,
|
|
1980
|
+
sf,
|
|
1981
|
+
'PWA manifest incomplete. Add: icons (192x192, 512x512), theme_color, background_color, display: standalone. Enables Add to Home Screen.',
|
|
1982
|
+
findings
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
if (content.includes('serviceWorker') && !content.includes('unregister')) {
|
|
1988
|
+
pushFinding(
|
|
1989
|
+
"frontend.pwa.missing_sw_unregister",
|
|
1990
|
+
"low",
|
|
1991
|
+
sf,
|
|
1992
|
+
sf,
|
|
1993
|
+
'Service Worker registration without unregister fallback. Add: if (!production) navigator.serviceWorker.unregister(). Prevents caching issues in development.',
|
|
1994
|
+
findings
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
if (sf.getFilePath().includes('lighthouse') || sf.getFilePath().includes('performance')) {
|
|
1999
|
+
const hasThresholds = content.includes('performance') && content.includes('accessibility');
|
|
2000
|
+
|
|
2001
|
+
if (!hasThresholds) {
|
|
2002
|
+
pushFinding(
|
|
2003
|
+
"frontend.performance.lighthouse_monitoring",
|
|
2004
|
+
"low",
|
|
2005
|
+
sf,
|
|
2006
|
+
sf,
|
|
2007
|
+
'Lighthouse config without thresholds. Set CI thresholds: performance: 90, accessibility: 95, best-practices: 90, seo: 90. Prevents performance regressions.',
|
|
2008
|
+
findings
|
|
2009
|
+
);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
const hasWebVitals = content.includes('reportWebVitals') || content.includes('web-vitals');
|
|
2014
|
+
const hasAnalytics = content.includes('analytics') || content.includes('gtag');
|
|
2015
|
+
|
|
2016
|
+
if (hasWebVitals && !hasAnalytics) {
|
|
2017
|
+
pushFinding(
|
|
2018
|
+
"frontend.monitoring.web_vitals_no_analytics",
|
|
2019
|
+
"low",
|
|
2020
|
+
sf,
|
|
2021
|
+
sf,
|
|
2022
|
+
'Web Vitals captured but not sent to analytics. Send to Google Analytics: gtag(\'event\', metric.name, { value: metric.value }). Track LCP, FID, CLS in production.',
|
|
2023
|
+
findings
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
if (sf.getFilePath().includes('layout') && !content.includes('analytics') && !content.includes('gtag')) {
|
|
2028
|
+
pushFinding(
|
|
2029
|
+
"frontend.monitoring.missing_analytics",
|
|
2030
|
+
"low",
|
|
2031
|
+
sf,
|
|
2032
|
+
sf,
|
|
2033
|
+
'Root layout without analytics. Add Google Analytics: <Script src="https://www.googletagmanager.com/gtag/js" />. Track user behavior, conversion rates.',
|
|
2034
|
+
findings
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
if (sf.getFilePath().includes('error.tsx') && !content.includes('Sentry') && !content.includes('captureException')) {
|
|
2039
|
+
pushFinding(
|
|
2040
|
+
"frontend.monitoring.missing_error_tracking",
|
|
2041
|
+
"low",
|
|
2042
|
+
sf,
|
|
2043
|
+
sf,
|
|
2044
|
+
'Error page without error tracking. Install: npm i @sentry/nextjs. Track production errors: Sentry.captureException(error). Get alerts when users hit errors.',
|
|
2045
|
+
findings
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
if (content.includes('if (') && /feature|experiment|rollout|beta/i.test(content)) {
|
|
2050
|
+
const hasFeatureFlagLib = content.includes('unleash') || content.includes('launchdarkly') || content.includes('flagsmith');
|
|
2051
|
+
|
|
2052
|
+
if (!hasFeatureFlagLib) {
|
|
2053
|
+
pushFinding(
|
|
2054
|
+
"frontend.devops.hardcoded_feature_flag",
|
|
2055
|
+
"low",
|
|
2056
|
+
sf,
|
|
2057
|
+
sf,
|
|
2058
|
+
'Hardcoded feature flag. Use feature flag service: Unleash, LaunchDarkly, Flagsmith. Benefits: Toggle features without deployment, gradual rollout, A/B testing.',
|
|
2059
|
+
findings
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
if (content.includes('variant') && content.includes('Math.random')) {
|
|
2065
|
+
pushFinding(
|
|
2066
|
+
"frontend.devops.manual_ab_testing",
|
|
2067
|
+
"low",
|
|
2068
|
+
sf,
|
|
2069
|
+
sf,
|
|
2070
|
+
'Manual A/B testing with Math.random. Use: Google Optimize, Optimizely, VWO. Benefits: Statistical significance, reporting, targeting rules.',
|
|
2071
|
+
findings
|
|
2072
|
+
);
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
if (content.includes('fetch') && !content.includes('cache') && !content.includes('revalidate')) {
|
|
2076
|
+
const isFetchInServerComponent = content.includes('async function') && !content.includes('\'use client\'');
|
|
2077
|
+
|
|
2078
|
+
if (isFetchInServerComponent) {
|
|
2079
|
+
pushFinding(
|
|
2080
|
+
"frontend.performance.missing_cache_strategy",
|
|
2081
|
+
"low",
|
|
2082
|
+
sf,
|
|
2083
|
+
sf,
|
|
2084
|
+
'Fetch without cache strategy. Add: { cache: \'force-cache\' } or { next: { revalidate: 3600 } }. Default behavior may change. Be explicit.',
|
|
2085
|
+
findings
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
module.exports = {
|
|
2093
|
+
runFrontendIntelligence,
|
|
2094
|
+
};
|