code-auditor-mcp 3.10.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +197 -0
- package/dist/__tests__/fixtures/spec-52/item-04-storage-sql-exec-in-loop.d.ts.map +1 -1
- package/dist/__tests__/fixtures/spec-52/item-05-upsert-forms.d.ts +4 -4
- package/dist/__tests__/fixtures/spec-52/item-05-upsert-forms.d.ts.map +1 -1
- package/dist/analyzers/analyzerUtils.d.ts.map +1 -1
- package/dist/analyzers/analyzerUtils.js +2 -2
- package/dist/analyzers/analyzerUtils.js.map +1 -1
- package/dist/analyzers/applicability.d.ts.map +1 -1
- package/dist/analyzers/applicability.js +1 -0
- package/dist/analyzers/applicability.js.map +1 -1
- package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js +3 -3
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +2 -2
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +62 -17
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
- package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/documentationAnalyzer.js +14 -16
- package/dist/analyzers/documentationAnalyzer.js.map +1 -1
- package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/invariantsAnalyzer.js +30 -25
- package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
- package/dist/analyzers/orm/adapterRegistry.d.ts.map +1 -1
- package/dist/analyzers/orm/drizzleAdapter.d.ts.map +1 -1
- package/dist/analyzers/orm/prismaAdapter.d.ts.map +1 -1
- package/dist/analyzers/provenance.d.ts +10 -1
- package/dist/analyzers/provenance.d.ts.map +1 -1
- package/dist/analyzers/provenance.js +138 -1
- package/dist/analyzers/provenance.js.map +1 -1
- package/dist/analyzers/reactAnalyzer.d.ts +1 -1
- package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/reactAnalyzer.js +34 -116
- package/dist/analyzers/reactAnalyzer.js.map +1 -1
- package/dist/analyzers/ruleRegistry.d.ts +5 -3
- package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
- package/dist/analyzers/ruleRegistry.js +199 -55
- package/dist/analyzers/ruleRegistry.js.map +1 -1
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts +2 -2
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +10 -17
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +6 -6
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js +13 -13
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +266 -101
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +3 -2
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +14 -13
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +11 -11
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +19 -7
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSecretsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +2 -2
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js +93 -46
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/schema/codeAnalysis.d.ts +58 -34
- package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -1
- package/dist/analyzers/universal/schema/codeAnalysis.js +239 -97
- package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -1
- package/dist/analyzers/universal/schema/config.d.ts +6 -6
- package/dist/analyzers/universal/schema/config.d.ts.map +1 -1
- package/dist/analyzers/universal/schema/jsonSchema.js +1 -1
- package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -1
- package/dist/analyzers/universal/schema/types.d.ts +10 -0
- package/dist/analyzers/universal/schema/types.d.ts.map +1 -1
- package/dist/analyzers/universal/schema/violations.d.ts +1 -1
- package/dist/analyzers/universal/schema/violations.d.ts.map +1 -1
- package/dist/auditRouter.js +7 -7
- package/dist/auditRouter.js.map +1 -1
- package/dist/auditRunner.d.ts +1 -1
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +53 -8
- package/dist/auditRunner.js.map +1 -1
- package/dist/churn/churnExtractor.js.map +1 -1
- package/dist/cli.js +52 -24
- package/dist/cli.js.map +1 -1
- package/dist/codeIndexDB.d.ts +1 -1
- package/dist/codeIndexDB.d.ts.map +1 -1
- package/dist/codeIndexDB.js +164 -18
- package/dist/codeIndexDB.js.map +1 -1
- package/dist/codeIndexService.d.ts.map +1 -1
- package/dist/codeIndexService.js.map +1 -1
- package/dist/componentScanner.d.ts.map +1 -1
- package/dist/componentScanner.js +89 -28
- package/dist/componentScanner.js.map +1 -1
- package/dist/config/configLoader.js +1 -1
- package/dist/config/configLoader.js.map +1 -1
- package/dist/config/defaults.d.ts +2 -2
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +10 -3
- package/dist/config/defaults.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/conventions/conventionMiner.d.ts.map +1 -1
- package/dist/conventions/conventionMiner.js +3 -10
- package/dist/conventions/conventionMiner.js.map +1 -1
- package/dist/coverage/lcovParser.js.map +1 -1
- package/dist/daemon/args.js.map +1 -1
- package/dist/daemon/core.d.ts.map +1 -1
- package/dist/daemon/core.js +50 -46
- package/dist/daemon/core.js.map +1 -1
- package/dist/daemon/lspServer.d.ts.map +1 -1
- package/dist/daemon/lspServer.js +1 -1
- package/dist/daemon/lspServer.js.map +1 -1
- package/dist/daemon/main.d.ts.map +1 -1
- package/dist/daemon/main.js +14 -4
- package/dist/daemon/main.js.map +1 -1
- package/dist/daemon/resolve.js.map +1 -1
- package/dist/daemon/socketServer.d.ts.map +1 -1
- package/dist/enforcement/gate.js +2 -2
- package/dist/functionScanner.d.ts.map +1 -1
- package/dist/functionScanner.js +47 -42
- package/dist/functionScanner.js.map +1 -1
- package/dist/generators/AWSQConfigGenerator.d.ts.map +1 -1
- package/dist/generators/AiderConfigGenerator.d.ts.map +1 -1
- package/dist/generators/BaseConfigGenerator.d.ts.map +1 -1
- package/dist/generators/BaseConfigGenerator.js.map +1 -1
- package/dist/generators/ClaudeConfigGenerator.d.ts.map +1 -1
- package/dist/generators/ClineConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CodeiumConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CodexConfigGenerator.d.ts.map +1 -1
- package/dist/generators/ConfigGeneratorFactory.d.ts.map +1 -1
- package/dist/generators/ConfigGeneratorFactory.js.map +1 -1
- package/dist/generators/ContinueConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CopilotConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CursorConfigGenerator.d.ts.map +1 -1
- package/dist/generators/GeminiConfigGenerator.d.ts.map +1 -1
- package/dist/generators/JetBrainsConfigGenerator.d.ts.map +1 -1
- package/dist/generators/VSCodeConfigGenerator.d.ts.map +1 -1
- package/dist/generators/ZCodeConfigGenerator.d.ts.map +1 -1
- package/dist/graph/importClassification.d.ts +114 -0
- package/dist/graph/importClassification.d.ts.map +1 -0
- package/dist/graph/importClassification.js +393 -0
- package/dist/graph/importClassification.js.map +1 -0
- package/dist/graph/importGraph.d.ts +1 -0
- package/dist/graph/importGraph.d.ts.map +1 -1
- package/dist/graph/importGraph.js +1 -1
- package/dist/graph/importGraph.js.map +1 -1
- package/dist/graph/outputFormatter.js.map +1 -1
- package/dist/hooks/codex.d.ts.map +1 -1
- package/dist/hooks/codex.js +1 -1
- package/dist/hooks/codex.js.map +1 -1
- package/dist/hooks/core.d.ts +1 -1
- package/dist/hooks/core.d.ts.map +1 -1
- package/dist/hooks/core.js +4 -4
- package/dist/hooks/core.js.map +1 -1
- package/dist/hooks/cursor.d.ts.map +1 -1
- package/dist/hooks/cursor.js +2 -2
- package/dist/hooks/cursor.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/installer.d.ts +13 -7
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +25 -15
- package/dist/installer.js.map +1 -1
- package/dist/invariants/invariant-rules.schema.json +1 -1
- package/dist/invariants/ruleEngine.js.map +1 -1
- package/dist/invariants/ruleValidator.js +1 -1
- package/dist/invariants/ruleValidator.js.map +1 -1
- package/dist/invariants/types.d.ts +1 -1
- package/dist/invariants/types.d.ts.map +1 -1
- package/dist/languages/LanguageOrchestrator.d.ts +3 -2
- package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
- package/dist/languages/LanguageOrchestrator.js +1 -1
- package/dist/languages/LanguageOrchestrator.js.map +1 -1
- package/dist/languages/LanguageRegistry.d.ts.map +1 -1
- package/dist/languages/RuntimeManager.d.ts +20 -11
- package/dist/languages/RuntimeManager.d.ts.map +1 -1
- package/dist/languages/RuntimeManager.js +151 -51
- package/dist/languages/RuntimeManager.js.map +1 -1
- package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
- package/dist/languages/UniversalAnalyzer.js.map +1 -1
- package/dist/languages/adapterBridge.d.ts +10 -0
- package/dist/languages/adapterBridge.d.ts.map +1 -1
- package/dist/languages/adapterBridge.js +30 -7
- package/dist/languages/adapterBridge.js.map +1 -1
- package/dist/languages/go/GoAdapter.d.ts +1 -19
- package/dist/languages/go/GoAdapter.d.ts.map +1 -1
- package/dist/languages/go/GoAdapter.js +33 -116
- package/dist/languages/go/GoAdapter.js.map +1 -1
- package/dist/languages/go/analyzer +0 -0
- package/dist/languages/go/analyzer-src/analyzer.go +8 -8
- package/dist/languages/go/analyzer-src/solid.go +5 -5
- package/dist/languages/goBinary.d.ts +46 -0
- package/dist/languages/goBinary.d.ts.map +1 -0
- package/dist/languages/goBinary.js +169 -0
- package/dist/languages/goBinary.js.map +1 -0
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts +1 -28
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts.map +1 -1
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.js +3 -42
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.js.map +1 -1
- package/dist/languages/tree-sitter/converter.d.ts.map +1 -1
- package/dist/languages/tree-sitter/converter.js +2 -1
- package/dist/languages/tree-sitter/converter.js.map +1 -1
- package/dist/languages/tree-sitter/parser.js.map +1 -1
- package/dist/languages/tree-sitter/rawNode.d.ts +31 -0
- package/dist/languages/tree-sitter/rawNode.d.ts.map +1 -0
- package/dist/languages/tree-sitter/rawNode.js +38 -0
- package/dist/languages/tree-sitter/rawNode.js.map +1 -0
- package/dist/languages/types.d.ts +15 -21
- package/dist/languages/types.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +12 -32
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +103 -203
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
- package/dist/ledger.d.ts +3 -0
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +45 -9
- package/dist/ledger.js.map +1 -1
- package/dist/mcp-tools/projectTasks.d.ts +37 -37
- package/dist/mcp-tools/projectTasks.js +2 -2
- package/dist/mcp-tools/projectTasks.js.map +1 -1
- package/dist/mcp-tools/whitelistTools.d.ts +17 -17
- package/dist/mcp-tools/whitelistTools.d.ts.map +1 -1
- package/dist/mcp-tools/workflowGuide.js +3 -3
- package/dist/mcp-tools/workflowGuide.js.map +1 -1
- package/dist/mcp-tools-shared.d.ts.map +1 -1
- package/dist/mcp-tools-shared.js +13 -13
- package/dist/mcp-tools-shared.js.map +1 -1
- package/dist/mcp-ui-simple.js +7 -7
- package/dist/mcp-ui-simple.js.map +1 -1
- package/dist/mcp.js +14 -14
- package/dist/mcp.js.map +1 -1
- package/dist/mcpAuditJobs.js +7 -7
- package/dist/mcpAuditJobs.js.map +1 -1
- package/dist/mcpAutoIndex.js +1 -1
- package/dist/mcpAutoIndex.js.map +1 -1
- package/dist/mcpToolErrors.d.ts.map +1 -1
- package/dist/mcpToolErrors.js.map +1 -1
- package/dist/native-bootstrap.js.map +1 -1
- package/dist/nextFile.d.ts +2 -2
- package/dist/nextFile.d.ts.map +1 -1
- package/dist/nextFile.js +19 -5
- package/dist/nextFile.js.map +1 -1
- package/dist/nextFileIncremental.d.ts +10 -2
- package/dist/nextFileIncremental.d.ts.map +1 -1
- package/dist/nextFileIncremental.js +45 -16
- package/dist/nextFileIncremental.js.map +1 -1
- package/dist/pipeline.d.ts +2 -2
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +35 -0
- package/dist/pipeline.js.map +1 -1
- package/dist/pipelineAdapters.d.ts +17 -0
- package/dist/pipelineAdapters.d.ts.map +1 -1
- package/dist/pipelineAdapters.js +356 -197
- package/dist/pipelineAdapters.js.map +1 -1
- package/dist/reporting/csvReportGenerator.d.ts.map +1 -1
- package/dist/reporting/csvReportGenerator.js +6 -6
- package/dist/reporting/csvReportGenerator.js.map +1 -1
- package/dist/reporting/htmlReportGenerator.d.ts.map +1 -1
- package/dist/reporting/htmlReportGenerator.js +11 -11
- package/dist/reporting/htmlReportGenerator.js.map +1 -1
- package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
- package/dist/reporting/jsonReportGenerator.js +2 -2
- package/dist/reporting/jsonReportGenerator.js.map +1 -1
- package/dist/reporting/reportGenerator.d.ts.map +1 -1
- package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
- package/dist/reporting/sarifReportGenerator.js +1 -1
- package/dist/reporting/sarifReportGenerator.js.map +1 -1
- package/dist/scripts/benchmarkWorkers.js +1 -1
- package/dist/scripts/benchmarkWorkers.js.map +1 -1
- package/dist/search/QueryParser.d.ts.map +1 -1
- package/dist/search/QueryParser.js.map +1 -1
- package/dist/services/CodeMapGenerator.d.ts.map +1 -1
- package/dist/services/CodeMapGenerator.js.map +1 -1
- package/dist/services/ProjectTaskRepository.d.ts.map +1 -1
- package/dist/services/ProjectTaskRepository.js.map +1 -1
- package/dist/services/auditJobService.js.map +1 -1
- package/dist/services/fileAccounting.d.ts.map +1 -1
- package/dist/services/projectTaskHelpers.js.map +1 -1
- package/dist/services/whitelistService.d.ts.map +1 -1
- package/dist/sqlite/betterSqlite3.d.ts.map +1 -1
- package/dist/sqlite/betterSqlite3.js.map +1 -1
- package/dist/sqlite/nodeSqlite.d.ts.map +1 -1
- package/dist/sqlite/nodeSqlite.js.map +1 -1
- package/dist/structuralSignature.js.map +1 -1
- package/dist/styles/cssAstExtractor.d.ts +2 -2
- package/dist/styles/cssAstExtractor.d.ts.map +1 -1
- package/dist/styles/cssAstExtractor.js +67 -111
- package/dist/styles/cssAstExtractor.js.map +1 -1
- package/dist/styles/styleExtractor.js.map +1 -1
- package/dist/styles/styleIndexer.d.ts.map +1 -1
- package/dist/styles/styleIndexer.js +19 -6
- package/dist/styles/styleIndexer.js.map +1 -1
- package/dist/styles/tailwindConfigLoader.js.map +1 -1
- package/dist/styles/tailwindProbe.d.ts.map +1 -1
- package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
- package/dist/styles/tailwindUtilityExpander.js.map +1 -1
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js.map +1 -1
- package/dist/tool-registry.d.ts.map +1 -1
- package/dist/types/crossLanguage.d.ts +1 -1
- package/dist/types/crossLanguage.d.ts.map +1 -1
- package/dist/types.d.ts +67 -19
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -5
- package/dist/types.js.map +1 -1
- package/dist/ui/InteractivePrompts.d.ts.map +1 -1
- package/dist/ui/InteractivePrompts.js.map +1 -1
- package/dist/utils/astParser.d.ts +1 -17
- package/dist/utils/astParser.d.ts.map +1 -1
- package/dist/utils/astParser.js +18 -60
- package/dist/utils/astParser.js.map +1 -1
- package/dist/utils/astUtils.d.ts +12 -12
- package/dist/utils/astUtils.d.ts.map +1 -1
- package/dist/utils/astUtils.js +83 -82
- package/dist/utils/astUtils.js.map +1 -1
- package/dist/utils/componentPatterns.js.map +1 -1
- package/dist/utils/componentResponsibility.d.ts +8 -8
- package/dist/utils/componentResponsibility.d.ts.map +1 -1
- package/dist/utils/componentResponsibility.js +33 -38
- package/dist/utils/componentResponsibility.js.map +1 -1
- package/dist/utils/dependencyExtractor.d.ts +3 -3
- package/dist/utils/dependencyExtractor.d.ts.map +1 -1
- package/dist/utils/dependencyExtractor.js +26 -30
- package/dist/utils/dependencyExtractor.js.map +1 -1
- package/dist/utils/fileDiscovery.d.ts.map +1 -1
- package/dist/utils/fileDiscovery.js +65 -0
- package/dist/utils/fileDiscovery.js.map +1 -1
- package/dist/utils/jsDocParser.d.ts +6 -6
- package/dist/utils/jsDocParser.d.ts.map +1 -1
- package/dist/utils/jsDocParser.js +18 -21
- package/dist/utils/jsDocParser.js.map +1 -1
- package/dist/utils/reactDetection.d.ts +7 -7
- package/dist/utils/reactDetection.d.ts.map +1 -1
- package/dist/utils/reactDetection.js +42 -49
- package/dist/utils/reactDetection.js.map +1 -1
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.d.ts.map +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/version.generated.js.map +1 -1
- package/dist/workers/auditWorker.js.map +1 -1
- package/package.json +4 -3
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/README.md +5 -4
- package/plugin/hooks/hooks.json +11 -0
- package/plugin/scripts/hook-common.sh +85 -31
- package/plugin/scripts/hook-warm.sh +44 -0
- package/plugin/skills/code-auditor/SKILL-RULE-KINDS.md +28 -21
- package/plugin/skills/code-auditor/SKILL.md +10 -10
- package/server.json +2 -2
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { UniversalAnalyzer } from '../../languages/UniversalAnalyzer.js';
|
|
7
7
|
import { withRuleTiming } from '../ruleTiming.js';
|
|
8
|
-
import { buildProvenanceContext, isDBProvenanced, DB_CALL_METHODS, } from '../provenance.js';
|
|
8
|
+
import { buildProvenanceContext, isDBProvenanced, getCallExpressionCallee, DB_CALL_METHODS, ORM_METHODS, } from '../provenance.js';
|
|
9
9
|
import { DB_RECEIVER_NAMES, DB_CALL_METHOD_NAMES, DB_BINDING_NAMES, DB_WRAPPER_NAMES, } from './UniversalSchemaAnalyzer.js';
|
|
10
|
-
import { isSqlKeyword } from './schema/codeAnalysis.js';
|
|
10
|
+
import { isSqlKeyword, findEnclosingFunctionIdentity, functionIdentityLabel } from './schema/codeAnalysis.js';
|
|
11
11
|
import { isTestOrSpecPath } from '../../languages/testConventions.js';
|
|
12
12
|
/**
|
|
13
13
|
* SQL keywords recognized as evidence that a string is a SQL query.
|
|
@@ -209,7 +209,8 @@ function buildDatabaseCall(node, ast, scan) {
|
|
|
209
209
|
hasFilter,
|
|
210
210
|
hasParameterizedQuery: security.parameterized,
|
|
211
211
|
hasSqlInjectionRisk: security.injectionRisk,
|
|
212
|
-
|
|
212
|
+
sqlEscaped: security.escaped,
|
|
213
|
+
enclosingFunction: enclosingIdentity(node, adapter, ast.filePath),
|
|
213
214
|
};
|
|
214
215
|
}
|
|
215
216
|
/**
|
|
@@ -307,36 +308,57 @@ function checkViolations(call, analysis, ctx) {
|
|
|
307
308
|
const violations = [];
|
|
308
309
|
const symbol = nextSymbol(call.enclosingFunction ?? 'top-level', call.method, symbolOrdinals);
|
|
309
310
|
const push = (message, opts) => violations.push(makeViolation(filePath, { line: call.line, column: call.column }, message, { ...opts, symbol }));
|
|
310
|
-
// Security: SQL injection
|
|
311
|
-
// now
|
|
311
|
+
// Security: SQL injection. Raw unescaped interpolation of input is a live
|
|
312
|
+
// vulnerability now → `critical`. Manual quote-escaping
|
|
313
|
+
// (`.replace(/'/g, "''")`) is *defended* — single-quote doubling handles only
|
|
314
|
+
// the single-quote vector, not backslash escapes or unicode quote variants —
|
|
315
|
+
// so it downgrades to `advisory` ("verify escaping") rather than asserting a
|
|
316
|
+
// certified vulnerability.
|
|
312
317
|
if (config.checkSQLInjection && call.hasSqlInjectionRisk) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
318
|
+
if (call.sqlEscaped) {
|
|
319
|
+
push(`Interpolated SQL in ${call.method} — verify escaping is sufficient. Use parameterized queries.`, {
|
|
320
|
+
severity: 'advisory',
|
|
321
|
+
rule: 'sql-injection-risk',
|
|
322
|
+
resolution: {
|
|
323
|
+
action: 'parameterize',
|
|
324
|
+
summary: `The SQL in ${call.method} interpolates a manually quote-escaped value. Quote-doubling defends only the single-quote case (not backslash escapes, unicode quote variants, or numeric/identifier positions) — replace the interpolation with a parameterized query (\`?\`, \`$1\`, or \`:name\`) for a full guarantee.`,
|
|
325
|
+
symbols: [call.method],
|
|
326
|
+
files: [filePath],
|
|
327
|
+
lines: [call.line],
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
push(`Potential SQL injection risk in ${call.method}. Use parameterized queries.`, {
|
|
333
|
+
severity: 'critical',
|
|
334
|
+
rule: 'sql-injection-risk',
|
|
335
|
+
resolution: {
|
|
336
|
+
action: 'parameterize',
|
|
337
|
+
summary: `Replace the string-interpolated SQL in ${call.method} with a parameterized query — bind values via the driver's placeholder form (\`?\`, \`$1\`, or \`:name\`) instead of concatenating them into the statement.`,
|
|
338
|
+
symbols: [call.method],
|
|
339
|
+
files: [filePath],
|
|
340
|
+
lines: [call.line],
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
}
|
|
324
344
|
}
|
|
325
|
-
// Security: Missing Organization Filter
|
|
345
|
+
// Security: Missing Organization Filter — a query on a tenant-scoped table with
|
|
346
|
+
// no org/tenant filter is a live data-isolation breach: one tenant reads
|
|
347
|
+
// another's rows. `critical` (already wrong in production), not `severe`.
|
|
326
348
|
if (config.checkOrgFilters && !call.hasOrganizationFilter && call.tables.length > 0 && requiresOrgFilter(call.tables, config)) {
|
|
327
|
-
push(`Query on ${call.tables.join(', ')} missing organization/tenant filter`, { severity: '
|
|
349
|
+
push(`Query on ${call.tables.join(', ')} missing organization/tenant filter`, { severity: 'critical', rule: 'missing-org-filter' });
|
|
328
350
|
}
|
|
329
351
|
// Performance: Complex Query — a join-heavy query (many tables). Spec 55 R5:
|
|
330
352
|
// a subquery alone is no longer "complex" — it is an ordinary SQLite/D1 idiom.
|
|
331
353
|
if (analysis.performanceRisk === 'high') {
|
|
332
|
-
push(`Query references ${call.tables.length} tables`, { severity: '
|
|
354
|
+
push(`Query references ${call.tables.length} tables`, { severity: 'severe', rule: 'complex-query' });
|
|
333
355
|
}
|
|
334
356
|
// Performance: Unfiltered Query — an unfiltered write (DELETE/UPDATE with no
|
|
335
357
|
// row-limiting clause), the classic mass-mutation foot-gun. Spec 55 R3: it is
|
|
336
358
|
// a query-shape rule excluded from test files; R5: it is about writes now, not
|
|
337
359
|
// reads (an unfiltered SELECT is often an intentional full-set load).
|
|
338
360
|
if (!skipTestRules && analysis.performanceRisk === 'medium') {
|
|
339
|
-
push(`Unfiltered write on ${call.tables.join(', ')} has no WHERE/HAVING/LIMIT`, { severity: '
|
|
361
|
+
push(`Unfiltered write on ${call.tables.join(', ')} has no WHERE/HAVING/LIMIT`, { severity: 'severe', rule: 'unfiltered-query' });
|
|
340
362
|
}
|
|
341
363
|
return violations;
|
|
342
364
|
}
|
|
@@ -360,7 +382,7 @@ function checkGeneralPatterns(ast, adapter, sourceCode, config) {
|
|
|
360
382
|
for (const node of stringNodes) {
|
|
361
383
|
const text = adapter.getNodeText(node, sourceCode);
|
|
362
384
|
if (isConnectionString(text)) {
|
|
363
|
-
const fnName =
|
|
385
|
+
const fnName = enclosingIdentity(node, adapter, ast.filePath);
|
|
364
386
|
const baseSym = `${fnName}:hardcoded-connection`;
|
|
365
387
|
const count = (hardcodedOrdinals.get(baseSym) ?? 0) + 1;
|
|
366
388
|
hardcodedOrdinals.set(baseSym, count);
|
|
@@ -660,6 +682,64 @@ function isStatementConstructionOnly(node, adapter, sourceCode) {
|
|
|
660
682
|
const method = dbCallMethodName(node, adapter, sourceCode);
|
|
661
683
|
return (method === 'prepare' || method === 'bind') && !hasEagerMethodInCallChain(node, adapter, sourceCode);
|
|
662
684
|
}
|
|
685
|
+
/**
|
|
686
|
+
* True when a db-call node is SQL-string construction inside a query compiler:
|
|
687
|
+
* an ORM *builder* method (e.g. `aggregate`, `select`, `where` — never an eager
|
|
688
|
+
* I/O method) invoked on an unprovenanced `this` receiver whose result is
|
|
689
|
+
* accumulated into an array via `.push(...)`. knex's query compilers build SQL
|
|
690
|
+
* exactly this way — `sql.push(...this.aggregate(stmt))` inside a
|
|
691
|
+
* column-iteration loop — and the call returns a string fragment, not a query
|
|
692
|
+
* result, so the loop is not an N+1 (Spec 52 R1 extension). Eager methods
|
|
693
|
+
* (`run`/`all`/`exec`/`query`/…) are excluded, so a genuine
|
|
694
|
+
* `results.push(db.query(...))` still fires.
|
|
695
|
+
*/
|
|
696
|
+
function isSqlStringConstruction(node, adapter, sourceCode) {
|
|
697
|
+
const method = dbCallMethodName(node, adapter, sourceCode);
|
|
698
|
+
if (!method || EAGER_DB_METHODS.has(method))
|
|
699
|
+
return false;
|
|
700
|
+
if (!ORM_METHODS.has(method))
|
|
701
|
+
return false;
|
|
702
|
+
const call = resolveDbCallNode(node, adapter);
|
|
703
|
+
if (!call)
|
|
704
|
+
return false;
|
|
705
|
+
const memberExpr = findMemberCallee(call, adapter);
|
|
706
|
+
if (!memberExpr)
|
|
707
|
+
return false;
|
|
708
|
+
// The receiver must be an unprovenanced `this`/`super` — the query-compiler
|
|
709
|
+
// object itself. `this.db.aggregate(...)` (receiver `this.db`) would be a real
|
|
710
|
+
// ORM query on a provenanced `db`, not a compiler fragment.
|
|
711
|
+
const objectText = memberObjectText(memberExpr, adapter, sourceCode);
|
|
712
|
+
if (objectText !== 'this' && objectText !== 'super')
|
|
713
|
+
return false;
|
|
714
|
+
return isConsumedByArrayPush(call, adapter, sourceCode);
|
|
715
|
+
}
|
|
716
|
+
/** The object text of a member_expression (`this.aggregate` → `this`). */
|
|
717
|
+
function memberObjectText(node, adapter, sourceCode) {
|
|
718
|
+
const object = adapter.getChildren(node).find((c) => {
|
|
719
|
+
const t = adapter.getNodeType(c);
|
|
720
|
+
return t !== 'property_identifier' && t !== 'field_identifier' && t !== '.';
|
|
721
|
+
});
|
|
722
|
+
return object ? adapter.getNodeText(object, sourceCode) : null;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* True when a call's result is consumed by an array `.push(...)` — the call is
|
|
726
|
+
* an argument (possibly spread) of a `push` invocation. The accumulation
|
|
727
|
+
* signature (`sql.push(...this.aggregate(stmt))`) marks the value as a string
|
|
728
|
+
* fragment being collected, not a query result being read.
|
|
729
|
+
*/
|
|
730
|
+
function isConsumedByArrayPush(call, adapter, sourceCode) {
|
|
731
|
+
let cur = adapter.getParent(call);
|
|
732
|
+
if (cur && adapter.getNodeType(cur) === 'spread_element') {
|
|
733
|
+
cur = adapter.getParent(cur);
|
|
734
|
+
}
|
|
735
|
+
if (!cur || adapter.getNodeType(cur) !== 'arguments')
|
|
736
|
+
return false;
|
|
737
|
+
const pushCall = adapter.getParent(cur);
|
|
738
|
+
if (!pushCall || adapter.getNodeType(pushCall) !== 'call_expression')
|
|
739
|
+
return false;
|
|
740
|
+
const pushMember = findMemberCallee(pushCall, adapter);
|
|
741
|
+
return !!pushMember && memberPropertyName(pushMember, adapter, sourceCode) === 'push';
|
|
742
|
+
}
|
|
663
743
|
/** True when a call's arguments contain a spread_element (…binds). */
|
|
664
744
|
function hasSpreadArgument(call, adapter) {
|
|
665
745
|
const args = adapter.getChildren(call).find(c => adapter.getNodeType(c) === 'arguments');
|
|
@@ -754,10 +834,12 @@ function isWrapperFunctionWithBindParams(node, adapter, sourceCode, wrapperNames
|
|
|
754
834
|
if (!call || adapter.getNodeType(call) !== 'call_expression')
|
|
755
835
|
return false;
|
|
756
836
|
// Check if the callee is a simple identifier (not member expression)
|
|
757
|
-
// matching one of the wrapper names.
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
837
|
+
// matching one of the wrapper names. Use the shared callee extractor so an
|
|
838
|
+
// `await fn<T>(...)` call — whose callee is an `await_expression` wrapping the
|
|
839
|
+
// identifier, plus a `type_arguments` child — resolves the same way the
|
|
840
|
+
// provenance detector does. A naive `children.find(identifier)` misses it.
|
|
841
|
+
const callee = getCallExpressionCallee(call, adapter);
|
|
842
|
+
if (!callee || adapter.getNodeType(callee) !== 'identifier')
|
|
761
843
|
return false;
|
|
762
844
|
const calleeName = adapter.getNodeText(callee, sourceCode);
|
|
763
845
|
if (!wrapperNames.includes(calleeName))
|
|
@@ -1048,17 +1130,35 @@ function isUnfilteredWrite(call) {
|
|
|
1048
1130
|
* wrapper function with bind params). All four short-circuit checkQuerySecurity
|
|
1049
1131
|
* to a "safe" result.
|
|
1050
1132
|
*/
|
|
1051
|
-
function isParameterizedByChain(node, adapter, sourceCode, config) {
|
|
1133
|
+
function isParameterizedByChain(node, adapter, sourceCode, config, wrapperNames) {
|
|
1052
1134
|
if (isInPrepareBindChain(node, adapter, sourceCode))
|
|
1053
1135
|
return true;
|
|
1054
1136
|
if (isInExecChain(node, adapter, sourceCode))
|
|
1055
1137
|
return true;
|
|
1056
1138
|
if (isD1ConvenienceCall(node, adapter, sourceCode))
|
|
1057
1139
|
return true;
|
|
1058
|
-
if (isWrapperFunctionWithBindParams(node, adapter, sourceCode,
|
|
1140
|
+
if (isWrapperFunctionWithBindParams(node, adapter, sourceCode, wrapperNames))
|
|
1059
1141
|
return true;
|
|
1060
1142
|
return false;
|
|
1061
1143
|
}
|
|
1144
|
+
/**
|
|
1145
|
+
* The effective set of DB-wrapper names the FP guards recognize as
|
|
1146
|
+
* parameterized. This is the union of the static `config.dbWrapperNames`
|
|
1147
|
+
* (d1Query, d1Exec) and the names the provenance detector *learned* at
|
|
1148
|
+
* file-scan time (`reason: 'wrapper'` — e.g. a bare `d1(sql, params)` D1 REST
|
|
1149
|
+
* helper). Keeping the two in sync is what prevents a wrapper the detector
|
|
1150
|
+
* already treats as DB-bound from being re-flagged as raw interpolation by
|
|
1151
|
+
* `checkQuerySecurity`.
|
|
1152
|
+
*/
|
|
1153
|
+
function effectiveWrapperNames(scan) {
|
|
1154
|
+
const staticNames = scan.config.dbWrapperNames ?? [];
|
|
1155
|
+
const learned = scan.provenanceContext
|
|
1156
|
+
? [...scan.provenanceContext.dbProvenanced.values()]
|
|
1157
|
+
.filter(ev => ev.reason === 'wrapper')
|
|
1158
|
+
.map(ev => ev.identifier)
|
|
1159
|
+
: [];
|
|
1160
|
+
return [...new Set([...staticNames, ...learned])];
|
|
1161
|
+
}
|
|
1062
1162
|
/**
|
|
1063
1163
|
* True when a single dynamic interpolation part is provably safe — via a
|
|
1064
1164
|
* config-driven sanitizer allowlist, the adapter's cross-function safety
|
|
@@ -1104,30 +1204,33 @@ function checkQuerySecurity(node, text, ast, scan) {
|
|
|
1104
1204
|
const { adapter, sourceCode, config } = scan;
|
|
1105
1205
|
// Parameterized chains (.prepare().bind(), .exec() spread, D1 convenience,
|
|
1106
1206
|
// DB wrappers) and explicit parameterization are always safe.
|
|
1107
|
-
if (isParameterizedByChain(node, adapter, sourceCode, config)) {
|
|
1108
|
-
return { parameterized: true, injectionRisk: false };
|
|
1207
|
+
if (isParameterizedByChain(node, adapter, sourceCode, config, effectiveWrapperNames(scan))) {
|
|
1208
|
+
return { parameterized: true, injectionRisk: false, escaped: false };
|
|
1109
1209
|
}
|
|
1110
1210
|
if ((config.securityPatterns?.parameterizedQueries || []).some(p => text.includes(p))) {
|
|
1111
|
-
return { parameterized: true, injectionRisk: false };
|
|
1211
|
+
return { parameterized: true, injectionRisk: false, escaped: false };
|
|
1112
1212
|
}
|
|
1113
1213
|
// No dynamic-string capability → can't prove unsafe; err quiet.
|
|
1114
1214
|
if (!adapter.isDynamicStringConstruction || !adapter.getDynamicParts) {
|
|
1115
|
-
return { parameterized: false, injectionRisk: false };
|
|
1215
|
+
return { parameterized: false, injectionRisk: false, escaped: false };
|
|
1116
1216
|
}
|
|
1117
1217
|
// Only a dynamically-constructed string carrying SQL keywords can inject.
|
|
1118
1218
|
if (!adapter.isDynamicStringConstruction(node) || !containsSQLKeywords(text)) {
|
|
1119
|
-
return { parameterized: false, injectionRisk: false };
|
|
1219
|
+
return { parameterized: false, injectionRisk: false, escaped: false };
|
|
1120
1220
|
}
|
|
1121
1221
|
const unresolved = adapter.getDynamicParts(node, sourceCode)
|
|
1122
|
-
.filter(part => !isSafeDynamicPart(part, ast, scan))
|
|
1123
|
-
.map(part => part.text);
|
|
1222
|
+
.filter(part => !isSafeDynamicPart(part, ast, scan));
|
|
1124
1223
|
if (unresolved.length === 0) {
|
|
1125
|
-
return { parameterized: false, injectionRisk: false };
|
|
1224
|
+
return { parameterized: false, injectionRisk: false, escaped: false };
|
|
1126
1225
|
}
|
|
1226
|
+
// Manual quote-escaping (`.replace(/'/g, "''")`) is defended, not raw — every
|
|
1227
|
+
// unresolved part being quote-escaped downgrades the finding to advisory.
|
|
1228
|
+
const escaped = unresolved.every(part => !!part.node && !!adapter.isEscapedInterpolation && adapter.isEscapedInterpolation(part.node, ast, sourceCode));
|
|
1127
1229
|
return {
|
|
1128
1230
|
parameterized: false,
|
|
1129
1231
|
injectionRisk: true,
|
|
1130
|
-
|
|
1232
|
+
escaped,
|
|
1233
|
+
message: `Cannot protect interpolated content: ${unresolved.map(part => '${' + part.text + '}').join(', ')}`,
|
|
1131
1234
|
};
|
|
1132
1235
|
}
|
|
1133
1236
|
function extractCallExpressionMethod(callExpr, adapter, sourceCode) {
|
|
@@ -1137,11 +1240,10 @@ function extractCallExpressionMethod(callExpr, adapter, sourceCode) {
|
|
|
1137
1240
|
// keywords (COUNT, JOIN, WHERE, ...) that appear inside template literals
|
|
1138
1241
|
// in the call arguments, which a regex scan of the full call-expression
|
|
1139
1242
|
// text would incorrectly match.
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
});
|
|
1243
|
+
// Use the shared callee extractor: it recurses through `await_expression` and
|
|
1244
|
+
// skips `type_arguments`, so `await d1<{...}>(...)` resolves to `d1` rather
|
|
1245
|
+
// than falling through to the "unknown" regex fallback.
|
|
1246
|
+
const callee = getCallExpressionCallee(callExpr, adapter);
|
|
1145
1247
|
if (callee) {
|
|
1146
1248
|
const calleeType = adapter.getNodeType(callee);
|
|
1147
1249
|
if (calleeType === 'member_expression') {
|
|
@@ -1244,6 +1346,31 @@ function isConnectionString(text) {
|
|
|
1244
1346
|
return patterns.some(pattern => pattern.test(text));
|
|
1245
1347
|
}
|
|
1246
1348
|
// ── R4.1: Loop-query detection ──────────────────────────────────────
|
|
1349
|
+
/**
|
|
1350
|
+
* LLM provider / SDK namespaces. A callee whose text carries one of these is
|
|
1351
|
+
* an unambiguous LLM/agent invocation (e.g. `gatewayDeepseekModel`,
|
|
1352
|
+
* `anthropic.messages.create`, `openai.chat`).
|
|
1353
|
+
*/
|
|
1354
|
+
const LLM_PROVIDER_RE = /(?:anthropic|openai|deepseek|claude|gpt|gemini|cohere|mistral|bedrock|vertex|ollama|llm|gateway)/i;
|
|
1355
|
+
/**
|
|
1356
|
+
* LLM action verbs — AI SDK methods and the wrapper names that survive the
|
|
1357
|
+
* provider-token check (e.g. `aiEmbed`, `chatCompletion`, `thesisEngine`).
|
|
1358
|
+
* Deliberately narrower than `generate`/`extract` (those collide with ordinary
|
|
1359
|
+
* helpers like `insertSingleGeneratedBuild`); the provider namespace and the
|
|
1360
|
+
* model-client argument signal cover the rest.
|
|
1361
|
+
*/
|
|
1362
|
+
const LLM_ACTION_RE = /(?:embed|reembed|completion|chat|prompt|synthesize|thesis|agent|classify|summarize|translate)/i;
|
|
1363
|
+
/**
|
|
1364
|
+
* Identifier names that denote an LLM/model client when they appear as a call
|
|
1365
|
+
* argument (e.g. `extractBuildsFromCorpus(db, model, …)`) or as the object of a
|
|
1366
|
+
* member callee (e.g. `model.chat(…)`). Deliberately excludes `client`/`ai`
|
|
1367
|
+
* (ambiguous — a DB client is also a `client`) so we never suppress a real N+1
|
|
1368
|
+
* on a generic variable name.
|
|
1369
|
+
*/
|
|
1370
|
+
const LLM_CLIENT_ARG_NAMES = new Set([
|
|
1371
|
+
'model', 'llm', 'embedder', 'gateway',
|
|
1372
|
+
'anthropic', 'openai', 'deepseek', 'claude', 'gpt', 'gemini', 'cohere', 'mistral', 'bedrock',
|
|
1373
|
+
]);
|
|
1247
1374
|
/**
|
|
1248
1375
|
* R4.1: Find database queries inside loops and flag them as N+1 risks.
|
|
1249
1376
|
* Each finding carries the query call location (never line 1).
|
|
@@ -1258,6 +1385,14 @@ function checkLoopQueries(ast, scan) {
|
|
|
1258
1385
|
const reported = new Set();
|
|
1259
1386
|
const loopOrdinals = new Map();
|
|
1260
1387
|
for (const node of dbNodes) {
|
|
1388
|
+
// A DB call and its template-literal SQL argument both satisfy isDbCallNode —
|
|
1389
|
+
// the call via provenance, the literal via the template-literal branch — so one
|
|
1390
|
+
// query site (`db.prepare(`…`)`) would otherwise emit two findings, one at the
|
|
1391
|
+
// call line and one at the SQL line. Collapse to the call node: it is always
|
|
1392
|
+
// present when its literal is (isDbCallNode only admits a literal whose
|
|
1393
|
+
// enclosing call is itself provenanced), so skipping the literal loses nothing.
|
|
1394
|
+
if (isTemplateLiteral(node, adapter))
|
|
1395
|
+
continue;
|
|
1261
1396
|
const nodeText = adapter.getNodeText(node, sourceCode);
|
|
1262
1397
|
if (!nodeText || nodeText.trim().length < 10)
|
|
1263
1398
|
continue;
|
|
@@ -1265,16 +1400,33 @@ function checkLoopQueries(ast, scan) {
|
|
|
1265
1400
|
// an eager call or a prepare chained into one still fires.
|
|
1266
1401
|
if (isStatementConstructionOnly(node, adapter, sourceCode))
|
|
1267
1402
|
continue;
|
|
1403
|
+
// Spec 52 R1 (extension) — skip SQL-string construction inside a query
|
|
1404
|
+
// compiler: an ORM builder method (e.g. `aggregate`) on an unprovenanced
|
|
1405
|
+
// `this` whose result is accumulated via `.push(...)` returns a string
|
|
1406
|
+
// fragment, not a query result (knex `sql.push(...this.aggregate(stmt))`).
|
|
1407
|
+
if (isSqlStringConstruction(node, adapter, sourceCode))
|
|
1408
|
+
continue;
|
|
1268
1409
|
const loopInfo = findEnclosingLoop(node, adapter);
|
|
1269
1410
|
if (!loopInfo)
|
|
1270
1411
|
continue;
|
|
1271
|
-
// R4.1
|
|
1272
|
-
|
|
1273
|
-
|
|
1412
|
+
// R4.1 (Spec 46): LLM-pipeline discriminator. A loop whose body invokes an
|
|
1413
|
+
// LLM/agent (embedding, model completion, corpus extraction) is an intentional
|
|
1414
|
+
// *sequential pipeline* — its per-item DB calls are persistence steps gated by
|
|
1415
|
+
// rate limits and per-item crash-recovery, not a batchable N+1. Batching or
|
|
1416
|
+
// joining would regress those semantics, so suppress the finding rather than
|
|
1417
|
+
// emit an unfixable `severe`.
|
|
1418
|
+
if (loopBodyContainsLlmCall(loopInfo.loopNode, adapter, sourceCode))
|
|
1419
|
+
continue;
|
|
1420
|
+
// R4.1: one finding per *loop*, not per query — an N+1 is a property of the
|
|
1421
|
+
// loop, so a loop issuing several queries is still one violation (defect #51).
|
|
1422
|
+
// Key on the loop node's start byte offset so distinct loops (including
|
|
1423
|
+
// nested ones) never collapse, while every query in the same loop dedups to
|
|
1424
|
+
// the first. The finding anchors to that first query's location below.
|
|
1425
|
+
const dedupKey = String(loopInfo.loopNode.range[0]);
|
|
1274
1426
|
if (reported.has(dedupKey))
|
|
1275
1427
|
continue;
|
|
1276
1428
|
reported.add(dedupKey);
|
|
1277
|
-
const sym = nextSymbol(
|
|
1429
|
+
const sym = nextSymbol(enclosingIdentity(node, adapter, ast.filePath), 'loop-query', loopOrdinals);
|
|
1278
1430
|
// R4.2: Nested-loop attribution.
|
|
1279
1431
|
const depthMsg = loopInfo.depth > 1 ? ` (nested ${loopInfo.depth} levels deep)` : '';
|
|
1280
1432
|
violations.push(makeViolation(ast.filePath, node.location.start, `Database query inside loop${depthMsg} ` +
|
|
@@ -1283,6 +1435,71 @@ function checkLoopQueries(ast, scan) {
|
|
|
1283
1435
|
}
|
|
1284
1436
|
return violations;
|
|
1285
1437
|
}
|
|
1438
|
+
/**
|
|
1439
|
+
* R4.1 (Spec 46): True when the loop's subtree contains an LLM/agent invocation.
|
|
1440
|
+
* Used as the discriminator between a batchable N+1 (no LLM call — the queries
|
|
1441
|
+
* should be batched/joined) and an intentional sequential pipeline (an LLM call
|
|
1442
|
+
* — the per-item DB calls are persistence steps behind rate limits and
|
|
1443
|
+
* crash-recovery). Walking the whole loop subtree (header + body) is safe: a
|
|
1444
|
+
* loop header rarely hosts an LLM call, and a body-level one is exactly the
|
|
1445
|
+
* signal we want.
|
|
1446
|
+
*/
|
|
1447
|
+
function loopBodyContainsLlmCall(loopNode, adapter, sourceCode) {
|
|
1448
|
+
let found = false;
|
|
1449
|
+
walkSubtree(loopNode, adapter, (node) => {
|
|
1450
|
+
if (found || adapter.getNodeType(node) !== 'call_expression')
|
|
1451
|
+
return;
|
|
1452
|
+
if (isLlmCallNode(node, adapter, sourceCode))
|
|
1453
|
+
found = true;
|
|
1454
|
+
});
|
|
1455
|
+
return found;
|
|
1456
|
+
}
|
|
1457
|
+
/** Depth-first walk over an ASTNode subtree (children only, no parent links). */
|
|
1458
|
+
function walkSubtree(node, adapter, visitor) {
|
|
1459
|
+
visitor(node);
|
|
1460
|
+
for (const child of adapter.getChildren(node)) {
|
|
1461
|
+
walkSubtree(child, adapter, visitor);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Classify a call_expression as an LLM/agent invocation via three unambiguous
|
|
1466
|
+
* signals: a provider/SDK namespace in the callee text, an LLM action verb in the
|
|
1467
|
+
* callee text, or a model-client identifier passed as an argument / used as the
|
|
1468
|
+
* member-callee object.
|
|
1469
|
+
*/
|
|
1470
|
+
function isLlmCallNode(node, adapter, sourceCode) {
|
|
1471
|
+
const children = adapter.getChildren(node);
|
|
1472
|
+
const callee = children.find(c => adapter.getNodeType(c) !== 'arguments');
|
|
1473
|
+
if (callee) {
|
|
1474
|
+
const calleeText = adapter.getNodeText(callee, sourceCode);
|
|
1475
|
+
if (LLM_PROVIDER_RE.test(calleeText) || LLM_ACTION_RE.test(calleeText))
|
|
1476
|
+
return true;
|
|
1477
|
+
// Member callee whose object is an LLM client: `model.chat(…)`, `llm.invoke(…)`.
|
|
1478
|
+
if (adapter.getNodeType(callee) === 'member_expression') {
|
|
1479
|
+
const obj = adapter.getChildren(callee).find(c => adapter.getNodeType(c) === 'identifier');
|
|
1480
|
+
if (obj && LLM_CLIENT_ARG_NAMES.has(adapter.getNodeText(obj, sourceCode)))
|
|
1481
|
+
return true;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
// Argument signal: an LLM client identifier passed into the call, e.g.
|
|
1485
|
+
// `extractBuildsFromCorpus(db, model, …)`.
|
|
1486
|
+
const args = children.find(c => adapter.getNodeType(c) === 'arguments');
|
|
1487
|
+
if (args) {
|
|
1488
|
+
for (const arg of adapter.getChildren(args)) {
|
|
1489
|
+
if (adapter.getNodeType(arg) === 'identifier' &&
|
|
1490
|
+
LLM_CLIENT_ARG_NAMES.has(adapter.getNodeText(arg, sourceCode))) {
|
|
1491
|
+
return true;
|
|
1492
|
+
}
|
|
1493
|
+
// Member access argument (`client.messages`) — check its object identifier.
|
|
1494
|
+
if (adapter.getNodeType(arg) === 'member_expression') {
|
|
1495
|
+
const obj = adapter.getChildren(arg).find(c => adapter.getNodeType(c) === 'identifier');
|
|
1496
|
+
if (obj && LLM_CLIENT_ARG_NAMES.has(adapter.getNodeText(obj, sourceCode)))
|
|
1497
|
+
return true;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return false;
|
|
1502
|
+
}
|
|
1286
1503
|
/** True when `node` is a function call whose callee is DB-provenanced. */
|
|
1287
1504
|
function isDBProvenancedFunctionCall(node, adapter, sourceCode, provenanceContext) {
|
|
1288
1505
|
return isFunctionCall(node, adapter) &&
|
|
@@ -1410,63 +1627,11 @@ function getPropertyName(node, adapter) {
|
|
|
1410
1627
|
return '';
|
|
1411
1628
|
}
|
|
1412
1629
|
/**
|
|
1413
|
-
*
|
|
1414
|
-
*
|
|
1415
|
-
*
|
|
1416
|
-
* Used for stable fingerprint symbols — the enclosing function name is
|
|
1417
|
-
* immune to line drift (Spec 18 Gap 2).
|
|
1418
|
-
*/
|
|
1419
|
-
function findEnclosingFunctionName(node, adapter) {
|
|
1420
|
-
let current = adapter.getParent(node);
|
|
1421
|
-
while (current) {
|
|
1422
|
-
const type = adapter.getNodeType(current);
|
|
1423
|
-
// Arrow functions, function declarations, function expressions
|
|
1424
|
-
if (type === 'arrow_function' ||
|
|
1425
|
-
type === 'function_declaration' ||
|
|
1426
|
-
type === 'function_expression' ||
|
|
1427
|
-
type === 'generator_function_declaration' ||
|
|
1428
|
-
type === 'generator_function_expression') {
|
|
1429
|
-
const name = getNodeName(current, adapter);
|
|
1430
|
-
if (name)
|
|
1431
|
-
return name;
|
|
1432
|
-
}
|
|
1433
|
-
// Method definitions on classes/objects
|
|
1434
|
-
if (type === 'method_definition' || adapter.isMethod(current)) {
|
|
1435
|
-
const name = getNodeName(current, adapter);
|
|
1436
|
-
if (name)
|
|
1437
|
-
return name;
|
|
1438
|
-
}
|
|
1439
|
-
current = adapter.getParent(current);
|
|
1440
|
-
}
|
|
1441
|
-
return 'top-level';
|
|
1442
|
-
}
|
|
1443
|
-
/**
|
|
1444
|
-
* Extract the name/identifier from a function or method AST node.
|
|
1630
|
+
* The enclosing-function identity used for stable fingerprint symbols. The
|
|
1631
|
+
* coordinate (start line + column) keeps anonymous handlers distinct; the single
|
|
1632
|
+
* definition lives in codeAnalysis.ts (Spec 61 Amendment A).
|
|
1445
1633
|
*/
|
|
1446
|
-
|
|
1447
|
-
// Try named children first
|
|
1448
|
-
if (node.name && typeof node.name === 'string')
|
|
1449
|
-
return node.name;
|
|
1450
|
-
if (node.text && typeof node.text === 'string')
|
|
1451
|
-
return node.text;
|
|
1452
|
-
// Fall back to the raw tree-sitter node's text content (leaf identifiers etc.)
|
|
1453
|
-
const rawText = node.raw?.text;
|
|
1454
|
-
if (typeof rawText === 'string' && rawText.length > 0)
|
|
1455
|
-
return rawText;
|
|
1456
|
-
// Walk children for identifier / property_identifier
|
|
1457
|
-
if (node.children) {
|
|
1458
|
-
for (const child of node.children) {
|
|
1459
|
-
const childType = adapter.getNodeType(child);
|
|
1460
|
-
if (childType === 'identifier' ||
|
|
1461
|
-
childType === 'property_identifier') {
|
|
1462
|
-
const name = getNodeName(child, adapter);
|
|
1463
|
-
if (name)
|
|
1464
|
-
return name;
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
return '';
|
|
1469
|
-
}
|
|
1634
|
+
const enclosingIdentity = (node, adapter, filePath) => functionIdentityLabel(findEnclosingFunctionIdentity(node, adapter, filePath));
|
|
1470
1635
|
/**
|
|
1471
1636
|
* Universal data access analyzer.
|
|
1472
1637
|
*/
|