codeguardian-mcp 1.0.0
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 +348 -0
- package/dist/agent/agentTools.d.ts +26 -0
- package/dist/agent/agentTools.d.ts.map +1 -0
- package/dist/agent/agentTools.js +699 -0
- package/dist/agent/agentTools.js.map +1 -0
- package/dist/agent/autoValidator.d.ts +110 -0
- package/dist/agent/autoValidator.d.ts.map +1 -0
- package/dist/agent/autoValidator.js +964 -0
- package/dist/agent/autoValidator.js.map +1 -0
- package/dist/agent/fileWatcher.d.ts +28 -0
- package/dist/agent/fileWatcher.d.ts.map +1 -0
- package/dist/agent/fileWatcher.js +88 -0
- package/dist/agent/fileWatcher.js.map +1 -0
- package/dist/agent/guardianPersistence.d.ts +98 -0
- package/dist/agent/guardianPersistence.d.ts.map +1 -0
- package/dist/agent/guardianPersistence.js +296 -0
- package/dist/agent/guardianPersistence.js.map +1 -0
- package/dist/agent/mcpNotifications.d.ts +38 -0
- package/dist/agent/mcpNotifications.d.ts.map +1 -0
- package/dist/agent/mcpNotifications.js +81 -0
- package/dist/agent/mcpNotifications.js.map +1 -0
- package/dist/analyzers/aiPatterns.d.ts +16 -0
- package/dist/analyzers/aiPatterns.d.ts.map +1 -0
- package/dist/analyzers/aiPatterns.js +103 -0
- package/dist/analyzers/aiPatterns.js.map +1 -0
- package/dist/analyzers/antiPatterns.d.ts +60 -0
- package/dist/analyzers/antiPatterns.d.ts.map +1 -0
- package/dist/analyzers/antiPatterns.js +198 -0
- package/dist/analyzers/antiPatterns.js.map +1 -0
- package/dist/analyzers/builtinTypes.d.ts +18 -0
- package/dist/analyzers/builtinTypes.d.ts.map +1 -0
- package/dist/analyzers/builtinTypes.js +1275 -0
- package/dist/analyzers/builtinTypes.js.map +1 -0
- package/dist/analyzers/complexity.d.ts +14 -0
- package/dist/analyzers/complexity.d.ts.map +1 -0
- package/dist/analyzers/complexity.js +610 -0
- package/dist/analyzers/complexity.js.map +1 -0
- package/dist/analyzers/findingVerifier.d.ts +59 -0
- package/dist/analyzers/findingVerifier.d.ts.map +1 -0
- package/dist/analyzers/findingVerifier.js +1169 -0
- package/dist/analyzers/findingVerifier.js.map +1 -0
- package/dist/analyzers/impactAnalyzer.d.ts +53 -0
- package/dist/analyzers/impactAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/impactAnalyzer.js +152 -0
- package/dist/analyzers/impactAnalyzer.js.map +1 -0
- package/dist/analyzers/languageDetector.d.ts +48 -0
- package/dist/analyzers/languageDetector.d.ts.map +1 -0
- package/dist/analyzers/languageDetector.js +404 -0
- package/dist/analyzers/languageDetector.js.map +1 -0
- package/dist/analyzers/parsers/incrementalParser.d.ts +53 -0
- package/dist/analyzers/parsers/incrementalParser.d.ts.map +1 -0
- package/dist/analyzers/parsers/incrementalParser.js +193 -0
- package/dist/analyzers/parsers/incrementalParser.js.map +1 -0
- package/dist/analyzers/parsers/scopeResolver.d.ts +92 -0
- package/dist/analyzers/parsers/scopeResolver.d.ts.map +1 -0
- package/dist/analyzers/parsers/scopeResolver.js +324 -0
- package/dist/analyzers/parsers/scopeResolver.js.map +1 -0
- package/dist/analyzers/parsers/semanticIndex.d.ts +127 -0
- package/dist/analyzers/parsers/semanticIndex.d.ts.map +1 -0
- package/dist/analyzers/parsers/semanticIndex.js +429 -0
- package/dist/analyzers/parsers/semanticIndex.js.map +1 -0
- package/dist/analyzers/parsers/sessionDiffAnalyzer.d.ts +42 -0
- package/dist/analyzers/parsers/sessionDiffAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/parsers/sessionDiffAnalyzer.js +233 -0
- package/dist/analyzers/parsers/sessionDiffAnalyzer.js.map +1 -0
- package/dist/analyzers/parsers/treeSitterParser.d.ts +76 -0
- package/dist/analyzers/parsers/treeSitterParser.d.ts.map +1 -0
- package/dist/analyzers/parsers/treeSitterParser.js +709 -0
- package/dist/analyzers/parsers/treeSitterParser.js.map +1 -0
- package/dist/analyzers/relevanceScorer.d.ts +43 -0
- package/dist/analyzers/relevanceScorer.d.ts.map +1 -0
- package/dist/analyzers/relevanceScorer.js +200 -0
- package/dist/analyzers/relevanceScorer.js.map +1 -0
- package/dist/analyzers/standardLibrary.d.ts +22 -0
- package/dist/analyzers/standardLibrary.d.ts.map +1 -0
- package/dist/analyzers/standardLibrary.js +211 -0
- package/dist/analyzers/standardLibrary.js.map +1 -0
- package/dist/analyzers/symbolGraph.d.ts +30 -0
- package/dist/analyzers/symbolGraph.d.ts.map +1 -0
- package/dist/analyzers/symbolGraph.js +380 -0
- package/dist/analyzers/symbolGraph.js.map +1 -0
- package/dist/analyzers/symbolTable.d.ts +18 -0
- package/dist/analyzers/symbolTable.d.ts.map +1 -0
- package/dist/analyzers/symbolTable.js +176 -0
- package/dist/analyzers/symbolTable.js.map +1 -0
- package/dist/analyzers/typeChecker.d.ts +13 -0
- package/dist/analyzers/typeChecker.d.ts.map +1 -0
- package/dist/analyzers/typeChecker.js +580 -0
- package/dist/analyzers/typeChecker.js.map +1 -0
- package/dist/analyzers/usagePatterns.d.ts +42 -0
- package/dist/analyzers/usagePatterns.d.ts.map +1 -0
- package/dist/analyzers/usagePatterns.js +75 -0
- package/dist/analyzers/usagePatterns.js.map +1 -0
- package/dist/api-contract/context/backend.d.ts +19 -0
- package/dist/api-contract/context/backend.d.ts.map +1 -0
- package/dist/api-contract/context/backend.js +64 -0
- package/dist/api-contract/context/backend.js.map +1 -0
- package/dist/api-contract/context/contract.d.ts +34 -0
- package/dist/api-contract/context/contract.d.ts.map +1 -0
- package/dist/api-contract/context/contract.js +306 -0
- package/dist/api-contract/context/contract.js.map +1 -0
- package/dist/api-contract/context/frontend.d.ts +19 -0
- package/dist/api-contract/context/frontend.d.ts.map +1 -0
- package/dist/api-contract/context/frontend.js +64 -0
- package/dist/api-contract/context/frontend.js.map +1 -0
- package/dist/api-contract/detector.d.ts +28 -0
- package/dist/api-contract/detector.d.ts.map +1 -0
- package/dist/api-contract/detector.js +393 -0
- package/dist/api-contract/detector.js.map +1 -0
- package/dist/api-contract/extractors/python.d.ts +32 -0
- package/dist/api-contract/extractors/python.d.ts.map +1 -0
- package/dist/api-contract/extractors/python.js +521 -0
- package/dist/api-contract/extractors/python.js.map +1 -0
- package/dist/api-contract/extractors/pythonAstUtils.d.ts +44 -0
- package/dist/api-contract/extractors/pythonAstUtils.d.ts.map +1 -0
- package/dist/api-contract/extractors/pythonAstUtils.js +489 -0
- package/dist/api-contract/extractors/pythonAstUtils.js.map +1 -0
- package/dist/api-contract/extractors/tsAstUtils.d.ts +47 -0
- package/dist/api-contract/extractors/tsAstUtils.d.ts.map +1 -0
- package/dist/api-contract/extractors/tsAstUtils.js +173 -0
- package/dist/api-contract/extractors/tsAstUtils.js.map +1 -0
- package/dist/api-contract/extractors/typescript.d.ts +32 -0
- package/dist/api-contract/extractors/typescript.d.ts.map +1 -0
- package/dist/api-contract/extractors/typescript.js +666 -0
- package/dist/api-contract/extractors/typescript.js.map +1 -0
- package/dist/api-contract/index.d.ts +104 -0
- package/dist/api-contract/index.d.ts.map +1 -0
- package/dist/api-contract/index.js +232 -0
- package/dist/api-contract/index.js.map +1 -0
- package/dist/api-contract/types.d.ts +151 -0
- package/dist/api-contract/types.d.ts.map +1 -0
- package/dist/api-contract/types.js +19 -0
- package/dist/api-contract/types.js.map +1 -0
- package/dist/api-contract/validators/endpoint.d.ts +21 -0
- package/dist/api-contract/validators/endpoint.d.ts.map +1 -0
- package/dist/api-contract/validators/endpoint.js +224 -0
- package/dist/api-contract/validators/endpoint.js.map +1 -0
- package/dist/api-contract/validators/index.d.ts +40 -0
- package/dist/api-contract/validators/index.d.ts.map +1 -0
- package/dist/api-contract/validators/index.js +875 -0
- package/dist/api-contract/validators/index.js.map +1 -0
- package/dist/api-contract/validators/parameter.d.ts +17 -0
- package/dist/api-contract/validators/parameter.d.ts.map +1 -0
- package/dist/api-contract/validators/parameter.js +250 -0
- package/dist/api-contract/validators/parameter.js.map +1 -0
- package/dist/api-contract/validators/type.d.ts +38 -0
- package/dist/api-contract/validators/type.d.ts.map +1 -0
- package/dist/api-contract/validators/type.js +244 -0
- package/dist/api-contract/validators/type.js.map +1 -0
- package/dist/context/apiContract/complexTypeSupport.d.ts +83 -0
- package/dist/context/apiContract/complexTypeSupport.d.ts.map +1 -0
- package/dist/context/apiContract/complexTypeSupport.js +665 -0
- package/dist/context/apiContract/complexTypeSupport.js.map +1 -0
- package/dist/context/apiContract/graphqlSupport.d.ts +105 -0
- package/dist/context/apiContract/graphqlSupport.d.ts.map +1 -0
- package/dist/context/apiContract/graphqlSupport.js +671 -0
- package/dist/context/apiContract/graphqlSupport.js.map +1 -0
- package/dist/context/apiContract/index.d.ts +14 -0
- package/dist/context/apiContract/index.d.ts.map +1 -0
- package/dist/context/apiContract/index.js +17 -0
- package/dist/context/apiContract/index.js.map +1 -0
- package/dist/context/apiContract/webSocketSupport.d.ts +104 -0
- package/dist/context/apiContract/webSocketSupport.d.ts.map +1 -0
- package/dist/context/apiContract/webSocketSupport.js +465 -0
- package/dist/context/apiContract/webSocketSupport.js.map +1 -0
- package/dist/context/apiContractContext.d.ts +15 -0
- package/dist/context/apiContractContext.d.ts.map +1 -0
- package/dist/context/apiContractContext.js +979 -0
- package/dist/context/apiContractContext.js.map +1 -0
- package/dist/context/apiContractExtraction.d.ts +52 -0
- package/dist/context/apiContractExtraction.d.ts.map +1 -0
- package/dist/context/apiContractExtraction.js +438 -0
- package/dist/context/apiContractExtraction.js.map +1 -0
- package/dist/context/contextLineage.d.ts +79 -0
- package/dist/context/contextLineage.d.ts.map +1 -0
- package/dist/context/contextLineage.js +259 -0
- package/dist/context/contextLineage.js.map +1 -0
- package/dist/context/contextOrchestrator.d.ts +57 -0
- package/dist/context/contextOrchestrator.d.ts.map +1 -0
- package/dist/context/contextOrchestrator.js +162 -0
- package/dist/context/contextOrchestrator.js.map +1 -0
- package/dist/context/intentTracker.d.ts +73 -0
- package/dist/context/intentTracker.d.ts.map +1 -0
- package/dist/context/intentTracker.js +168 -0
- package/dist/context/intentTracker.js.map +1 -0
- package/dist/context/projectContext.d.ts +219 -0
- package/dist/context/projectContext.d.ts.map +1 -0
- package/dist/context/projectContext.js +1984 -0
- package/dist/context/projectContext.js.map +1 -0
- package/dist/prompts/index.d.ts +17 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +260 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/library.d.ts +51 -0
- package/dist/prompts/library.d.ts.map +1 -0
- package/dist/prompts/library.js +65 -0
- package/dist/prompts/library.js.map +1 -0
- package/dist/prompts/templates.d.ts +44 -0
- package/dist/prompts/templates.d.ts.map +1 -0
- package/dist/prompts/templates.js +97 -0
- package/dist/prompts/templates.js.map +1 -0
- package/dist/queue/jobPersistence.d.ts +46 -0
- package/dist/queue/jobPersistence.d.ts.map +1 -0
- package/dist/queue/jobPersistence.js +158 -0
- package/dist/queue/jobPersistence.js.map +1 -0
- package/dist/queue/jobQueue.d.ts +116 -0
- package/dist/queue/jobQueue.d.ts.map +1 -0
- package/dist/queue/jobQueue.js +275 -0
- package/dist/queue/jobQueue.js.map +1 -0
- package/dist/queue/validationJob.d.ts +69 -0
- package/dist/queue/validationJob.d.ts.map +1 -0
- package/dist/queue/validationJob.js +435 -0
- package/dist/queue/validationJob.js.map +1 -0
- package/dist/resources/index.d.ts +15 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +328 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/validationReportStore.d.ts +170 -0
- package/dist/resources/validationReportStore.d.ts.map +1 -0
- package/dist/resources/validationReportStore.js +515 -0
- package/dist/resources/validationReportStore.js.map +1 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +102 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/asyncValidation.d.ts +19 -0
- package/dist/tools/asyncValidation.d.ts.map +1 -0
- package/dist/tools/asyncValidation.js +346 -0
- package/dist/tools/asyncValidation.js.map +1 -0
- package/dist/tools/buildContext.d.ts +17 -0
- package/dist/tools/buildContext.d.ts.map +1 -0
- package/dist/tools/buildContext.js +188 -0
- package/dist/tools/buildContext.js.map +1 -0
- package/dist/tools/getDependencyGraph.d.ts +16 -0
- package/dist/tools/getDependencyGraph.d.ts.map +1 -0
- package/dist/tools/getDependencyGraph.js +436 -0
- package/dist/tools/getDependencyGraph.js.map +1 -0
- package/dist/tools/incrementalValidation.d.ts +71 -0
- package/dist/tools/incrementalValidation.d.ts.map +1 -0
- package/dist/tools/incrementalValidation.js +203 -0
- package/dist/tools/incrementalValidation.js.map +1 -0
- package/dist/tools/index.d.ts +24 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +106 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/validateCode.d.ts +17 -0
- package/dist/tools/validateCode.d.ts.map +1 -0
- package/dist/tools/validateCode.js +368 -0
- package/dist/tools/validateCode.js.map +1 -0
- package/dist/tools/validateCodeLite.d.ts +2 -0
- package/dist/tools/validateCodeLite.d.ts.map +1 -0
- package/dist/tools/validateCodeLite.js +2 -0
- package/dist/tools/validateCodeLite.js.map +1 -0
- package/dist/tools/validation/builtins.d.ts +92 -0
- package/dist/tools/validation/builtins.d.ts.map +1 -0
- package/dist/tools/validation/builtins.js +2184 -0
- package/dist/tools/validation/builtins.js.map +1 -0
- package/dist/tools/validation/contextualNaming.d.ts +99 -0
- package/dist/tools/validation/contextualNaming.d.ts.map +1 -0
- package/dist/tools/validation/contextualNaming.js +959 -0
- package/dist/tools/validation/contextualNaming.js.map +1 -0
- package/dist/tools/validation/deadCode.d.ts +115 -0
- package/dist/tools/validation/deadCode.d.ts.map +1 -0
- package/dist/tools/validation/deadCode.js +861 -0
- package/dist/tools/validation/deadCode.js.map +1 -0
- package/dist/tools/validation/extractors/index.d.ts +131 -0
- package/dist/tools/validation/extractors/index.d.ts.map +1 -0
- package/dist/tools/validation/extractors/index.js +233 -0
- package/dist/tools/validation/extractors/index.js.map +1 -0
- package/dist/tools/validation/extractors/javascript.d.ts +73 -0
- package/dist/tools/validation/extractors/javascript.d.ts.map +1 -0
- package/dist/tools/validation/extractors/javascript.js +1841 -0
- package/dist/tools/validation/extractors/javascript.js.map +1 -0
- package/dist/tools/validation/extractors/python.d.ts +93 -0
- package/dist/tools/validation/extractors/python.d.ts.map +1 -0
- package/dist/tools/validation/extractors/python.js +799 -0
- package/dist/tools/validation/extractors/python.js.map +1 -0
- package/dist/tools/validation/manifest.d.ts +45 -0
- package/dist/tools/validation/manifest.d.ts.map +1 -0
- package/dist/tools/validation/manifest.js +719 -0
- package/dist/tools/validation/manifest.js.map +1 -0
- package/dist/tools/validation/parser.d.ts +58 -0
- package/dist/tools/validation/parser.d.ts.map +1 -0
- package/dist/tools/validation/parser.js +232 -0
- package/dist/tools/validation/parser.js.map +1 -0
- package/dist/tools/validation/registry.d.ts +15 -0
- package/dist/tools/validation/registry.d.ts.map +1 -0
- package/dist/tools/validation/registry.js +169 -0
- package/dist/tools/validation/registry.js.map +1 -0
- package/dist/tools/validation/scoring.d.ts +54 -0
- package/dist/tools/validation/scoring.d.ts.map +1 -0
- package/dist/tools/validation/scoring.js +242 -0
- package/dist/tools/validation/scoring.js.map +1 -0
- package/dist/tools/validation/types.d.ts +120 -0
- package/dist/tools/validation/types.d.ts.map +1 -0
- package/dist/tools/validation/types.js +11 -0
- package/dist/tools/validation/types.js.map +1 -0
- package/dist/tools/validation/unusedLocals.d.ts +36 -0
- package/dist/tools/validation/unusedLocals.d.ts.map +1 -0
- package/dist/tools/validation/unusedLocals.js +333 -0
- package/dist/tools/validation/unusedLocals.js.map +1 -0
- package/dist/tools/validation/validation.d.ts +98 -0
- package/dist/tools/validation/validation.d.ts.map +1 -0
- package/dist/tools/validation/validation.js +1837 -0
- package/dist/tools/validation/validation.js.map +1 -0
- package/dist/types/codeGraph.d.ts +163 -0
- package/dist/types/codeGraph.d.ts.map +1 -0
- package/dist/types/codeGraph.js +9 -0
- package/dist/types/codeGraph.js.map +1 -0
- package/dist/types/symbolGraph.d.ts +68 -0
- package/dist/types/symbolGraph.d.ts.map +1 -0
- package/dist/types/symbolGraph.js +10 -0
- package/dist/types/symbolGraph.js.map +1 -0
- package/dist/types/tools.d.ts +43 -0
- package/dist/types/tools.d.ts.map +1 -0
- package/dist/types/tools.js +7 -0
- package/dist/types/tools.js.map +1 -0
- package/dist/utils/fileFilter.d.ts +37 -0
- package/dist/utils/fileFilter.d.ts.map +1 -0
- package/dist/utils/fileFilter.js +91 -0
- package/dist/utils/fileFilter.js.map +1 -0
- package/dist/utils/gitUtils.d.ts +28 -0
- package/dist/utils/gitUtils.d.ts.map +1 -0
- package/dist/utils/gitUtils.js +81 -0
- package/dist/utils/gitUtils.js.map +1 -0
- package/dist/utils/logger.d.ts +15 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +38 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/serialization.d.ts +25 -0
- package/dist/utils/serialization.d.ts.map +1 -0
- package/dist/utils/serialization.js +53 -0
- package/dist/utils/serialization.js.map +1 -0
- package/package.json +90 -0
|
@@ -0,0 +1,1837 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Validation Logic Module
|
|
3
|
+
*
|
|
4
|
+
* This module contains the core validation algorithms for detecting hallucinations
|
|
5
|
+
* and validating symbols against the project context.
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities:
|
|
8
|
+
* - Validate that imported packages exist in manifest files
|
|
9
|
+
* - Validate that used symbols exist in the project symbol table
|
|
10
|
+
* - Check parameter counts for function calls
|
|
11
|
+
* - Calculate confidence scores for validation issues
|
|
12
|
+
* - Generate reasoning explanations for each issue
|
|
13
|
+
* - Build symbol lookup tables from project context
|
|
14
|
+
* - Handle strict mode vs. non-strict mode validation
|
|
15
|
+
*
|
|
16
|
+
* @format
|
|
17
|
+
*/
|
|
18
|
+
import * as path from "path";
|
|
19
|
+
import { resolveImport } from "../../context/projectContext.js";
|
|
20
|
+
import { extractSymbolsAST, collectLocalDefinitionsAST } from "./extractors/index.js";
|
|
21
|
+
import { parseCodeCached } from "./parser.js";
|
|
22
|
+
import { suggestSimilar, extractSimilarSymbols } from "./scoring.js";
|
|
23
|
+
import { isPythonSymbolExported, getPythonPipNameForImport } from "./manifest.js";
|
|
24
|
+
import { isJSBuiltin, isPythonBuiltin, isTSBuiltinType, NODE_BUILTIN_MODULES, } from "./builtins.js";
|
|
25
|
+
import { usagePatternAnalyzer } from "../../analyzers/usagePatterns.js";
|
|
26
|
+
import { isContextuallyValid, } from "./contextualNaming.js";
|
|
27
|
+
import { checkPackageRegistry } from "./registry.js";
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Manifest Validation (Tier 0)
|
|
30
|
+
// ============================================================================
|
|
31
|
+
/**
|
|
32
|
+
* Validate that all imported packages exist in manifest files.
|
|
33
|
+
* Checks package.json for JavaScript/TypeScript and requirements.txt/pyproject.toml for Python.
|
|
34
|
+
*
|
|
35
|
+
* @param imports - Array of import statements extracted from code
|
|
36
|
+
* @param manifest - Manifest dependencies loaded from package files
|
|
37
|
+
* @param newCode - The source code being validated (for extracting line content)
|
|
38
|
+
* @param language - Programming language (default: typescript)
|
|
39
|
+
* @returns Array of validation issues for missing dependencies
|
|
40
|
+
*/
|
|
41
|
+
export async function validateManifest(imports, manifest, newCode, language = "typescript", filePath = "") {
|
|
42
|
+
const issues = [];
|
|
43
|
+
// Phase 1: Collect all unknown packages that need registry lookup
|
|
44
|
+
const unknownPackages = [];
|
|
45
|
+
for (const imp of imports) {
|
|
46
|
+
if (!imp.isExternal)
|
|
47
|
+
continue;
|
|
48
|
+
const pkgName = getPackageName(imp.module, language);
|
|
49
|
+
// Skip Node.js built-ins
|
|
50
|
+
if (imp.module.startsWith("node:") || NODE_BUILTIN_MODULES.has(pkgName)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
// Check if package is in manifest
|
|
54
|
+
if (!manifest.all.has(pkgName)) {
|
|
55
|
+
const scopedName = imp.module.startsWith("@") ?
|
|
56
|
+
imp.module.split("/").slice(0, 2).join("/")
|
|
57
|
+
: pkgName;
|
|
58
|
+
if (!manifest.all.has(scopedName)) {
|
|
59
|
+
// For Python, check if this import name is a known alias of a pip package
|
|
60
|
+
// (e.g. "dateutil" -> "python-dateutil", "pythonjsonlogger" -> "python-json-logger")
|
|
61
|
+
// These are commonly installed as transitive deps and shouldn't be flagged
|
|
62
|
+
if (language === "python" && getPythonPipNameForImport(pkgName)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
unknownPackages.push({ imp, pkgName, scopedName });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (unknownPackages.length === 0)
|
|
70
|
+
return issues;
|
|
71
|
+
// Phase 2: Batch registry lookups — deduplicate and check in parallel
|
|
72
|
+
const uniquePkgNames = [...new Set(unknownPackages.map(u => u.pkgName))];
|
|
73
|
+
const REGISTRY_BATCH_SIZE = 10;
|
|
74
|
+
const registryResults = new Map();
|
|
75
|
+
for (let i = 0; i < uniquePkgNames.length; i += REGISTRY_BATCH_SIZE) {
|
|
76
|
+
const batch = uniquePkgNames.slice(i, i + REGISTRY_BATCH_SIZE);
|
|
77
|
+
const results = await Promise.all(batch.map(async (pkgName) => ({
|
|
78
|
+
pkgName,
|
|
79
|
+
exists: await checkPackageRegistry(pkgName, language),
|
|
80
|
+
})));
|
|
81
|
+
for (const { pkgName, exists } of results) {
|
|
82
|
+
registryResults.set(pkgName, exists);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Phase 3: Build issues from cached results
|
|
86
|
+
for (const { imp, pkgName } of unknownPackages) {
|
|
87
|
+
const existsInRegistry = registryResults.get(pkgName) ?? false;
|
|
88
|
+
if (existsInRegistry) {
|
|
89
|
+
const installCmd = language === "python"
|
|
90
|
+
? `Run: pip install ${pkgName} (or add to requirements.txt)`
|
|
91
|
+
: `Run: npm install ${pkgName}`;
|
|
92
|
+
issues.push({
|
|
93
|
+
type: "missingDependency",
|
|
94
|
+
severity: "low",
|
|
95
|
+
message: `Package '${pkgName}' is not installed (but exists on registry)`,
|
|
96
|
+
line: imp.line,
|
|
97
|
+
file: filePath,
|
|
98
|
+
code: getLineFromCode(newCode, imp.line),
|
|
99
|
+
suggestion: installCmd,
|
|
100
|
+
confidence: 100,
|
|
101
|
+
reasoning: `Package not found in manifest, but verified to exist on ${language} registry. Safe to install.`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
issues.push({
|
|
106
|
+
type: "dependencyHallucination",
|
|
107
|
+
severity: "critical",
|
|
108
|
+
message: `Package '${imp.module}' does not exist on ${language} registry`,
|
|
109
|
+
line: imp.line,
|
|
110
|
+
file: filePath,
|
|
111
|
+
code: getLineFromCode(newCode, imp.line),
|
|
112
|
+
suggestion: `Did you mean: ${suggestSimilar(pkgName, Array.from(manifest.all)) || "unknown"}?`,
|
|
113
|
+
confidence: 99,
|
|
114
|
+
reasoning: `Package not found in manifest AND lookup failed on registry. This is likely a hallucination.`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return issues;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Extract package name from import path.
|
|
122
|
+
* Handles scoped packages correctly and Python submodules.
|
|
123
|
+
*
|
|
124
|
+
* @param importPath - The import path (e.g., '@scope/package/path' or 'package/path' or 'package.submodule')
|
|
125
|
+
* @param language - Programming language to determine submodule handling (optional)
|
|
126
|
+
* @returns The base package name
|
|
127
|
+
*/
|
|
128
|
+
function getPackageName(importPath, language) {
|
|
129
|
+
// Handle scoped packages: @scope/package/path -> @scope/package
|
|
130
|
+
if (importPath.startsWith("@")) {
|
|
131
|
+
const parts = importPath.split("/");
|
|
132
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : importPath;
|
|
133
|
+
}
|
|
134
|
+
// For Python, handle dot notation for submodules: package.submodule -> package
|
|
135
|
+
// This is critical because Python uses dots for submodules (e.g., fastapi.middleware.cors)
|
|
136
|
+
// while JavaScript/TypeScript uses slashes (e.g., package/submodule)
|
|
137
|
+
if (language === "python") {
|
|
138
|
+
return importPath.split(".")[0];
|
|
139
|
+
}
|
|
140
|
+
// Regular packages: package/path -> package
|
|
141
|
+
return importPath.split("/")[0];
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Extract a specific line from code by line number.
|
|
145
|
+
*
|
|
146
|
+
* @param code - The source code
|
|
147
|
+
* @param lineNum - Line number (1-indexed)
|
|
148
|
+
* @returns The trimmed line content
|
|
149
|
+
*/
|
|
150
|
+
export function getLineFromCode(code, lineNum) {
|
|
151
|
+
const lines = code.split("\n");
|
|
152
|
+
return lines[lineNum - 1]?.trim() || "";
|
|
153
|
+
}
|
|
154
|
+
// ============================================================================
|
|
155
|
+
// Symbol Table Building
|
|
156
|
+
// ============================================================================
|
|
157
|
+
/**
|
|
158
|
+
* Build a symbol table from project context for validation.
|
|
159
|
+
* Converts the project context symbol index into a flat array of ProjectSymbol objects.
|
|
160
|
+
*
|
|
161
|
+
* @param context - The project context containing all symbols
|
|
162
|
+
* @param relevantSymbols - Optional list of symbol names to include (for smart context filtering)
|
|
163
|
+
* @returns Array of project symbols for validation
|
|
164
|
+
*/
|
|
165
|
+
export function buildSymbolTable(context, relevantSymbols) {
|
|
166
|
+
const symbols = [];
|
|
167
|
+
const relevantSet = relevantSymbols ? new Set(relevantSymbols) : null;
|
|
168
|
+
for (const [name, definitions] of context.symbolIndex) {
|
|
169
|
+
// If smart context is enabled, only include relevant symbols
|
|
170
|
+
if (relevantSet && !relevantSet.has(name)) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
for (const def of definitions) {
|
|
174
|
+
symbols.push({
|
|
175
|
+
name,
|
|
176
|
+
type: mapSymbolKind(def.symbol.kind),
|
|
177
|
+
file: def.file,
|
|
178
|
+
line: def.symbol.line,
|
|
179
|
+
params: def.symbol.params?.map((p) => p.name),
|
|
180
|
+
paramCount: def.symbol.params?.length,
|
|
181
|
+
scope: def.symbol.scope,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return symbols;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Map symbol kind from project context to ProjectSymbol type.
|
|
189
|
+
*
|
|
190
|
+
* @param kind - The symbol kind from project context
|
|
191
|
+
* @returns The mapped ProjectSymbol type
|
|
192
|
+
*/
|
|
193
|
+
function mapSymbolKind(kind) {
|
|
194
|
+
switch (kind) {
|
|
195
|
+
case "function":
|
|
196
|
+
case "hook":
|
|
197
|
+
return "function";
|
|
198
|
+
case "class":
|
|
199
|
+
case "component":
|
|
200
|
+
return "class";
|
|
201
|
+
case "interface":
|
|
202
|
+
case "type":
|
|
203
|
+
case "enum":
|
|
204
|
+
case "variable":
|
|
205
|
+
case "route":
|
|
206
|
+
return "variable";
|
|
207
|
+
default:
|
|
208
|
+
return "method";
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// ============================================================================
|
|
212
|
+
// Confidence Scoring and Reasoning
|
|
213
|
+
// ============================================================================
|
|
214
|
+
/**
|
|
215
|
+
* Calculate confidence score for a validation issue.
|
|
216
|
+
* Based on multiple factors: similarity to existing symbols, context, etc.
|
|
217
|
+
*
|
|
218
|
+
* @param options - Configuration for confidence calculation
|
|
219
|
+
* @returns Object containing confidence score (0-100) and reasoning explanation
|
|
220
|
+
*/
|
|
221
|
+
export function calculateConfidence(options) {
|
|
222
|
+
const { issueType, symbolName, similarSymbols, existsInProject, strictMode } = options;
|
|
223
|
+
let confidence = 0;
|
|
224
|
+
let reasoning = "";
|
|
225
|
+
switch (issueType) {
|
|
226
|
+
case "nonExistentFunction":
|
|
227
|
+
case "nonExistentClass":
|
|
228
|
+
if (existsInProject && strictMode) {
|
|
229
|
+
// Symbol exists but not imported
|
|
230
|
+
confidence = 90;
|
|
231
|
+
reasoning = `Symbol '${symbolName}' found in project but not imported. High confidence this is a missing import.`;
|
|
232
|
+
}
|
|
233
|
+
else if (similarSymbols.length === 0) {
|
|
234
|
+
// No similar symbols at all
|
|
235
|
+
confidence = 95;
|
|
236
|
+
reasoning = `Searched entire project, found no symbol named '${symbolName}' or similar. Very high confidence this is a hallucination.`;
|
|
237
|
+
}
|
|
238
|
+
else if (similarSymbols.length === 1) {
|
|
239
|
+
// One very similar symbol (likely typo)
|
|
240
|
+
confidence = 92;
|
|
241
|
+
reasoning = `Found very similar symbol: ${similarSymbols[0]}. High confidence this is a typo.`;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
// Multiple similar symbols
|
|
245
|
+
confidence = 85;
|
|
246
|
+
reasoning = `Found ${similarSymbols.length} similar symbols. Likely a typo or wrong function name.`;
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
case "dependencyHallucination":
|
|
250
|
+
confidence = 95;
|
|
251
|
+
reasoning = `Package not found in manifest. This will cause import errors at runtime.`;
|
|
252
|
+
break;
|
|
253
|
+
case "wrongParamCount":
|
|
254
|
+
confidence = 88;
|
|
255
|
+
reasoning = `Parameter count mismatch detected via AST analysis. High confidence this will cause runtime errors.`;
|
|
256
|
+
break;
|
|
257
|
+
case "nonExistentMethod":
|
|
258
|
+
confidence = 70;
|
|
259
|
+
reasoning = `Method not found on object. Medium confidence - may be dynamic or inherited.`;
|
|
260
|
+
break;
|
|
261
|
+
case "nonExistentImport":
|
|
262
|
+
confidence = 93;
|
|
263
|
+
reasoning = `Imported symbol not found in target module. High confidence this will fail at runtime.`;
|
|
264
|
+
break;
|
|
265
|
+
case "undefinedVariable":
|
|
266
|
+
confidence = 90;
|
|
267
|
+
reasoning = `Variable '${symbolName}' is used but not defined or imported. High confidence this is a hallucination.`;
|
|
268
|
+
break;
|
|
269
|
+
case "unusedImport":
|
|
270
|
+
confidence = 98;
|
|
271
|
+
reasoning = `Imported symbol '${symbolName}' is never used in the code. Very high confidence.`;
|
|
272
|
+
break;
|
|
273
|
+
default:
|
|
274
|
+
confidence = 75;
|
|
275
|
+
reasoning = `Issue detected via static analysis.`;
|
|
276
|
+
}
|
|
277
|
+
return { confidence, reasoning };
|
|
278
|
+
}
|
|
279
|
+
// ============================================================================
|
|
280
|
+
// Symbol Validation (Tier 1)
|
|
281
|
+
// ============================================================================
|
|
282
|
+
/**
|
|
283
|
+
* Validate that all used symbols exist in the project symbol table.
|
|
284
|
+
* Performs comprehensive validation including:
|
|
285
|
+
* - Function calls
|
|
286
|
+
* - Method calls
|
|
287
|
+
* - Class instantiations
|
|
288
|
+
* - Parameter count checking
|
|
289
|
+
* - Import validation
|
|
290
|
+
* - Python __all__ export validation
|
|
291
|
+
*
|
|
292
|
+
* @param usedSymbols - Array of symbol usages extracted from code
|
|
293
|
+
* @param symbolTable - Array of project symbols for validation
|
|
294
|
+
* @param newCode - The source code being validated
|
|
295
|
+
* @param language - Programming language ('python', 'javascript', 'typescript')
|
|
296
|
+
* @param strictMode - If true, requires explicit imports for all symbols
|
|
297
|
+
* @param imports - Array of import statements (for internal import validation)
|
|
298
|
+
* @param pythonExports - Map of Python module exports (for __all__ validation)
|
|
299
|
+
* @param typeReferences - Optional array of type references (for unused import detection)
|
|
300
|
+
* @returns Array of validation issues
|
|
301
|
+
*/
|
|
302
|
+
export function validateSymbols(usedSymbols, symbolTable, newCode, language, strictMode, imports = [], pythonExports = new Map(), context = null, // Added context
|
|
303
|
+
filePath = "", // Added file path
|
|
304
|
+
missingPackages = new Set(), // Added missing packages for smart validation
|
|
305
|
+
typeReferences = []) {
|
|
306
|
+
const issues = [];
|
|
307
|
+
// Build lookup maps for PROJECT symbols
|
|
308
|
+
// Use arrays to handle multiple symbols with same name (different scopes)
|
|
309
|
+
const projectFunctions = new Map();
|
|
310
|
+
const projectClasses = new Map();
|
|
311
|
+
const projectMethods = new Map();
|
|
312
|
+
const projectVariables = new Map(); // includes interfaces, types, enums
|
|
313
|
+
for (const sym of symbolTable) {
|
|
314
|
+
if (sym.type === "function") {
|
|
315
|
+
const existing = projectFunctions.get(sym.name) || [];
|
|
316
|
+
existing.push(sym);
|
|
317
|
+
projectFunctions.set(sym.name, existing);
|
|
318
|
+
}
|
|
319
|
+
else if (sym.type === "class") {
|
|
320
|
+
projectClasses.set(sym.name, sym);
|
|
321
|
+
}
|
|
322
|
+
else if (sym.type === "method") {
|
|
323
|
+
const existing = projectMethods.get(sym.name) || [];
|
|
324
|
+
existing.push(sym);
|
|
325
|
+
projectMethods.set(sym.name, existing);
|
|
326
|
+
}
|
|
327
|
+
else if (sym.type === "variable") {
|
|
328
|
+
projectVariables.set(sym.name, sym);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// When smart context filtering is enabled, symbolTable may not include all symbols.
|
|
332
|
+
// For certain checks (especially internal-import method validation), we consult the
|
|
333
|
+
// full ProjectContext symbol index as a fallback.
|
|
334
|
+
const hasContextClass = (name) => {
|
|
335
|
+
if (!context)
|
|
336
|
+
return false;
|
|
337
|
+
const defs = context.symbolIndex.get(name);
|
|
338
|
+
if (!defs)
|
|
339
|
+
return false;
|
|
340
|
+
for (const def of defs) {
|
|
341
|
+
const kind = def?.symbol?.kind;
|
|
342
|
+
if (kind === "class" || kind === "component")
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
return false;
|
|
346
|
+
};
|
|
347
|
+
const getContextSymbolsByName = (name) => {
|
|
348
|
+
if (!context)
|
|
349
|
+
return [];
|
|
350
|
+
const defs = context.symbolIndex.get(name);
|
|
351
|
+
if (!defs)
|
|
352
|
+
return [];
|
|
353
|
+
const out = [];
|
|
354
|
+
for (const def of defs) {
|
|
355
|
+
out.push({
|
|
356
|
+
name,
|
|
357
|
+
type: mapSymbolKind(def.symbol.kind),
|
|
358
|
+
file: def.file,
|
|
359
|
+
line: def.symbol.line,
|
|
360
|
+
params: def.symbol.params?.map((p) => p.name),
|
|
361
|
+
paramCount: def.symbol.params?.length,
|
|
362
|
+
scope: def.symbol.scope,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
366
|
+
};
|
|
367
|
+
// Build lookup maps for VALID symbols
|
|
368
|
+
const validFunctions = new Map();
|
|
369
|
+
const validClasses = new Map();
|
|
370
|
+
const validMethods = new Map();
|
|
371
|
+
const validVariables = new Map();
|
|
372
|
+
// Helper to get first matching symbol by scope
|
|
373
|
+
const getMatchingSymbol = (symbols, objectName) => {
|
|
374
|
+
if (!symbols || symbols.length === 0)
|
|
375
|
+
return undefined;
|
|
376
|
+
if (symbols.length === 1) {
|
|
377
|
+
// Single symbol - check if scope matches (if it has one)
|
|
378
|
+
const sym = symbols[0];
|
|
379
|
+
if (!sym.scope || !objectName || sym.scope === objectName) {
|
|
380
|
+
return sym;
|
|
381
|
+
}
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
// Multiple symbols - find one with matching scope, or one without scope (general method)
|
|
385
|
+
for (const sym of symbols) {
|
|
386
|
+
if (sym.scope === objectName)
|
|
387
|
+
return sym; // Exact scope match
|
|
388
|
+
if (!sym.scope)
|
|
389
|
+
return sym; // General method (no scope)
|
|
390
|
+
}
|
|
391
|
+
return undefined;
|
|
392
|
+
};
|
|
393
|
+
// In non-strict mode: all project symbols are valid (backwards compatible)
|
|
394
|
+
if (!strictMode) {
|
|
395
|
+
for (const [name, syms] of projectFunctions) {
|
|
396
|
+
validFunctions.set(name, syms[0]); // Use first function as representative
|
|
397
|
+
}
|
|
398
|
+
for (const [name, sym] of projectClasses)
|
|
399
|
+
validClasses.set(name, sym);
|
|
400
|
+
for (const [name, syms] of projectMethods) {
|
|
401
|
+
validMethods.set(name, syms[0]); // Use first method as representative
|
|
402
|
+
}
|
|
403
|
+
for (const [name, sym] of projectVariables)
|
|
404
|
+
validVariables.set(name, sym);
|
|
405
|
+
}
|
|
406
|
+
// Tier 1: Add symbols defined in the new code itself (including parameters, destructured variables)
|
|
407
|
+
// These MUST TAKE PRECEDENCE over project symbols to avoid false positives on local scope
|
|
408
|
+
const newCodeSymbols = extractSymbolsAST(newCode, "(new code)", language, {
|
|
409
|
+
includeParameterSymbols: true,
|
|
410
|
+
});
|
|
411
|
+
for (const sym of newCodeSymbols) {
|
|
412
|
+
const projectSym = {
|
|
413
|
+
name: sym.name,
|
|
414
|
+
type: sym.type === "interface" || sym.type === "type" ?
|
|
415
|
+
"variable"
|
|
416
|
+
: sym.type,
|
|
417
|
+
file: "(new code)",
|
|
418
|
+
params: sym.params,
|
|
419
|
+
paramCount: sym.paramCount,
|
|
420
|
+
};
|
|
421
|
+
switch (sym.type) {
|
|
422
|
+
case "function":
|
|
423
|
+
validFunctions.set(sym.name, projectSym);
|
|
424
|
+
break;
|
|
425
|
+
case "class":
|
|
426
|
+
validClasses.set(sym.name, projectSym);
|
|
427
|
+
break;
|
|
428
|
+
case "method":
|
|
429
|
+
validMethods.set(sym.name, projectSym);
|
|
430
|
+
break;
|
|
431
|
+
case "variable":
|
|
432
|
+
case "interface":
|
|
433
|
+
case "type":
|
|
434
|
+
validVariables.set(sym.name, projectSym);
|
|
435
|
+
// Variables can be called if they hold functions (e.g., destructured from hooks or params)
|
|
436
|
+
validFunctions.set(sym.name, projectSym);
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// Tier 2: Add imported symbols (both internal and external)
|
|
441
|
+
for (const imp of imports) {
|
|
442
|
+
if (!imp.isExternal) {
|
|
443
|
+
// Internal imports: Validate against project symbol table AND precise module resolution
|
|
444
|
+
// Attempt module resolution if context is available AND we have a real file path
|
|
445
|
+
// Skip resolution for empty filePath (code snippets without known location)
|
|
446
|
+
let resolvedFile = null;
|
|
447
|
+
if (context && filePath && filePath.trim()) {
|
|
448
|
+
resolvedFile = resolveImport(imp.module, filePath, Array.from(context.files.keys()));
|
|
449
|
+
// Python-specific: convert dot notation to path separators
|
|
450
|
+
if (!resolvedFile && language === "python") {
|
|
451
|
+
if (imp.module.startsWith(".")) {
|
|
452
|
+
// Relative import: from ..models.deliverable import X
|
|
453
|
+
// Count leading dots to determine how many directories to go up
|
|
454
|
+
const dotMatch = imp.module.match(/^(\.+)/);
|
|
455
|
+
const dotCount = dotMatch ? dotMatch[1].length : 0;
|
|
456
|
+
const remainder = imp.module.slice(dotCount);
|
|
457
|
+
// Go up (dotCount) directories from current file's package directory
|
|
458
|
+
let baseDir = filePath.substring(0, filePath.lastIndexOf("/"));
|
|
459
|
+
for (let i = 1; i < dotCount; i++) {
|
|
460
|
+
baseDir = baseDir.substring(0, baseDir.lastIndexOf("/"));
|
|
461
|
+
}
|
|
462
|
+
if (remainder) {
|
|
463
|
+
const modPath = remainder.replace(/\./g, "/");
|
|
464
|
+
const pyFile = path.join(baseDir, `${modPath}.py`);
|
|
465
|
+
const pyInit = path.join(baseDir, modPath, "__init__.py");
|
|
466
|
+
if (context.files.has(pyFile)) {
|
|
467
|
+
resolvedFile = pyFile;
|
|
468
|
+
}
|
|
469
|
+
else if (context.files.has(pyInit)) {
|
|
470
|
+
resolvedFile = pyInit;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
// "from . import X" — resolve to current package's __init__.py
|
|
475
|
+
const pyInit = path.join(baseDir, "__init__.py");
|
|
476
|
+
if (context.files.has(pyInit)) {
|
|
477
|
+
resolvedFile = pyInit;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
// Absolute import: from app.core.config import X
|
|
483
|
+
const modulePath = imp.module.replace(/\./g, "/");
|
|
484
|
+
const basePath = context.projectPath;
|
|
485
|
+
const pyFile = path.join(basePath, `${modulePath}.py`);
|
|
486
|
+
const pyInit = path.join(basePath, modulePath, "__init__.py");
|
|
487
|
+
if (context.files.has(pyFile)) {
|
|
488
|
+
resolvedFile = pyFile;
|
|
489
|
+
}
|
|
490
|
+
else if (context.files.has(pyInit)) {
|
|
491
|
+
resolvedFile = pyInit;
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
// Full-stack fallback: the Python root may be a subdirectory
|
|
495
|
+
// (e.g., report/backend/ instead of report/). Walk up from the
|
|
496
|
+
// file being validated to find the directory that resolves the import.
|
|
497
|
+
let tryDir = path.dirname(filePath);
|
|
498
|
+
while (tryDir.length > basePath.length) {
|
|
499
|
+
const tryPy = path.join(tryDir, `${modulePath}.py`);
|
|
500
|
+
const tryInit = path.join(tryDir, modulePath, "__init__.py");
|
|
501
|
+
if (context.files.has(tryPy)) {
|
|
502
|
+
resolvedFile = tryPy;
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
else if (context.files.has(tryInit)) {
|
|
506
|
+
resolvedFile = tryInit;
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
tryDir = path.dirname(tryDir);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
for (const name of imp.names) {
|
|
516
|
+
// If we resolved the file, check its exports directly (Robust Check)
|
|
517
|
+
if (resolvedFile && context) {
|
|
518
|
+
const fileInfo = context.files.get(resolvedFile);
|
|
519
|
+
if (fileInfo) {
|
|
520
|
+
// Check exact exports
|
|
521
|
+
const exports = fileInfo.exports;
|
|
522
|
+
const hasExport = exports.some((e) => e.name === name.imported ||
|
|
523
|
+
(name.imported === "default" && e.isDefault));
|
|
524
|
+
if (!hasExport) {
|
|
525
|
+
// Double check against all symbols in file marked as exported
|
|
526
|
+
// (sometimes exports are implicit in simple extractors)
|
|
527
|
+
// For Python: ALL module-level names are importable (no export keyword)
|
|
528
|
+
let symExport = fileInfo.symbols.find((s) => (language === "python" || s.exported) && s.name === name.imported);
|
|
529
|
+
// For Python: check if the symbol is imported at module level in the target file.
|
|
530
|
+
// In Python, any name imported at module level is part of the module's namespace
|
|
531
|
+
// and is importable by other modules (e.g., `from app.api.cli_tokens import CLIToken`
|
|
532
|
+
// works if cli_tokens.py has `from app.models.cli_token import CLIToken`).
|
|
533
|
+
if (!symExport && language === "python") {
|
|
534
|
+
const isImportedInModule = fileInfo.imports.some((modImp) => modImp.namedImports.includes(name.imported) ||
|
|
535
|
+
modImp.defaultImport === name.imported);
|
|
536
|
+
if (isImportedInModule) {
|
|
537
|
+
// Treat as found — symbol is a valid Python namespace member via re-import
|
|
538
|
+
symExport = { name: name.imported, kind: "variable", line: 0, exported: true };
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (!symExport) {
|
|
542
|
+
// For Python: check if the imported name is a sub-module file
|
|
543
|
+
// e.g., "from app.api import auth" where app/api/auth.py exists
|
|
544
|
+
if (language === "python" && context) {
|
|
545
|
+
const resolvedDir = resolvedFile.endsWith("__init__.py")
|
|
546
|
+
? path.dirname(resolvedFile)
|
|
547
|
+
: path.dirname(resolvedFile);
|
|
548
|
+
const subModPy = path.join(resolvedDir, `${name.imported}.py`);
|
|
549
|
+
const subModInit = path.join(resolvedDir, name.imported, "__init__.py");
|
|
550
|
+
if (context.files.has(subModPy) || context.files.has(subModInit)) {
|
|
551
|
+
// Valid sub-module import
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// For Python: check pythonExports (__all__) for the module
|
|
556
|
+
// At this point, the symbol was NOT found in fileInfo.symbols.
|
|
557
|
+
// The only way it can still be valid is if __all__ explicitly lists it
|
|
558
|
+
// (meaning it was re-exported via star import or dynamic assignment).
|
|
559
|
+
if (language === "python") {
|
|
560
|
+
// Convert resolved file to module path for pythonExports lookup
|
|
561
|
+
const basePath = context?.projectPath || "";
|
|
562
|
+
const relPath = resolvedFile.startsWith(basePath)
|
|
563
|
+
? resolvedFile.slice(basePath.length + 1)
|
|
564
|
+
: resolvedFile;
|
|
565
|
+
// Convert file path to Python module path: app/utils/git_providers/__init__.py -> app.utils.git_providers
|
|
566
|
+
const pyModPath = relPath
|
|
567
|
+
.replace(/__init__\.py$/, "")
|
|
568
|
+
.replace(/\.py$/, "")
|
|
569
|
+
.replace(/\//g, ".")
|
|
570
|
+
.replace(/\.$/, "");
|
|
571
|
+
const moduleAllExports = pythonExports.get(pyModPath);
|
|
572
|
+
if (moduleAllExports && moduleAllExports.has(name.imported)) {
|
|
573
|
+
continue; // Symbol is explicitly in __all__ (re-exported) — valid
|
|
574
|
+
}
|
|
575
|
+
// Symbol is NOT in fileInfo.symbols AND NOT in __all__
|
|
576
|
+
// Fall through to flag as nonExistentImport
|
|
577
|
+
}
|
|
578
|
+
// This is a TRUE hallucination - module exists, but export doesn't
|
|
579
|
+
const allExports = exports
|
|
580
|
+
.map((e) => e.name)
|
|
581
|
+
.concat(fileInfo.symbols
|
|
582
|
+
.filter((s) => language === "python" || s.exported)
|
|
583
|
+
.map((s) => s.name));
|
|
584
|
+
const suggestion = suggestSimilar(name.imported, allExports);
|
|
585
|
+
issues.push({
|
|
586
|
+
type: "nonExistentImport",
|
|
587
|
+
severity: "critical",
|
|
588
|
+
message: `Module '${imp.module}' exists but has no export named '${name.imported}'`,
|
|
589
|
+
line: imp.line,
|
|
590
|
+
file: filePath,
|
|
591
|
+
code: getLineFromCode(newCode, imp.line),
|
|
592
|
+
suggestion,
|
|
593
|
+
confidence: 99,
|
|
594
|
+
reasoning: `Resolved module to ${resolvedFile}, but it does not export '${name.imported}'.`,
|
|
595
|
+
});
|
|
596
|
+
continue; // Skip further checks for this symbol
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
// If we found the export, map it to valid symbols
|
|
600
|
+
// We need to find the symbol in projectFunctions etc. to add it to 'valid' maps
|
|
601
|
+
// But if we can't find it in the global map (e.g. default export unnamed),
|
|
602
|
+
// we still mark the LOCAL name as valid because we verified the export exists.
|
|
603
|
+
// Construct a synthetic valid symbol if not found in global map
|
|
604
|
+
const validSym = {
|
|
605
|
+
name: name.local,
|
|
606
|
+
type: "variable", // fallback
|
|
607
|
+
file: resolvedFile,
|
|
608
|
+
};
|
|
609
|
+
validVariables.set(name.local, validSym);
|
|
610
|
+
validFunctions.set(name.local, { ...validSym, type: "function" });
|
|
611
|
+
validClasses.set(name.local, { ...validSym, type: "class" });
|
|
612
|
+
// Also try to find real symbol for better type info
|
|
613
|
+
const funcSyms = projectFunctions.get(name.imported);
|
|
614
|
+
const realSym = (funcSyms && funcSyms[0]) ||
|
|
615
|
+
projectClasses.get(name.imported) ||
|
|
616
|
+
projectVariables.get(name.imported);
|
|
617
|
+
if (realSym) {
|
|
618
|
+
if (realSym.type === "function")
|
|
619
|
+
validFunctions.set(name.local, realSym);
|
|
620
|
+
else if (realSym.type === "class")
|
|
621
|
+
validClasses.set(name.local, realSym);
|
|
622
|
+
else
|
|
623
|
+
validVariables.set(name.local, realSym);
|
|
624
|
+
}
|
|
625
|
+
continue; // Successfully validated
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// If it was a relative import and we couldn't resolve the file, THAT IS A HALLUCINATION
|
|
629
|
+
// We should NOT fall back to global lookup for relative imports if context is available
|
|
630
|
+
// BUT: Only enforce this if we have a real file path (not empty/scratchpad)
|
|
631
|
+
if (context &&
|
|
632
|
+
filePath &&
|
|
633
|
+
filePath.trim() &&
|
|
634
|
+
imp.module.startsWith(".")) {
|
|
635
|
+
if (!resolvedFile) {
|
|
636
|
+
issues.push({
|
|
637
|
+
type: "nonExistentImport",
|
|
638
|
+
severity: "critical",
|
|
639
|
+
message: `Module '${imp.module}' found in import does not exist`,
|
|
640
|
+
line: imp.line,
|
|
641
|
+
file: filePath,
|
|
642
|
+
code: getLineFromCode(newCode, imp.line),
|
|
643
|
+
suggestion: "Check the relative file path",
|
|
644
|
+
confidence: 99,
|
|
645
|
+
reasoning: `Could not resolve relative import path '${imp.module}' from '${filePath}'. File does not exist.`,
|
|
646
|
+
});
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
// FALLBACK: Old global lookup logic (only for non-relative or if context missing)
|
|
651
|
+
const importedFuncSyms = projectFunctions.get(name.imported);
|
|
652
|
+
const localFuncSyms = projectFunctions.get(name.local);
|
|
653
|
+
const projectSym = (importedFuncSyms && importedFuncSyms[0]) ||
|
|
654
|
+
(localFuncSyms && localFuncSyms[0]) ||
|
|
655
|
+
projectClasses.get(name.imported) ||
|
|
656
|
+
projectClasses.get(name.local) ||
|
|
657
|
+
projectVariables.get(name.imported) ||
|
|
658
|
+
projectVariables.get(name.local);
|
|
659
|
+
// For Python, also check if the symbol is in __all__ of the module
|
|
660
|
+
// But skip this check if the imported name is a sub-module file
|
|
661
|
+
if (language === "python" && projectSym) {
|
|
662
|
+
// First check if it's a sub-module file (sub-modules don't need __all__)
|
|
663
|
+
if (context) {
|
|
664
|
+
const moduleDir = imp.module.replace(/^\.+/, "").replace(/\./g, "/");
|
|
665
|
+
const basePath = context.projectPath;
|
|
666
|
+
let subModFound = false;
|
|
667
|
+
const subModPy = path.join(basePath, moduleDir, `${name.imported}.py`);
|
|
668
|
+
const subModInit = path.join(basePath, moduleDir, name.imported, "__init__.py");
|
|
669
|
+
if (context.files.has(subModPy) || context.files.has(subModInit)) {
|
|
670
|
+
subModFound = true;
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
// Full-stack fallback: walk up from file dir
|
|
674
|
+
let tryDir = path.dirname(filePath);
|
|
675
|
+
while (!subModFound && tryDir.length > basePath.length) {
|
|
676
|
+
if (context.files.has(path.join(tryDir, moduleDir, `${name.imported}.py`)) ||
|
|
677
|
+
context.files.has(path.join(tryDir, moduleDir, name.imported, "__init__.py"))) {
|
|
678
|
+
subModFound = true;
|
|
679
|
+
}
|
|
680
|
+
tryDir = path.dirname(tryDir);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (subModFound) {
|
|
684
|
+
// Valid sub-module import, skip __all__ check
|
|
685
|
+
validVariables.set(name.local, projectSym);
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const modulePath = imp.module
|
|
690
|
+
.replace(/^\.+/, "")
|
|
691
|
+
.replace(/[/\\]/g, ".");
|
|
692
|
+
if (!isPythonSymbolExported(modulePath, name.imported, pythonExports)) {
|
|
693
|
+
const { confidence, reasoning } = calculateConfidence({
|
|
694
|
+
issueType: "nonExistentImport",
|
|
695
|
+
symbolName: name.imported,
|
|
696
|
+
similarSymbols: [],
|
|
697
|
+
existsInProject: true,
|
|
698
|
+
strictMode,
|
|
699
|
+
});
|
|
700
|
+
issues.push({
|
|
701
|
+
type: "nonExistentImport",
|
|
702
|
+
severity: "critical",
|
|
703
|
+
message: `Symbol '${name.imported}' exists but is not exported in __all__ of '${imp.module}'`,
|
|
704
|
+
line: imp.line,
|
|
705
|
+
file: filePath,
|
|
706
|
+
code: getLineFromCode(newCode, imp.line),
|
|
707
|
+
suggestion: `Add '${name.imported}' to __all__ in ${imp.module}/__init__.py, or import directly from the submodule`,
|
|
708
|
+
confidence,
|
|
709
|
+
reasoning: `Symbol found in module but not in __all__ export list. ${reasoning}`,
|
|
710
|
+
});
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (projectSym) {
|
|
715
|
+
if (projectSym.type === "function")
|
|
716
|
+
validFunctions.set(name.local, projectSym);
|
|
717
|
+
else if (projectSym.type === "class")
|
|
718
|
+
validClasses.set(name.local, projectSym);
|
|
719
|
+
else if (projectSym.type === "variable")
|
|
720
|
+
validVariables.set(name.local, projectSym);
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
// For Python: check if the imported name is a sub-module file
|
|
724
|
+
// e.g., "from app.api import auth" where app/api/auth.py exists
|
|
725
|
+
if (language === "python" && context) {
|
|
726
|
+
const modulePath = imp.module.replace(/^\.+/, "").replace(/\./g, "/");
|
|
727
|
+
const basePath = context.projectPath;
|
|
728
|
+
let resolvedSubMod = null;
|
|
729
|
+
const subModulePy = path.join(basePath, modulePath, `${name.imported}.py`);
|
|
730
|
+
const subModuleInit = path.join(basePath, modulePath, name.imported, "__init__.py");
|
|
731
|
+
if (context.files.has(subModulePy)) {
|
|
732
|
+
resolvedSubMod = subModulePy;
|
|
733
|
+
}
|
|
734
|
+
else if (context.files.has(subModuleInit)) {
|
|
735
|
+
resolvedSubMod = subModuleInit;
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
// Full-stack fallback: walk up from file dir
|
|
739
|
+
let tryDir = path.dirname(filePath);
|
|
740
|
+
while (!resolvedSubMod && tryDir.length > basePath.length) {
|
|
741
|
+
const tryPy = path.join(tryDir, modulePath, `${name.imported}.py`);
|
|
742
|
+
const tryInit = path.join(tryDir, modulePath, name.imported, "__init__.py");
|
|
743
|
+
if (context.files.has(tryPy)) {
|
|
744
|
+
resolvedSubMod = tryPy;
|
|
745
|
+
}
|
|
746
|
+
else if (context.files.has(tryInit)) {
|
|
747
|
+
resolvedSubMod = tryInit;
|
|
748
|
+
}
|
|
749
|
+
tryDir = path.dirname(tryDir);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (resolvedSubMod) {
|
|
753
|
+
// It's a valid sub-module import — treat as a module variable
|
|
754
|
+
validVariables.set(name.local, {
|
|
755
|
+
name: name.local,
|
|
756
|
+
type: "variable",
|
|
757
|
+
file: resolvedSubMod,
|
|
758
|
+
});
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
// Symbol not found in project - this is a hallucinated import!
|
|
763
|
+
const allNames = Array.from(projectFunctions.keys())
|
|
764
|
+
.concat(Array.from(projectClasses.keys()))
|
|
765
|
+
.concat(Array.from(projectVariables.keys()));
|
|
766
|
+
const suggestion = suggestSimilar(name.imported, allNames);
|
|
767
|
+
const similarSymbols = extractSimilarSymbols(suggestion);
|
|
768
|
+
const { confidence, reasoning } = calculateConfidence({
|
|
769
|
+
issueType: "nonExistentImport",
|
|
770
|
+
symbolName: name.imported,
|
|
771
|
+
similarSymbols,
|
|
772
|
+
existsInProject: false,
|
|
773
|
+
strictMode,
|
|
774
|
+
});
|
|
775
|
+
issues.push({
|
|
776
|
+
type: "nonExistentImport",
|
|
777
|
+
severity: "critical",
|
|
778
|
+
message: `Imported symbol '${name.imported}' does not exist in module '${imp.module}'`,
|
|
779
|
+
line: imp.line,
|
|
780
|
+
file: filePath,
|
|
781
|
+
code: getLineFromCode(newCode, imp.line),
|
|
782
|
+
suggestion,
|
|
783
|
+
confidence,
|
|
784
|
+
reasoning,
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
// External imports: Treat as valid (manifest check was done in Tier 0)
|
|
791
|
+
for (const name of imp.names) {
|
|
792
|
+
const extSym = {
|
|
793
|
+
name: name.local,
|
|
794
|
+
type: "variable",
|
|
795
|
+
file: imp.module,
|
|
796
|
+
};
|
|
797
|
+
// Add to all maps as we don't know the exact type of external symbols
|
|
798
|
+
validVariables.set(name.local, extSym);
|
|
799
|
+
validFunctions.set(name.local, { ...extSym, type: "function" });
|
|
800
|
+
validClasses.set(name.local, { ...extSym, type: "class" });
|
|
801
|
+
// For Python dotted imports (e.g., `import concurrent.futures`),
|
|
802
|
+
// also register the base module name as valid since Python makes it available
|
|
803
|
+
if (language === "python" && name.local.includes(".")) {
|
|
804
|
+
const baseName = name.local.split(".")[0];
|
|
805
|
+
const baseSym = { ...extSym, name: baseName };
|
|
806
|
+
validVariables.set(baseName, baseSym);
|
|
807
|
+
validFunctions.set(baseName, { ...baseSym, type: "function" });
|
|
808
|
+
validClasses.set(baseName, { ...baseSym, type: "class" });
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
// Build set of ALL imported names (including ones that failed resolution)
|
|
814
|
+
// This prevents double-flagging: nonExistentImport + undefinedVariable for the same symbol
|
|
815
|
+
const allImportedNames = new Set();
|
|
816
|
+
for (const imp of imports) {
|
|
817
|
+
for (const name of imp.names) {
|
|
818
|
+
allImportedNames.add(name.local);
|
|
819
|
+
// For Python dotted imports: `import concurrent.futures` → also add `concurrent`
|
|
820
|
+
if (language === "python" && name.local.includes(".")) {
|
|
821
|
+
allImportedNames.add(name.local.split(".")[0]);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
// Collect locally-defined names (function params, assignments, loop vars, etc.)
|
|
826
|
+
// These are local scope variables that won't appear in the project symbol table
|
|
827
|
+
// but are valid identifiers — prevents false undefinedVariable on method calls like db.execute()
|
|
828
|
+
const localDefinitions = collectLocalDefinitionsAST(newCode, language);
|
|
829
|
+
// Lightweight literal-type inference for locally-defined variables in the new code.
|
|
830
|
+
// Used to validate method calls on obvious built-in types (e.g., array literals) without
|
|
831
|
+
// over-flagging methods on unknown values (e.g., results of createRoot()).
|
|
832
|
+
const localLiteralKinds = new Map();
|
|
833
|
+
if (language === "javascript" || language === "typescript") {
|
|
834
|
+
try {
|
|
835
|
+
const tree = parseCodeCached(newCode, language, {
|
|
836
|
+
filePath: filePath || "(new code)",
|
|
837
|
+
useCache: false,
|
|
838
|
+
});
|
|
839
|
+
const walk = (node) => {
|
|
840
|
+
if (node.type === "variable_declarator") {
|
|
841
|
+
const nameNode = node.childForFieldName("name");
|
|
842
|
+
const valueNode = node.childForFieldName("value");
|
|
843
|
+
if (nameNode?.type === "identifier" && valueNode) {
|
|
844
|
+
const name = newCode.slice(nameNode.startIndex, nameNode.endIndex);
|
|
845
|
+
if (valueNode.type === "array" || valueNode.type === "object" || valueNode.type === "string") {
|
|
846
|
+
localLiteralKinds.set(name, valueNode.type);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
for (const child of node.children || []) {
|
|
851
|
+
if (child)
|
|
852
|
+
walk(child);
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
walk(tree.rootNode);
|
|
856
|
+
}
|
|
857
|
+
catch {
|
|
858
|
+
// Best-effort inference only.
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
// Validate each used symbol
|
|
862
|
+
for (const used of usedSymbols) {
|
|
863
|
+
if (used.type === "call") {
|
|
864
|
+
const func = validFunctions.get(used.name);
|
|
865
|
+
const cls = validClasses.get(used.name);
|
|
866
|
+
const method = validMethods.get(used.name);
|
|
867
|
+
if (!func && !cls && !method) {
|
|
868
|
+
// Skip imported names — validated via import checks, not function existence
|
|
869
|
+
if (allImportedNames.has(used.name))
|
|
870
|
+
continue;
|
|
871
|
+
// Skip locally-defined functions (nested defs, local assignments that hold callables)
|
|
872
|
+
if (localDefinitions.has(used.name))
|
|
873
|
+
continue;
|
|
874
|
+
// Built-in check (Tier 1.5) - handles browser globals, standard libraries
|
|
875
|
+
if ((language === "python" && isPythonBuiltin(used.name)) ||
|
|
876
|
+
((language === "javascript" || language === "typescript") &&
|
|
877
|
+
isJSBuiltin(used.name))) {
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
// SMART TEST RELAXATION:
|
|
881
|
+
// In test files, we assume unknown functions/variables are globals (describe, it, expect)
|
|
882
|
+
// or mocks, unless strict mode is explicitly forced.
|
|
883
|
+
if (isTestFile(filePath) && !strictMode) {
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
const funcSyms = projectFunctions.get(used.name);
|
|
887
|
+
const existsInProject = (funcSyms && funcSyms.length > 0) || projectClasses.has(used.name);
|
|
888
|
+
let suggestion = "";
|
|
889
|
+
if (existsInProject) {
|
|
890
|
+
const sym = (funcSyms && funcSyms[0]) || projectClasses.get(used.name);
|
|
891
|
+
if (sym) {
|
|
892
|
+
// Calculate a descriptive suggestion including the file
|
|
893
|
+
// Note: We'd ideally calculate a relative path here, but for now,
|
|
894
|
+
// telling the user the file name is a huge win.
|
|
895
|
+
suggestion = `Add: import { ${used.name} } from './${sym.file.replace(/\\/g, "/")}'`;
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
suggestion = `Add: import { ${used.name} } from '...'`;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
suggestion = suggestSimilar(used.name, Array.from(projectFunctions.keys()));
|
|
903
|
+
}
|
|
904
|
+
const similarSymbols = extractSimilarSymbols(suggestion);
|
|
905
|
+
const { confidence, reasoning } = calculateConfidence({
|
|
906
|
+
issueType: "nonExistentFunction",
|
|
907
|
+
symbolName: used.name,
|
|
908
|
+
similarSymbols,
|
|
909
|
+
existsInProject,
|
|
910
|
+
strictMode,
|
|
911
|
+
});
|
|
912
|
+
issues.push({
|
|
913
|
+
type: "nonExistentFunction",
|
|
914
|
+
severity: "critical",
|
|
915
|
+
message: existsInProject ?
|
|
916
|
+
`Function '${used.name}' exists in your project but is not imported in this file`
|
|
917
|
+
: `Function '${used.name}' does not exist in project`,
|
|
918
|
+
line: used.line,
|
|
919
|
+
file: filePath,
|
|
920
|
+
code: used.code,
|
|
921
|
+
suggestion,
|
|
922
|
+
confidence,
|
|
923
|
+
reasoning,
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
else if (func &&
|
|
927
|
+
used.argCount !== undefined &&
|
|
928
|
+
func.paramCount !== undefined) {
|
|
929
|
+
if (used.argCount !== func.paramCount && strictMode) {
|
|
930
|
+
const { confidence, reasoning } = calculateConfidence({
|
|
931
|
+
issueType: "wrongParamCount",
|
|
932
|
+
symbolName: used.name,
|
|
933
|
+
similarSymbols: [],
|
|
934
|
+
existsInProject: true,
|
|
935
|
+
strictMode,
|
|
936
|
+
});
|
|
937
|
+
issues.push({
|
|
938
|
+
type: "wrongParamCount",
|
|
939
|
+
severity: "high",
|
|
940
|
+
message: `Function '${used.name}' expects ${func.paramCount} args, got ${used.argCount}`,
|
|
941
|
+
line: used.line,
|
|
942
|
+
file: filePath,
|
|
943
|
+
code: used.code,
|
|
944
|
+
suggestion: func.params ?
|
|
945
|
+
`Expected params: ${func.params.join(", ")}`
|
|
946
|
+
: `Check the function signature in ${func.file}`,
|
|
947
|
+
confidence,
|
|
948
|
+
reasoning,
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
else if (used.type === "methodCall") {
|
|
954
|
+
// 0. Skip whitelisted objects and chains (e.g., 'this.*', 'window.*', 'z.*', 'smthRef.current.*')
|
|
955
|
+
// Extract the root object, handling complex expressions like:
|
|
956
|
+
// - obj.property -> obj
|
|
957
|
+
// - obj?.property -> obj (optional chaining)
|
|
958
|
+
// - obj.method() -> obj
|
|
959
|
+
// - arr[index] -> arr
|
|
960
|
+
// - arr[index].property -> arr
|
|
961
|
+
// - fn().property -> fn
|
|
962
|
+
// - (expr as Type).property -> expr
|
|
963
|
+
function extractRootObject(obj) {
|
|
964
|
+
if (!obj)
|
|
965
|
+
return "";
|
|
966
|
+
// Handle parenthesized expressions at the start: (expr).prop -> extract from expr
|
|
967
|
+
if (obj.startsWith("(")) {
|
|
968
|
+
// Find the matching closing parenthesis
|
|
969
|
+
let depth = 1;
|
|
970
|
+
let i = 1;
|
|
971
|
+
while (i < obj.length && depth > 0) {
|
|
972
|
+
if (obj[i] === "(")
|
|
973
|
+
depth++;
|
|
974
|
+
else if (obj[i] === ")")
|
|
975
|
+
depth--;
|
|
976
|
+
i++;
|
|
977
|
+
}
|
|
978
|
+
if (depth === 0) {
|
|
979
|
+
// Extract the content inside the outermost parentheses
|
|
980
|
+
const innerExpr = obj.slice(1, i - 1).trim();
|
|
981
|
+
// Check if it's a type assertion (expr as Type) or (expr satisfies Type)
|
|
982
|
+
const asMatch = innerExpr.match(/^(.+?)\s+as\s+.+$/s);
|
|
983
|
+
if (asMatch) {
|
|
984
|
+
return extractRootObject(asMatch[1].trim());
|
|
985
|
+
}
|
|
986
|
+
const satisfiesMatch = innerExpr.match(/^(.+?)\s+satisfies\s+.+$/s);
|
|
987
|
+
if (satisfiesMatch) {
|
|
988
|
+
return extractRootObject(satisfiesMatch[1].trim());
|
|
989
|
+
}
|
|
990
|
+
// Not a type assertion — check if it's an arithmetic/logical/nullish expression
|
|
991
|
+
// e.g., (i + 1), (value / 1000), (milestones || []), (budgetUsedPercentage || 0)
|
|
992
|
+
// These are NOT object references — skip method validation entirely.
|
|
993
|
+
if (/[+\-*/%|&<>=!~^]/.test(innerExpr) || /\s\|\|\s/.test(innerExpr) || /\s\?\?\s/.test(innerExpr)) {
|
|
994
|
+
return ""; // Not a valid object reference
|
|
995
|
+
}
|
|
996
|
+
// Otherwise extract from the inner expression (e.g., (myObj).method())
|
|
997
|
+
return extractRootObject(innerExpr);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
// Split on delimiters and take the first part
|
|
1001
|
+
const root = obj
|
|
1002
|
+
.split("?.")[0]
|
|
1003
|
+
.split(".")[0]
|
|
1004
|
+
.split("[")[0]
|
|
1005
|
+
.split("(")[0]
|
|
1006
|
+
.trim();
|
|
1007
|
+
// If the extracted root contains spaces or operators, it's an expression, not an identifier
|
|
1008
|
+
// e.g., "i + 1" from a failed parenthesized expression extraction
|
|
1009
|
+
if (/\s/.test(root) || /[+\-*/%|&<>=!~^]/.test(root)) {
|
|
1010
|
+
return ""; // Not a valid identifier
|
|
1011
|
+
}
|
|
1012
|
+
return root;
|
|
1013
|
+
}
|
|
1014
|
+
const rootObject = extractRootObject(used.object || "");
|
|
1015
|
+
// If we can't reliably identify a root identifier (e.g. `[1,2,3].map()`),
|
|
1016
|
+
// skip method validation to avoid false positives.
|
|
1017
|
+
if (!rootObject) {
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
// Heuristic: if an internal import is used as a lowerCamelCase instance name,
|
|
1021
|
+
// allow matching against a PascalCase class name (logger -> Logger).
|
|
1022
|
+
const inferredClassName = (() => {
|
|
1023
|
+
const first = rootObject[0];
|
|
1024
|
+
if (!first || first.toUpperCase() === first)
|
|
1025
|
+
return "";
|
|
1026
|
+
const candidate = first.toUpperCase() + rootObject.slice(1);
|
|
1027
|
+
return projectClasses.has(candidate) || hasContextClass(candidate) ? candidate : "";
|
|
1028
|
+
})();
|
|
1029
|
+
const scopesToMatch = new Set([used.object, rootObject, inferredClassName].filter(Boolean));
|
|
1030
|
+
// CRITICAL FIX: Skip ALL 'this'/'self'/'cls' method calls - we can't validate class scope
|
|
1031
|
+
// The 'this' keyword (JS/TS) and 'self'/'cls' (Python) are always in scope within a class context
|
|
1032
|
+
// TypeScript/ESLint/mypy handle class method validation, not CodeGuardian
|
|
1033
|
+
if (used.object === "this" || used.object?.startsWith("this.") ||
|
|
1034
|
+
used.object === "self" || used.object?.startsWith("self.") ||
|
|
1035
|
+
used.object === "cls" || used.object?.startsWith("cls.")) {
|
|
1036
|
+
continue; // Trust class scope - this is not a hallucination risk
|
|
1037
|
+
}
|
|
1038
|
+
// Skip other whitelisted global objects and common patterns
|
|
1039
|
+
if (used.object?.includes(".current") ||
|
|
1040
|
+
(rootObject &&
|
|
1041
|
+
[
|
|
1042
|
+
// JavaScript/TypeScript globals
|
|
1043
|
+
"window",
|
|
1044
|
+
"navigator",
|
|
1045
|
+
"document",
|
|
1046
|
+
"location",
|
|
1047
|
+
"history",
|
|
1048
|
+
"localStorage",
|
|
1049
|
+
"sessionStorage",
|
|
1050
|
+
"console",
|
|
1051
|
+
"process",
|
|
1052
|
+
"global",
|
|
1053
|
+
"globalThis",
|
|
1054
|
+
"Intl",
|
|
1055
|
+
"z",
|
|
1056
|
+
"t",
|
|
1057
|
+
"db",
|
|
1058
|
+
"prisma",
|
|
1059
|
+
"ctx",
|
|
1060
|
+
"supabase",
|
|
1061
|
+
"api",
|
|
1062
|
+
"client",
|
|
1063
|
+
"auth",
|
|
1064
|
+
// Python standard library modules (commonly used as objects for method calls)
|
|
1065
|
+
"os",
|
|
1066
|
+
"sys",
|
|
1067
|
+
"json",
|
|
1068
|
+
"re",
|
|
1069
|
+
"math",
|
|
1070
|
+
"logging",
|
|
1071
|
+
"datetime",
|
|
1072
|
+
"time",
|
|
1073
|
+
"pathlib",
|
|
1074
|
+
"collections",
|
|
1075
|
+
"itertools",
|
|
1076
|
+
"functools",
|
|
1077
|
+
"typing",
|
|
1078
|
+
"abc",
|
|
1079
|
+
"io",
|
|
1080
|
+
"hashlib",
|
|
1081
|
+
"hmac",
|
|
1082
|
+
"secrets",
|
|
1083
|
+
"base64",
|
|
1084
|
+
"urllib",
|
|
1085
|
+
"http",
|
|
1086
|
+
"email",
|
|
1087
|
+
"html",
|
|
1088
|
+
"xml",
|
|
1089
|
+
"csv",
|
|
1090
|
+
"sqlite3",
|
|
1091
|
+
"subprocess",
|
|
1092
|
+
"threading",
|
|
1093
|
+
"asyncio",
|
|
1094
|
+
"uuid",
|
|
1095
|
+
"copy",
|
|
1096
|
+
"shutil",
|
|
1097
|
+
"tempfile",
|
|
1098
|
+
"glob",
|
|
1099
|
+
"fnmatch",
|
|
1100
|
+
"pickle",
|
|
1101
|
+
"struct",
|
|
1102
|
+
"traceback",
|
|
1103
|
+
"inspect",
|
|
1104
|
+
"importlib",
|
|
1105
|
+
"contextlib",
|
|
1106
|
+
"dataclasses",
|
|
1107
|
+
"enum",
|
|
1108
|
+
"string",
|
|
1109
|
+
"textwrap",
|
|
1110
|
+
"random",
|
|
1111
|
+
"statistics",
|
|
1112
|
+
"decimal",
|
|
1113
|
+
"fractions",
|
|
1114
|
+
"operator",
|
|
1115
|
+
"warnings",
|
|
1116
|
+
"unittest",
|
|
1117
|
+
"pytest",
|
|
1118
|
+
"pprint",
|
|
1119
|
+
// Python common third-party module objects
|
|
1120
|
+
"logger",
|
|
1121
|
+
"app",
|
|
1122
|
+
"request",
|
|
1123
|
+
"response",
|
|
1124
|
+
"session",
|
|
1125
|
+
"cursor",
|
|
1126
|
+
"conn",
|
|
1127
|
+
"connection",
|
|
1128
|
+
"engine",
|
|
1129
|
+
"metadata",
|
|
1130
|
+
"router",
|
|
1131
|
+
"schema",
|
|
1132
|
+
"serializer",
|
|
1133
|
+
"queryset",
|
|
1134
|
+
"manager",
|
|
1135
|
+
"admin",
|
|
1136
|
+
"signals",
|
|
1137
|
+
"celery",
|
|
1138
|
+
"redis",
|
|
1139
|
+
"cache",
|
|
1140
|
+
"config",
|
|
1141
|
+
"settings",
|
|
1142
|
+
"flask",
|
|
1143
|
+
"django",
|
|
1144
|
+
"fastapi",
|
|
1145
|
+
"sqlalchemy",
|
|
1146
|
+
"pydantic",
|
|
1147
|
+
"httpx",
|
|
1148
|
+
"requests",
|
|
1149
|
+
"aiohttp",
|
|
1150
|
+
"np",
|
|
1151
|
+
"pd",
|
|
1152
|
+
"plt",
|
|
1153
|
+
"tf",
|
|
1154
|
+
"torch",
|
|
1155
|
+
"sk",
|
|
1156
|
+
].includes(rootObject))) {
|
|
1157
|
+
// Special Case: Still validate the method name itself if it's NOT a known builtin
|
|
1158
|
+
// This ensures we catch true hallucinations like toast.hallucinatedMethod()
|
|
1159
|
+
if (isJSBuiltin(used.name) || isPythonBuiltin(used.name)) {
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
// STEALTH HALLUCINATION DETECTION:
|
|
1163
|
+
// If the code casts a well-known global object to `any`/`unknown` and calls a
|
|
1164
|
+
// non-standard method, this is a high-confidence hallucination pattern.
|
|
1165
|
+
// e.g., `(window as any).applyMolecularStability?.()`
|
|
1166
|
+
// The `as any` cast is a deliberate type-safety bypass — the developer (or AI)
|
|
1167
|
+
// knows this method doesn't exist on the type. Combined with a non-standard
|
|
1168
|
+
// method name on a well-known global, this is a strong hallucination signal.
|
|
1169
|
+
//
|
|
1170
|
+
// We only flag this for true browser/Node globals (window, document, etc.),
|
|
1171
|
+
// NOT for generic whitelisted objects (db, prisma, etc.) where `as any` may
|
|
1172
|
+
// be a legitimate workaround for missing type definitions.
|
|
1173
|
+
const STEALTH_HALLUCINATION_GLOBALS = new Set([
|
|
1174
|
+
"window", "navigator", "document", "location", "history",
|
|
1175
|
+
"localStorage", "sessionStorage", "console", "process",
|
|
1176
|
+
"global", "globalThis", "Intl",
|
|
1177
|
+
]);
|
|
1178
|
+
if (STEALTH_HALLUCINATION_GLOBALS.has(rootObject)) {
|
|
1179
|
+
const codeLine = used.code || "";
|
|
1180
|
+
if (codeLine.includes("as any") || codeLine.includes("as unknown")) {
|
|
1181
|
+
issues.push({
|
|
1182
|
+
type: "nonExistentMethod",
|
|
1183
|
+
severity: "critical",
|
|
1184
|
+
message: `Stealth hallucination: '${used.name}' does not exist on '${rootObject}' (called via unsafe 'as any' cast)`,
|
|
1185
|
+
line: used.line,
|
|
1186
|
+
file: filePath,
|
|
1187
|
+
code: used.code,
|
|
1188
|
+
suggestion: `Remove the hallucinated method call. '${rootObject}' does not have a '${used.name}' method. The 'as any' cast was used to bypass type checking.`,
|
|
1189
|
+
confidence: 96,
|
|
1190
|
+
reasoning: `Builtin global '${rootObject}' is cast to 'any'/'unknown' to call non-existent method '${used.name}'. This type-safety bypass combined with a non-standard method name is a strong indicator of AI hallucination.`,
|
|
1191
|
+
});
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
// If the method is NOT whitelisted, we still proceed to check if it exists in the project
|
|
1196
|
+
// but we've already validated the object.
|
|
1197
|
+
}
|
|
1198
|
+
// 1. Check contextual naming patterns first (e.preventDefault(), req.body, etc.)
|
|
1199
|
+
if (isContextuallyValid(used)) {
|
|
1200
|
+
continue; // Trust the vibe - this is a standard pattern
|
|
1201
|
+
}
|
|
1202
|
+
// SMART TEST RELAXATION:
|
|
1203
|
+
// Skip method checks in test files (mocks/spies often have magic methods)
|
|
1204
|
+
if (isTestFile(filePath) && !strictMode) {
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
// 3. Check if the object itself exists
|
|
1208
|
+
const objExists = validClasses.has(rootObject) ||
|
|
1209
|
+
validVariables.has(rootObject) ||
|
|
1210
|
+
validFunctions.has(rootObject) ||
|
|
1211
|
+
isJSBuiltin(rootObject) ||
|
|
1212
|
+
(language === "python" && isPythonBuiltin(rootObject)) ||
|
|
1213
|
+
// Check if the object is an imported name (including failed imports — avoids double-flagging)
|
|
1214
|
+
allImportedNames.has(rootObject) ||
|
|
1215
|
+
// Check if the object is a locally-defined variable (function params, assignments, loop vars, etc.)
|
|
1216
|
+
localDefinitions.has(rootObject) ||
|
|
1217
|
+
// Python: Check if rootObject is the base of a dotted import (e.g., `import concurrent.futures` → `concurrent`)
|
|
1218
|
+
(language === "python" && imports.some(imp => imp.names.some(n => n.local.startsWith(rootObject + ".")))) ||
|
|
1219
|
+
// Always trust common short variable names in non-strict mode
|
|
1220
|
+
(!strictMode &&
|
|
1221
|
+
[
|
|
1222
|
+
// JS/TS common
|
|
1223
|
+
"z",
|
|
1224
|
+
"t",
|
|
1225
|
+
"db",
|
|
1226
|
+
"prisma",
|
|
1227
|
+
"ctx",
|
|
1228
|
+
"req",
|
|
1229
|
+
"res",
|
|
1230
|
+
"e",
|
|
1231
|
+
"i",
|
|
1232
|
+
// Python common
|
|
1233
|
+
"self",
|
|
1234
|
+
"cls",
|
|
1235
|
+
"logger",
|
|
1236
|
+
"app",
|
|
1237
|
+
"session",
|
|
1238
|
+
"cursor",
|
|
1239
|
+
"conn",
|
|
1240
|
+
"connection",
|
|
1241
|
+
"engine",
|
|
1242
|
+
"router",
|
|
1243
|
+
"config",
|
|
1244
|
+
"settings",
|
|
1245
|
+
"request",
|
|
1246
|
+
"response",
|
|
1247
|
+
"client",
|
|
1248
|
+
"server",
|
|
1249
|
+
"cache",
|
|
1250
|
+
"registry",
|
|
1251
|
+
"factory",
|
|
1252
|
+
"builder",
|
|
1253
|
+
"handler",
|
|
1254
|
+
"manager",
|
|
1255
|
+
"service",
|
|
1256
|
+
"controller",
|
|
1257
|
+
"serializer",
|
|
1258
|
+
"validator",
|
|
1259
|
+
"middleware",
|
|
1260
|
+
"schema",
|
|
1261
|
+
"model",
|
|
1262
|
+
"form",
|
|
1263
|
+
"view",
|
|
1264
|
+
"template",
|
|
1265
|
+
"context",
|
|
1266
|
+
"fixture",
|
|
1267
|
+
"mock",
|
|
1268
|
+
"patch",
|
|
1269
|
+
"monkeypatch",
|
|
1270
|
+
].includes(rootObject));
|
|
1271
|
+
// If the object doesn't exist at all, flag it as a hallucination
|
|
1272
|
+
if (!objExists) {
|
|
1273
|
+
// Use rootObject for checking (handles complex expressions like arr[index].method())
|
|
1274
|
+
const objectToCheck = rootObject || used.object;
|
|
1275
|
+
// ... (existing undefinedVariable check) ...
|
|
1276
|
+
const suggestion = suggestSimilar(objectToCheck, [
|
|
1277
|
+
...validClasses.keys(),
|
|
1278
|
+
...validVariables.keys(),
|
|
1279
|
+
...validFunctions.keys(),
|
|
1280
|
+
]);
|
|
1281
|
+
const similarSymbols = extractSimilarSymbols(suggestion);
|
|
1282
|
+
const { confidence, reasoning } = calculateConfidence({
|
|
1283
|
+
issueType: "undefinedVariable",
|
|
1284
|
+
symbolName: objectToCheck,
|
|
1285
|
+
similarSymbols,
|
|
1286
|
+
existsInProject: projectClasses.has(objectToCheck) ||
|
|
1287
|
+
projectVariables.has(objectToCheck) ||
|
|
1288
|
+
projectFunctions.has(objectToCheck),
|
|
1289
|
+
strictMode,
|
|
1290
|
+
});
|
|
1291
|
+
issues.push({
|
|
1292
|
+
type: "undefinedVariable",
|
|
1293
|
+
severity: "critical",
|
|
1294
|
+
message: `Object '${objectToCheck}' is not defined or imported (used in ${used.object}.${used.name}())`,
|
|
1295
|
+
line: used.line,
|
|
1296
|
+
file: filePath,
|
|
1297
|
+
code: used.code,
|
|
1298
|
+
suggestion,
|
|
1299
|
+
confidence,
|
|
1300
|
+
reasoning,
|
|
1301
|
+
});
|
|
1302
|
+
continue; // Skip method validation if object doesn't exist
|
|
1303
|
+
}
|
|
1304
|
+
// Skip standard built-in methods (map, toString, read, etc.) only AFTER we've
|
|
1305
|
+
// confirmed the object exists. This preserves hallucination detection for
|
|
1306
|
+
// unknown objects calling common method names (e.g., AITaskPredictor.connect()).
|
|
1307
|
+
if (((language === "javascript" || language === "typescript") &&
|
|
1308
|
+
isJSBuiltin(used.name)) ||
|
|
1309
|
+
(language === "python" && isPythonBuiltin(used.name))) {
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
// 3. Determine if we should check the method call itself
|
|
1313
|
+
// In strict mode, we check everything.
|
|
1314
|
+
// In auto mode, we check:
|
|
1315
|
+
// - Imports from missing/hallucinated packages (we know the package is gone)
|
|
1316
|
+
// - Internal imports where we have class/method information
|
|
1317
|
+
let shouldCheck = strictMode;
|
|
1318
|
+
if (!shouldCheck) {
|
|
1319
|
+
const objectName = rootObject || used.object;
|
|
1320
|
+
const imp = imports.find((i) => i.names.some((n) => n.local === objectName || n.local === used.object));
|
|
1321
|
+
if (imp) {
|
|
1322
|
+
if (missingPackages.has(imp.module)) {
|
|
1323
|
+
shouldCheck = true; // Hallucinated import - definitely flag usages!
|
|
1324
|
+
}
|
|
1325
|
+
else if (!imp.isExternal) {
|
|
1326
|
+
// For internal imports, check if we have CLASS info
|
|
1327
|
+
const objClass = projectClasses.get(objectName) ||
|
|
1328
|
+
(inferredClassName ? projectClasses.get(inferredClassName) : undefined);
|
|
1329
|
+
if (objClass || inferredClassName) {
|
|
1330
|
+
shouldCheck = true; // We have class info, so we can validate methods
|
|
1331
|
+
}
|
|
1332
|
+
// Also check if we have scoped method info for this object
|
|
1333
|
+
// This handles const object literals with known methods:
|
|
1334
|
+
// export const projectsApi = { getProjects: async () => {...}, ... }
|
|
1335
|
+
// The AST extractor captures these as methods with scope="projectsApi",
|
|
1336
|
+
// so we know the exact shape and can validate method calls.
|
|
1337
|
+
// This is different from plain key objects (contractKeys.all) which
|
|
1338
|
+
// have no methods in the symbol table.
|
|
1339
|
+
if (!shouldCheck) {
|
|
1340
|
+
for (const [, sym] of validMethods) {
|
|
1341
|
+
if (sym.scope === objectName) {
|
|
1342
|
+
shouldCheck = true;
|
|
1343
|
+
break;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
// Also check validFunctions — object literal methods may be stored
|
|
1348
|
+
// as kind=function (not method), landing in validFunctions instead
|
|
1349
|
+
if (!shouldCheck) {
|
|
1350
|
+
for (const [, sym] of validFunctions) {
|
|
1351
|
+
if (sym.scope === objectName) {
|
|
1352
|
+
shouldCheck = true;
|
|
1353
|
+
break;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
else {
|
|
1360
|
+
// Objects not from imports (locally defined)
|
|
1361
|
+
// ONLY check if we have class info, otherwise we don't know the type enough to flag it
|
|
1362
|
+
// Use projectClasses (actual class definitions), not validClasses (which includes all imports)
|
|
1363
|
+
const objClass = projectClasses.get(objectName) ||
|
|
1364
|
+
(inferredClassName ? projectClasses.get(inferredClassName) : undefined);
|
|
1365
|
+
if ((objClass || inferredClassName) && objClass?.file !== "(new code)") {
|
|
1366
|
+
shouldCheck = true;
|
|
1367
|
+
}
|
|
1368
|
+
// Local literal inference: method calls on obvious built-in types (e.g., array literals)
|
|
1369
|
+
// can be validated without introducing false positives on unknown call results.
|
|
1370
|
+
if (!shouldCheck && objectName) {
|
|
1371
|
+
const kind = localLiteralKinds.get(objectName);
|
|
1372
|
+
if (kind === "array" || kind === "string") {
|
|
1373
|
+
shouldCheck = true;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
// console.log(`DEBUG: Method ${used.object}.${used.name} - shouldCheck: ${shouldCheck}`);
|
|
1379
|
+
if (!shouldCheck)
|
|
1380
|
+
continue;
|
|
1381
|
+
// Look up method by name, checking ALL symbols with that name (not just the first)
|
|
1382
|
+
// This handles multiple services with same-named methods (e.g., getProject on both
|
|
1383
|
+
// clientPortalService and projectsApi)
|
|
1384
|
+
const contextSyms = getContextSymbolsByName(used.name);
|
|
1385
|
+
const contextMethodSyms = contextSyms.filter((s) => s.type === "method");
|
|
1386
|
+
const contextFuncSyms = contextSyms.filter((s) => s.type === "function");
|
|
1387
|
+
const methodSyms = [
|
|
1388
|
+
...(projectMethods.get(used.name) || []),
|
|
1389
|
+
...contextMethodSyms,
|
|
1390
|
+
];
|
|
1391
|
+
const funcSyms = [
|
|
1392
|
+
...(projectFunctions.get(used.name) || []),
|
|
1393
|
+
...contextFuncSyms,
|
|
1394
|
+
];
|
|
1395
|
+
const methodSym = validMethods.get(used.name);
|
|
1396
|
+
const funcSym = validFunctions.get(used.name);
|
|
1397
|
+
// Check ALL method symbols for a scope match, not just the single representative
|
|
1398
|
+
const methodMatches = methodSyms.some((s) => !s.scope || scopesToMatch.has(s.scope)) ||
|
|
1399
|
+
(methodSym && (!methodSym.scope || scopesToMatch.has(methodSym.scope)));
|
|
1400
|
+
// Check ALL function symbols for a scope match
|
|
1401
|
+
// Local variables (from "(new code)") like `const stream = ...` should NOT
|
|
1402
|
+
// validate `ApiService.stream()` — that's a name collision, not a real method.
|
|
1403
|
+
const funcMatches = funcSyms.some((s) => (!s.scope || scopesToMatch.has(s.scope)) && s.file !== "(new code)") ||
|
|
1404
|
+
(funcSym &&
|
|
1405
|
+
(!funcSym.scope || scopesToMatch.has(funcSym.scope)) &&
|
|
1406
|
+
funcSym.file !== "(new code)");
|
|
1407
|
+
// Skip well-known inherited methods from common frameworks
|
|
1408
|
+
// These methods exist on base classes (Pydantic BaseModel, SQLAlchemy Model, etc.)
|
|
1409
|
+
// and won't appear in the project symbol table
|
|
1410
|
+
const FRAMEWORK_METHODS = new Set([
|
|
1411
|
+
// Pydantic BaseModel methods (v1 + v2)
|
|
1412
|
+
"model_validate", "model_dump", "model_json_schema", "model_copy",
|
|
1413
|
+
"model_validate_json", "model_dump_json", "model_fields_set",
|
|
1414
|
+
"model_construct", "model_post_init", "model_rebuild",
|
|
1415
|
+
"dict", "json", "parse_obj", "parse_raw", "parse_file",
|
|
1416
|
+
"from_orm", "schema", "schema_json", "validate", "update_forward_refs",
|
|
1417
|
+
"copy", "construct",
|
|
1418
|
+
// SQLAlchemy Model/Query methods
|
|
1419
|
+
"query", "filter", "filter_by", "all", "first", "one", "one_or_none",
|
|
1420
|
+
"get", "count", "delete", "update", "order_by", "limit", "offset",
|
|
1421
|
+
"join", "outerjoin", "group_by", "having", "distinct", "subquery",
|
|
1422
|
+
"scalar", "scalars", "execute", "add", "flush", "commit", "rollback",
|
|
1423
|
+
"refresh", "expire", "expunge", "merge", "close",
|
|
1424
|
+
// SQLAlchemy Column expression methods (called on Model.column attributes)
|
|
1425
|
+
"desc", "asc", "in_", "notin_", "not_in", "isnot", "is_", "is_not",
|
|
1426
|
+
"like", "ilike", "not_like", "not_ilike", "contains", "startswith", "endswith",
|
|
1427
|
+
"between", "any_", "has", "label", "cast", "op", "collate",
|
|
1428
|
+
"nullsfirst", "nullslast", "regexp_match", "regexp_replace",
|
|
1429
|
+
"concat", "distinct", "nulls_first", "nulls_last",
|
|
1430
|
+
// Django ORM methods
|
|
1431
|
+
"objects", "create", "get_or_create", "update_or_create",
|
|
1432
|
+
"bulk_create", "bulk_update", "values", "values_list",
|
|
1433
|
+
"annotate", "aggregate", "exists", "exclude", "select_related",
|
|
1434
|
+
"prefetch_related", "defer", "only", "using", "raw",
|
|
1435
|
+
"save", "full_clean", "clean", "clean_fields",
|
|
1436
|
+
]);
|
|
1437
|
+
if (FRAMEWORK_METHODS.has(used.name))
|
|
1438
|
+
continue;
|
|
1439
|
+
// Python-specific: Skip common dynamic method patterns that can't be resolved
|
|
1440
|
+
// by static analysis due to Python's dynamic typing.
|
|
1441
|
+
// In Python, `client` could be httpx.AsyncClient, requests.Session, supabase.Client, etc.
|
|
1442
|
+
// We can't determine the runtime type, so we whitelist common method names that
|
|
1443
|
+
// appear on well-known library objects.
|
|
1444
|
+
if (language === "python") {
|
|
1445
|
+
const PYTHON_DYNAMIC_METHODS = new Set([
|
|
1446
|
+
// HTTP client methods (httpx, requests, aiohttp, etc.)
|
|
1447
|
+
"post", "put", "patch", "request", "head", "options",
|
|
1448
|
+
// Python string methods called on model attribute chains (e.g., user.email.split())
|
|
1449
|
+
"split", "strip", "lstrip", "rstrip", "lower", "upper",
|
|
1450
|
+
"replace", "encode", "decode", "format", "capitalize",
|
|
1451
|
+
// Storage/external client methods (Supabase, S3, GCS, etc.)
|
|
1452
|
+
"from_", "upload", "download", "create_signed_url", "get_public_url",
|
|
1453
|
+
"get_bucket", "create_bucket", "remove", "list",
|
|
1454
|
+
// Redis client methods
|
|
1455
|
+
"setex", "setnx", "getex", "hset", "hget", "hdel", "hgetall",
|
|
1456
|
+
"lpush", "rpush", "lpop", "rpop", "lrange", "sadd", "srem", "smembers",
|
|
1457
|
+
"zadd", "zrem", "zrange", "zrangebyscore", "expire", "ttl", "pttl",
|
|
1458
|
+
"publish", "subscribe", "unsubscribe", "pipeline",
|
|
1459
|
+
// Webhook/integration client methods
|
|
1460
|
+
"create_webhook", "delete_webhook", "grant_access", "revoke_access",
|
|
1461
|
+
"get_commit_details", "get_commits", "get_branches",
|
|
1462
|
+
// Task/job object attribute access
|
|
1463
|
+
"func", "args", "kwargs", "result", "status",
|
|
1464
|
+
// Celery task methods
|
|
1465
|
+
"delay", "apply_async", "retry", "revoke",
|
|
1466
|
+
]);
|
|
1467
|
+
if (PYTHON_DYNAMIC_METHODS.has(used.name))
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1470
|
+
if (!methodMatches && !funcMatches) {
|
|
1471
|
+
// Build list of valid methods for this object type
|
|
1472
|
+
const objectMethods = [];
|
|
1473
|
+
for (const [name, sym] of validMethods) {
|
|
1474
|
+
// Include methods that either have no scope (general) or match the object
|
|
1475
|
+
if (!sym.scope || scopesToMatch.has(sym.scope)) {
|
|
1476
|
+
objectMethods.push(name);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
const suggestion = suggestSimilar(used.name, objectMethods);
|
|
1480
|
+
const similarSymbols = extractSimilarSymbols(suggestion);
|
|
1481
|
+
const { confidence, reasoning } = calculateConfidence({
|
|
1482
|
+
issueType: "nonExistentMethod",
|
|
1483
|
+
symbolName: used.name,
|
|
1484
|
+
similarSymbols,
|
|
1485
|
+
existsInProject: false,
|
|
1486
|
+
strictMode,
|
|
1487
|
+
});
|
|
1488
|
+
issues.push({
|
|
1489
|
+
type: "nonExistentMethod",
|
|
1490
|
+
severity: "medium",
|
|
1491
|
+
message: `Method '${used.name}' not found on '${used.object}' (verify manually)`,
|
|
1492
|
+
line: used.line,
|
|
1493
|
+
file: filePath,
|
|
1494
|
+
code: used.code,
|
|
1495
|
+
suggestion,
|
|
1496
|
+
confidence,
|
|
1497
|
+
reasoning,
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
else if (used.type === "instantiation") {
|
|
1502
|
+
if (!validClasses.has(used.name)) {
|
|
1503
|
+
// Skip imported names — they are validated separately
|
|
1504
|
+
if (allImportedNames.has(used.name))
|
|
1505
|
+
continue;
|
|
1506
|
+
// Skip locally-defined variables (e.g., const ParserClass = require('pdf-parse'))
|
|
1507
|
+
if (localDefinitions.has(used.name))
|
|
1508
|
+
continue;
|
|
1509
|
+
// Skip if it exists as a variable or function (dynamic class assignment)
|
|
1510
|
+
if (validVariables.has(used.name) || validFunctions.has(used.name))
|
|
1511
|
+
continue;
|
|
1512
|
+
// Built-in check (Tier 1.5)
|
|
1513
|
+
if ((language === "python" && isPythonBuiltin(used.name)) ||
|
|
1514
|
+
((language === "javascript" || language === "typescript") &&
|
|
1515
|
+
isJSBuiltin(used.name))) {
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1518
|
+
// SMART TEST RELAXATION:
|
|
1519
|
+
if (isTestFile(filePath) && !strictMode) {
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
const existsInProject = projectClasses.has(used.name);
|
|
1523
|
+
let suggestion = "";
|
|
1524
|
+
if (existsInProject) {
|
|
1525
|
+
const sym = projectClasses.get(used.name);
|
|
1526
|
+
if (sym) {
|
|
1527
|
+
suggestion = `Add: import { ${used.name} } from './${sym.file.replace(/\\/g, "/")}'`;
|
|
1528
|
+
}
|
|
1529
|
+
else {
|
|
1530
|
+
suggestion = `Add: import { ${used.name} } from '...'`;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
else {
|
|
1534
|
+
suggestion = suggestSimilar(used.name, Array.from(projectClasses.keys()));
|
|
1535
|
+
}
|
|
1536
|
+
const similarSymbols = extractSimilarSymbols(suggestion);
|
|
1537
|
+
const { confidence, reasoning } = calculateConfidence({
|
|
1538
|
+
issueType: "nonExistentClass",
|
|
1539
|
+
symbolName: used.name,
|
|
1540
|
+
similarSymbols,
|
|
1541
|
+
existsInProject,
|
|
1542
|
+
strictMode,
|
|
1543
|
+
});
|
|
1544
|
+
issues.push({
|
|
1545
|
+
type: "nonExistentClass",
|
|
1546
|
+
severity: "critical",
|
|
1547
|
+
message: existsInProject ?
|
|
1548
|
+
`Class '${used.name}' exists in your project but is not imported in this file`
|
|
1549
|
+
: `Class '${used.name}' does not exist in project`,
|
|
1550
|
+
line: used.line,
|
|
1551
|
+
file: filePath,
|
|
1552
|
+
code: used.code,
|
|
1553
|
+
suggestion,
|
|
1554
|
+
confidence,
|
|
1555
|
+
reasoning,
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
else if (used.type === "reference") {
|
|
1560
|
+
// CRITICAL FIX: Skip property access on 'this'/'self'/'cls' (e.g., this.ws, self.data, cls._client)
|
|
1561
|
+
// These are class properties and should not be validated as standalone variables
|
|
1562
|
+
if (used.object === "this" || used.object?.startsWith("this.") ||
|
|
1563
|
+
used.object === "self" || used.object?.startsWith("self.") ||
|
|
1564
|
+
used.object === "cls" || used.object?.startsWith("cls.")) {
|
|
1565
|
+
continue; // Trust class scope - properties are validated by TypeScript/mypy
|
|
1566
|
+
}
|
|
1567
|
+
const func = validFunctions.get(used.name);
|
|
1568
|
+
const cls = validClasses.get(used.name);
|
|
1569
|
+
const variable = validVariables.get(used.name);
|
|
1570
|
+
if (!func && !cls && !variable) {
|
|
1571
|
+
// Skip imported names — they are validated separately via import checks
|
|
1572
|
+
if (allImportedNames.has(used.name))
|
|
1573
|
+
continue;
|
|
1574
|
+
// Skip locally-defined variables (function params, assignments, loop vars, etc.)
|
|
1575
|
+
if (localDefinitions.has(used.name))
|
|
1576
|
+
continue;
|
|
1577
|
+
// Built-in check (Tier 1.5)
|
|
1578
|
+
if ((language === "python" && isPythonBuiltin(used.name)) ||
|
|
1579
|
+
((language === "javascript" || language === "typescript") &&
|
|
1580
|
+
(isJSBuiltin(used.name) ||
|
|
1581
|
+
isTSBuiltinType(used.name) ||
|
|
1582
|
+
[
|
|
1583
|
+
"this",
|
|
1584
|
+
"props",
|
|
1585
|
+
"state",
|
|
1586
|
+
"window",
|
|
1587
|
+
"navigator",
|
|
1588
|
+
"document",
|
|
1589
|
+
"location",
|
|
1590
|
+
"Intl",
|
|
1591
|
+
"JSON",
|
|
1592
|
+
"Math",
|
|
1593
|
+
"Date",
|
|
1594
|
+
"Array",
|
|
1595
|
+
"Object",
|
|
1596
|
+
"String",
|
|
1597
|
+
"Number",
|
|
1598
|
+
"Boolean",
|
|
1599
|
+
"Promise",
|
|
1600
|
+
"Error",
|
|
1601
|
+
"process",
|
|
1602
|
+
"global",
|
|
1603
|
+
"globalThis",
|
|
1604
|
+
"self",
|
|
1605
|
+
].includes(used.name)))) {
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
// SMART TEST RELAXATION:
|
|
1609
|
+
if (isTestFile(filePath) && !strictMode) {
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1612
|
+
const existsInProject = projectFunctions.has(used.name) ||
|
|
1613
|
+
projectClasses.has(used.name) ||
|
|
1614
|
+
projectVariables.has(used.name);
|
|
1615
|
+
let suggestion = "";
|
|
1616
|
+
if (existsInProject) {
|
|
1617
|
+
const funcSymsForRef = projectFunctions.get(used.name);
|
|
1618
|
+
const sym = (funcSymsForRef && funcSymsForRef[0]) ||
|
|
1619
|
+
projectClasses.get(used.name) ||
|
|
1620
|
+
projectVariables.get(used.name);
|
|
1621
|
+
if (sym) {
|
|
1622
|
+
suggestion = `Add: import { ${used.name} } from './${sym.file.replace(/\\/g, "/")}'`;
|
|
1623
|
+
}
|
|
1624
|
+
else {
|
|
1625
|
+
suggestion = `Add: import { ${used.name} } from '...'`;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
else {
|
|
1629
|
+
suggestion = suggestSimilar(used.name, [
|
|
1630
|
+
...projectFunctions.keys(),
|
|
1631
|
+
...projectClasses.keys(),
|
|
1632
|
+
...projectVariables.keys(),
|
|
1633
|
+
]);
|
|
1634
|
+
}
|
|
1635
|
+
const similarSymbols = extractSimilarSymbols(suggestion);
|
|
1636
|
+
const { confidence, reasoning } = calculateConfidence({
|
|
1637
|
+
issueType: "undefinedVariable",
|
|
1638
|
+
symbolName: used.name,
|
|
1639
|
+
similarSymbols,
|
|
1640
|
+
existsInProject,
|
|
1641
|
+
strictMode,
|
|
1642
|
+
});
|
|
1643
|
+
issues.push({
|
|
1644
|
+
type: "undefinedVariable",
|
|
1645
|
+
severity: "critical",
|
|
1646
|
+
message: existsInProject ?
|
|
1647
|
+
`Variable '${used.name}' exists in your project but is not imported in this file`
|
|
1648
|
+
: `Variable '${used.name}' is not defined or imported`,
|
|
1649
|
+
line: used.line,
|
|
1650
|
+
file: filePath,
|
|
1651
|
+
code: used.code,
|
|
1652
|
+
suggestion,
|
|
1653
|
+
confidence,
|
|
1654
|
+
reasoning,
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
// Tier 3: Unused Import Detection
|
|
1660
|
+
const usedNames = new Set(usedSymbols.map((u) => u.name));
|
|
1661
|
+
// Also include object names from method calls (e.g., 'logger' in 'logger.info()')
|
|
1662
|
+
for (const used of usedSymbols) {
|
|
1663
|
+
if (used.type === "methodCall" && used.object) {
|
|
1664
|
+
usedNames.add(used.object);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
// Include type references (e.g., function(req: Request))
|
|
1668
|
+
for (const typeRef of typeReferences) {
|
|
1669
|
+
usedNames.add(typeRef.name);
|
|
1670
|
+
}
|
|
1671
|
+
// For Python __init__.py files, all imports are re-exports — skip unused detection
|
|
1672
|
+
const isInitPy = language === "python" && filePath.endsWith("__init__.py");
|
|
1673
|
+
// Also build a set of names in __all__ for the current file's module
|
|
1674
|
+
let currentModuleAllExports = null;
|
|
1675
|
+
if (language === "python" && filePath && pythonExports.size > 0) {
|
|
1676
|
+
const basePath = context?.projectPath || "";
|
|
1677
|
+
const relPath = filePath.startsWith(basePath) ? filePath.slice(basePath.length + 1) : filePath;
|
|
1678
|
+
const pyModPath = relPath.replace(/__init__\.py$/, "").replace(/\.py$/, "").replace(/\//g, ".").replace(/\.$/, "");
|
|
1679
|
+
currentModuleAllExports = pythonExports.get(pyModPath) || null;
|
|
1680
|
+
}
|
|
1681
|
+
// For Python: Build a set of symbol names that appear in the code text as word boundaries
|
|
1682
|
+
// This catches usages in type annotations, decorators, attribute access, etc. that the
|
|
1683
|
+
// AST-based extractor might miss (e.g., `def foo(x: Optional[str])`, `logging.DEBUG`)
|
|
1684
|
+
let codeWordSet = null;
|
|
1685
|
+
if (language === "python") {
|
|
1686
|
+
codeWordSet = new Set();
|
|
1687
|
+
// Extract all word-like tokens from the code (excluding comment/string lines for accuracy)
|
|
1688
|
+
const lines = newCode.split("\n");
|
|
1689
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1690
|
+
const line = lines[i].trim();
|
|
1691
|
+
// Skip pure comment lines (but still scan lines with inline code + comments)
|
|
1692
|
+
if (line.startsWith("#"))
|
|
1693
|
+
continue;
|
|
1694
|
+
// Extract all identifiers from the line
|
|
1695
|
+
const words = line.match(/\b[a-zA-Z_][a-zA-Z0-9_]*\b/g);
|
|
1696
|
+
if (words) {
|
|
1697
|
+
for (const w of words)
|
|
1698
|
+
codeWordSet.add(w);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
for (const imp of imports) {
|
|
1703
|
+
for (const name of imp.names) {
|
|
1704
|
+
if (name.local === "*" || name.imported === "*")
|
|
1705
|
+
continue; // Skip wildcards
|
|
1706
|
+
if (name.imported.startsWith("React"))
|
|
1707
|
+
continue; // Skip React imports
|
|
1708
|
+
// Skip unused import checks for Python __init__.py re-export files
|
|
1709
|
+
if (isInitPy)
|
|
1710
|
+
continue;
|
|
1711
|
+
// Skip if the import is listed in the module's __all__ (it's a re-export)
|
|
1712
|
+
if (currentModuleAllExports && currentModuleAllExports.has(name.local))
|
|
1713
|
+
continue;
|
|
1714
|
+
if (!usedNames.has(name.local)) {
|
|
1715
|
+
// Python fallback: Check if any non-import line in the code contains the symbol name
|
|
1716
|
+
// This catches type annotations, decorators, attribute access, etc. that AST misses
|
|
1717
|
+
if (language === "python" && codeWordSet) {
|
|
1718
|
+
// The symbol must appear in the code AND not only on import lines
|
|
1719
|
+
if (codeWordSet.has(name.local)) {
|
|
1720
|
+
// Verify it appears on at least one non-import line
|
|
1721
|
+
const importLineText = getLineFromCode(newCode, imp.line);
|
|
1722
|
+
const appearsElsewhere = newCode.split("\n").some((line, idx) => {
|
|
1723
|
+
if (idx + 1 === imp.line)
|
|
1724
|
+
return false; // Skip the import line itself
|
|
1725
|
+
const trimmed = line.trim();
|
|
1726
|
+
if (trimmed.startsWith("#"))
|
|
1727
|
+
return false; // Skip comments
|
|
1728
|
+
if (trimmed.startsWith("import ") || trimmed.startsWith("from "))
|
|
1729
|
+
return false; // Skip other imports
|
|
1730
|
+
return new RegExp(`\\b${name.local.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(line);
|
|
1731
|
+
});
|
|
1732
|
+
if (appearsElsewhere)
|
|
1733
|
+
continue; // Used in code, skip the unused import warning
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
const { confidence, reasoning } = calculateConfidence({
|
|
1737
|
+
issueType: "unusedImport",
|
|
1738
|
+
symbolName: name.local,
|
|
1739
|
+
similarSymbols: [],
|
|
1740
|
+
existsInProject: true,
|
|
1741
|
+
strictMode,
|
|
1742
|
+
});
|
|
1743
|
+
issues.push({
|
|
1744
|
+
type: "unusedImport",
|
|
1745
|
+
severity: "warning",
|
|
1746
|
+
message: `Imported symbol '${name.local}' is never used`,
|
|
1747
|
+
line: imp.line,
|
|
1748
|
+
file: filePath,
|
|
1749
|
+
code: getLineFromCode(newCode, imp.line),
|
|
1750
|
+
suggestion: `Remove the unused import: ${name.local}`,
|
|
1751
|
+
confidence,
|
|
1752
|
+
reasoning,
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
// Tier 3.5: Type-only import misuse detection
|
|
1758
|
+
// Check if something imported with "import type" is used as a value
|
|
1759
|
+
for (const imp of imports) {
|
|
1760
|
+
if (!imp.isTypeOnly)
|
|
1761
|
+
continue; // Only check type-only imports
|
|
1762
|
+
for (const name of imp.names) {
|
|
1763
|
+
// Check if this type-imported symbol is used as a value (not just in type positions)
|
|
1764
|
+
const usages = usedSymbols.filter(u => u.name === name.local);
|
|
1765
|
+
const typeUsages = typeReferences.filter(t => t.name === name.local);
|
|
1766
|
+
// If used in runtime contexts (call, instantiation, etc.) but imported as type
|
|
1767
|
+
const runtimeUsages = usages.filter(u => u.type === "call" ||
|
|
1768
|
+
u.type === "instantiation" ||
|
|
1769
|
+
u.type === "methodCall");
|
|
1770
|
+
if (runtimeUsages.length > 0) {
|
|
1771
|
+
issues.push({
|
|
1772
|
+
type: "typeOnlyImportMisuse",
|
|
1773
|
+
severity: "high",
|
|
1774
|
+
message: `'${name.local}' is imported as a type but used as a value at runtime`,
|
|
1775
|
+
line: runtimeUsages[0].line,
|
|
1776
|
+
file: filePath,
|
|
1777
|
+
code: getLineFromCode(newCode, runtimeUsages[0].line),
|
|
1778
|
+
suggestion: `Change to regular import: import { ${name.local} } from '${imp.module}'`,
|
|
1779
|
+
confidence: 95,
|
|
1780
|
+
reasoning: `Type-only imports are erased at compile time and cannot be used for runtime values like function calls or instantiation.`,
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
return issues;
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Validate that usage of symbols follows established project patterns (Secret #5).
|
|
1789
|
+
* Detects "ritual" deviations such as missing co-occurring calls.
|
|
1790
|
+
*
|
|
1791
|
+
* @param usedSymbols - Symbols used in the new code
|
|
1792
|
+
* @param projectContext - Project context with symbol graph and patterns
|
|
1793
|
+
* @returns Array of pattern deviation issues
|
|
1794
|
+
*/
|
|
1795
|
+
export function validateUsagePatterns(usedSymbols, projectContext, filePath = "") {
|
|
1796
|
+
const issues = [];
|
|
1797
|
+
if (!projectContext.symbolGraph)
|
|
1798
|
+
return issues;
|
|
1799
|
+
// Initialize analyzer if not already done for this graph
|
|
1800
|
+
// (In a real system, patterns would be cached/pre-computed)
|
|
1801
|
+
// usagePatternAnalyzer.analyze(projectContext.symbolGraph);
|
|
1802
|
+
const usedNames = usedSymbols.map((u) => u.name);
|
|
1803
|
+
for (const used of usedSymbols) {
|
|
1804
|
+
if (used.type !== "call")
|
|
1805
|
+
continue;
|
|
1806
|
+
const deviations = usagePatternAnalyzer.checkDeviations(used.name, usedNames);
|
|
1807
|
+
for (const msg of deviations) {
|
|
1808
|
+
issues.push({
|
|
1809
|
+
type: "architecturalDeviation",
|
|
1810
|
+
severity: "warning", // Patterns are suggestions, not necessarily hard errors
|
|
1811
|
+
message: msg,
|
|
1812
|
+
line: used.line,
|
|
1813
|
+
file: filePath || projectContext.projectPath, // Best effort for patterns
|
|
1814
|
+
code: used.code,
|
|
1815
|
+
suggestion: "Verify if this ritual call is required in your context.",
|
|
1816
|
+
confidence: 70,
|
|
1817
|
+
reasoning: `Learned from ${projectContext.totalFiles} files in your project that these symbols usually appear together.`,
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return issues;
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Helper to identify test files.
|
|
1825
|
+
* Test files often contain "hallucinated" globals (describe, it, expect) and mocks.
|
|
1826
|
+
*/
|
|
1827
|
+
function isTestFile(filePath) {
|
|
1828
|
+
if (!filePath)
|
|
1829
|
+
return false;
|
|
1830
|
+
const lower = filePath.toLowerCase();
|
|
1831
|
+
return (lower.includes(".test.") ||
|
|
1832
|
+
lower.includes(".spec.") ||
|
|
1833
|
+
lower.includes("/tests/") ||
|
|
1834
|
+
lower.includes("/__tests__/") ||
|
|
1835
|
+
lower.includes("/test-utils/"));
|
|
1836
|
+
}
|
|
1837
|
+
//# sourceMappingURL=validation.js.map
|