code-auditor-mcp 4.1.1 → 5.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 +338 -0
- package/README.md +4 -4
- package/configs/hhra-compat.json +0 -8
- package/dist/__tests__/fixtures/spec-52/item-03-exec-in-loop.d.ts +1 -1
- package/dist/__tests__/fixtures/spec-52/item-03-exec-in-loop.d.ts.map +1 -1
- package/dist/__tests__/fixtures/spec-52/item-03-exec-in-loop.js +7 -1
- package/dist/__tests__/fixtures/spec-52/item-03-exec-in-loop.js.map +1 -1
- package/dist/__tests__/fixtures/spec-52/item-04-storage-sql-exec-in-loop.d.ts +2 -8
- 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-04-storage-sql-exec-in-loop.js +4 -4
- package/dist/__tests__/fixtures/spec-52/item-04-storage-sql-exec-in-loop.js.map +1 -1
- package/dist/__tests__/fixtures/spec-52/item-06-chained-prepare-run-in-loop.d.ts +1 -1
- package/dist/__tests__/fixtures/spec-52/item-06-chained-prepare-run-in-loop.d.ts.map +1 -1
- package/dist/__tests__/fixtures/spec-52/item-06-chained-prepare-run-in-loop.js.map +1 -1
- package/dist/__tests__/fixtures/spec-52/item-07-chained-prepare-bind-first-in-loop.d.ts +2 -2
- package/dist/__tests__/fixtures/spec-52/item-07-chained-prepare-bind-first-in-loop.d.ts.map +1 -1
- package/dist/__tests__/fixtures/spec-52/item-07-chained-prepare-bind-first-in-loop.js.map +1 -1
- package/dist/__tests__/fixtures/spec-52/item-10-promise-all-map-of-eager-all-in-loop.d.ts +1 -1
- package/dist/__tests__/fixtures/spec-52/item-10-promise-all-map-of-eager-all-in-loop.d.ts.map +1 -1
- package/dist/__tests__/fixtures/spec-52/item-10-promise-all-map-of-eager-all-in-loop.js.map +1 -1
- package/dist/analyzers/applicability.d.ts +42 -0
- package/dist/analyzers/applicability.d.ts.map +1 -1
- package/dist/analyzers/applicability.js +74 -0
- package/dist/analyzers/applicability.js.map +1 -1
- package/dist/analyzers/cross-language/APIContractAnalyzer.js +18 -24
- package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +30 -23
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js +113 -76
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.d.ts +2 -0
- package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.js +8 -3
- package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +210 -148
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
- package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/documentationAnalyzer.js +23 -28
- package/dist/analyzers/documentationAnalyzer.js.map +1 -1
- package/dist/analyzers/handleIdentification.d.ts +356 -0
- package/dist/analyzers/handleIdentification.d.ts.map +1 -0
- package/dist/analyzers/handleIdentification.js +424 -0
- package/dist/analyzers/handleIdentification.js.map +1 -0
- package/dist/analyzers/manifestStaleness.d.ts +64 -0
- package/dist/analyzers/manifestStaleness.d.ts.map +1 -0
- package/dist/analyzers/manifestStaleness.js +115 -0
- package/dist/analyzers/manifestStaleness.js.map +1 -0
- package/dist/analyzers/orgFilterTiers.d.ts +73 -1
- package/dist/analyzers/orgFilterTiers.d.ts.map +1 -1
- package/dist/analyzers/orgFilterTiers.js +166 -1
- package/dist/analyzers/orgFilterTiers.js.map +1 -1
- package/dist/analyzers/provenance.d.ts +251 -57
- package/dist/analyzers/provenance.d.ts.map +1 -1
- package/dist/analyzers/provenance.js +1109 -429
- package/dist/analyzers/provenance.js.map +1 -1
- package/dist/analyzers/reactAnalyzer.d.ts +2 -0
- package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/reactAnalyzer.js +2 -0
- package/dist/analyzers/reactAnalyzer.js.map +1 -1
- package/dist/analyzers/receiverResolution.d.ts +183 -0
- package/dist/analyzers/receiverResolution.d.ts.map +1 -0
- package/dist/analyzers/receiverResolution.js +540 -0
- package/dist/analyzers/receiverResolution.js.map +1 -0
- package/dist/analyzers/receiverRoot.d.ts +328 -0
- package/dist/analyzers/receiverRoot.d.ts.map +1 -0
- package/dist/analyzers/receiverRoot.js +990 -0
- package/dist/analyzers/receiverRoot.js.map +1 -0
- package/dist/analyzers/ruleRegistry.d.ts +12 -68
- package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
- package/dist/analyzers/ruleRegistry.js +32 -342
- package/dist/analyzers/ruleRegistry.js.map +1 -1
- package/dist/analyzers/ruleTiming.d.ts +10 -0
- package/dist/analyzers/ruleTiming.d.ts.map +1 -1
- package/dist/analyzers/ruleTiming.js +20 -0
- package/dist/analyzers/ruleTiming.js.map +1 -1
- package/dist/analyzers/sqlLiteral.d.ts +76 -0
- package/dist/analyzers/sqlLiteral.d.ts.map +1 -0
- package/dist/analyzers/sqlLiteral.js +419 -0
- package/dist/analyzers/sqlLiteral.js.map +1 -0
- package/dist/analyzers/tsEcosystem.d.ts +115 -0
- package/dist/analyzers/tsEcosystem.d.ts.map +1 -0
- package/dist/analyzers/tsEcosystem.js +231 -0
- package/dist/analyzers/tsEcosystem.js.map +1 -0
- package/dist/analyzers/tsExpressionDescriptor.d.ts +156 -0
- package/dist/analyzers/tsExpressionDescriptor.d.ts.map +1 -0
- package/dist/analyzers/tsExpressionDescriptor.js +374 -0
- package/dist/analyzers/tsExpressionDescriptor.js.map +1 -0
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts +6 -0
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +107 -44
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js +23 -35
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +287 -49
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +2130 -512
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +2 -18
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +0 -33
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +20 -143
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +4 -3
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +36 -13
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSecretsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSecretsAnalyzer.js +6 -0
- package/dist/analyzers/universal/UniversalSecretsAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSecurityAnalyzer.d.ts +15 -0
- package/dist/analyzers/universal/UniversalSecurityAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSecurityAnalyzer.js +25 -0
- package/dist/analyzers/universal/UniversalSecurityAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +21 -32
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js +166 -196
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/functionConcerns.d.ts +3 -0
- package/dist/analyzers/universal/functionConcerns.d.ts.map +1 -1
- package/dist/analyzers/universal/functionConcerns.js +4 -1
- package/dist/analyzers/universal/functionConcerns.js.map +1 -1
- package/dist/analyzers/universal/schema/codeAnalysis.d.ts +310 -69
- package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -1
- package/dist/analyzers/universal/schema/codeAnalysis.js +709 -383
- package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -1
- package/dist/analyzers/universal/schema/config.d.ts +3 -14
- package/dist/analyzers/universal/schema/config.d.ts.map +1 -1
- package/dist/analyzers/universal/schema/config.js +3 -16
- package/dist/analyzers/universal/schema/config.js.map +1 -1
- package/dist/analyzers/universal/schema/discovery.d.ts +22 -1
- package/dist/analyzers/universal/schema/discovery.d.ts.map +1 -1
- package/dist/analyzers/universal/schema/discovery.js +36 -20
- package/dist/analyzers/universal/schema/discovery.js.map +1 -1
- package/dist/analyzers/universal/schema/jsonSchema.d.ts.map +1 -1
- package/dist/analyzers/universal/schema/jsonSchema.js +13 -3
- package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -1
- package/dist/analyzers/universal/schema/migrations.d.ts +187 -27
- package/dist/analyzers/universal/schema/migrations.d.ts.map +1 -1
- package/dist/analyzers/universal/schema/migrations.js +443 -186
- package/dist/analyzers/universal/schema/migrations.js.map +1 -1
- package/dist/analyzers/universal/schema/types.d.ts +17 -3
- package/dist/analyzers/universal/schema/types.d.ts.map +1 -1
- package/dist/analyzers/universal/schema/violations.js +1 -1
- package/dist/analyzers/universal/schema/violations.js.map +1 -1
- package/dist/analyzers/universal/styleScale.d.ts +52 -0
- package/dist/analyzers/universal/styleScale.d.ts.map +1 -0
- package/dist/analyzers/universal/styleScale.js +95 -0
- package/dist/analyzers/universal/styleScale.js.map +1 -0
- package/dist/applyDataDirEnv.js +1 -1
- package/dist/applyDataDirEnv.js.map +1 -1
- package/dist/auditRouter.d.ts +26 -30
- package/dist/auditRouter.d.ts.map +1 -1
- package/dist/auditRouter.js +373 -143
- package/dist/auditRouter.js.map +1 -1
- package/dist/auditRunner.d.ts +64 -1
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +1538 -1033
- package/dist/auditRunner.js.map +1 -1
- package/dist/baseline.d.ts +30 -3
- package/dist/baseline.d.ts.map +1 -1
- package/dist/baseline.js +70 -9
- package/dist/baseline.js.map +1 -1
- package/dist/cli.js +969 -821
- package/dist/cli.js.map +1 -1
- package/dist/codeIndex/analyzerConfig.d.ts +60 -0
- package/dist/codeIndex/analyzerConfig.d.ts.map +1 -0
- package/dist/codeIndex/analyzerConfig.js +98 -0
- package/dist/codeIndex/analyzerConfig.js.map +1 -0
- package/dist/codeIndex/auditResults.d.ts +39 -0
- package/dist/codeIndex/auditResults.d.ts.map +1 -0
- package/dist/codeIndex/auditResults.js +99 -0
- package/dist/codeIndex/auditResults.js.map +1 -0
- package/dist/codeIndex/codeMap.d.ts +59 -0
- package/dist/codeIndex/codeMap.d.ts.map +1 -0
- package/dist/codeIndex/codeMap.js +72 -0
- package/dist/codeIndex/codeMap.js.map +1 -0
- package/dist/codeIndex/conventions.d.ts +33 -0
- package/dist/codeIndex/conventions.d.ts.map +1 -0
- package/dist/codeIndex/conventions.js +84 -0
- package/dist/codeIndex/conventions.js.map +1 -0
- package/dist/codeIndex/coverage.d.ts +78 -0
- package/dist/codeIndex/coverage.d.ts.map +1 -0
- package/dist/codeIndex/coverage.js +164 -0
- package/dist/codeIndex/coverage.js.map +1 -0
- package/dist/codeIndex/dependencyGraph.d.ts +97 -0
- package/dist/codeIndex/dependencyGraph.d.ts.map +1 -0
- package/dist/codeIndex/dependencyGraph.js +241 -0
- package/dist/codeIndex/dependencyGraph.js.map +1 -0
- package/dist/codeIndex/functionIndex.d.ts +111 -0
- package/dist/codeIndex/functionIndex.d.ts.map +1 -0
- package/dist/codeIndex/functionIndex.js +306 -0
- package/dist/codeIndex/functionIndex.js.map +1 -0
- package/dist/codeIndex/inferredReceivers.d.ts +32 -0
- package/dist/codeIndex/inferredReceivers.d.ts.map +1 -0
- package/dist/codeIndex/inferredReceivers.js +41 -0
- package/dist/codeIndex/inferredReceivers.js.map +1 -0
- package/dist/codeIndex/meta.d.ts +30 -0
- package/dist/codeIndex/meta.d.ts.map +1 -0
- package/dist/codeIndex/meta.js +37 -0
- package/dist/codeIndex/meta.js.map +1 -0
- package/dist/codeIndex/migrations.d.ts +96 -0
- package/dist/codeIndex/migrations.d.ts.map +1 -0
- package/dist/codeIndex/migrations.js +1302 -0
- package/dist/codeIndex/migrations.js.map +1 -0
- package/dist/codeIndex/projectTasks.d.ts +54 -0
- package/dist/codeIndex/projectTasks.d.ts.map +1 -0
- package/dist/codeIndex/projectTasks.js +81 -0
- package/dist/codeIndex/projectTasks.js.map +1 -0
- package/dist/codeIndex/provenance.d.ts +47 -0
- package/dist/codeIndex/provenance.d.ts.map +1 -0
- package/dist/codeIndex/provenance.js +48 -0
- package/dist/codeIndex/provenance.js.map +1 -0
- package/dist/codeIndex/rawSql.d.ts +54 -0
- package/dist/codeIndex/rawSql.d.ts.map +1 -0
- package/dist/codeIndex/rawSql.js +71 -0
- package/dist/codeIndex/rawSql.js.map +1 -0
- package/dist/codeIndex/schema.d.ts +99 -0
- package/dist/codeIndex/schema.d.ts.map +1 -0
- package/dist/codeIndex/schema.js +174 -0
- package/dist/codeIndex/schema.js.map +1 -0
- package/dist/codeIndex/search.d.ts +40 -0
- package/dist/codeIndex/search.d.ts.map +1 -0
- package/dist/codeIndex/search.js +316 -0
- package/dist/codeIndex/search.js.map +1 -0
- package/dist/codeIndex/shared.d.ts +27 -0
- package/dist/codeIndex/shared.d.ts.map +1 -0
- package/dist/codeIndex/shared.js +46 -0
- package/dist/codeIndex/shared.js.map +1 -0
- package/dist/codeIndex/sqliteCollection.d.ts +81 -0
- package/dist/codeIndex/sqliteCollection.d.ts.map +1 -0
- package/dist/codeIndex/sqliteCollection.js +250 -0
- package/dist/codeIndex/sqliteCollection.js.map +1 -0
- package/dist/codeIndex/whitelist.d.ts +59 -0
- package/dist/codeIndex/whitelist.d.ts.map +1 -0
- package/dist/codeIndex/whitelist.js +188 -0
- package/dist/codeIndex/whitelist.js.map +1 -0
- package/dist/codeIndexDB.d.ts +132 -271
- package/dist/codeIndexDB.d.ts.map +1 -1
- package/dist/codeIndexDB.js +365 -2729
- package/dist/codeIndexDB.js.map +1 -1
- package/dist/codeIndexService.d.ts +87 -12
- package/dist/codeIndexService.d.ts.map +1 -1
- package/dist/codeIndexService.js +110 -34
- package/dist/codeIndexService.js.map +1 -1
- package/dist/componentScanner.d.ts +35 -0
- package/dist/componentScanner.d.ts.map +1 -1
- package/dist/componentScanner.js +114 -80
- package/dist/componentScanner.js.map +1 -1
- package/dist/config/configGeneratePath.d.ts +4 -0
- package/dist/config/configGeneratePath.d.ts.map +1 -1
- package/dist/config/configGeneratePath.js +4 -0
- package/dist/config/configGeneratePath.js.map +1 -1
- package/dist/config/configLoader.d.ts +15 -0
- package/dist/config/configLoader.d.ts.map +1 -1
- package/dist/config/configLoader.js +15 -16
- package/dist/config/configLoader.js.map +1 -1
- package/dist/config/defaults.d.ts +11 -7
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +16 -14
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/effectiveConfig.d.ts +17 -2
- package/dist/config/effectiveConfig.d.ts.map +1 -1
- package/dist/config/effectiveConfig.js +32 -25
- package/dist/config/effectiveConfig.js.map +1 -1
- package/dist/config/lintConfigReader.d.ts +6 -0
- package/dist/config/lintConfigReader.d.ts.map +1 -1
- package/dist/config/lintConfigReader.js +6 -0
- package/dist/config/lintConfigReader.js.map +1 -1
- package/dist/config/staticObjectExtract.d.ts +4 -0
- package/dist/config/staticObjectExtract.d.ts.map +1 -1
- package/dist/config/staticObjectExtract.js +7 -3
- package/dist/config/staticObjectExtract.js.map +1 -1
- package/dist/config/thresholdRationales.d.ts +2 -0
- package/dist/config/thresholdRationales.d.ts.map +1 -1
- package/dist/config/thresholdRationales.js +5 -2
- package/dist/config/thresholdRationales.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/conventions/conventionMiner.d.ts +105 -2
- package/dist/conventions/conventionMiner.d.ts.map +1 -1
- package/dist/conventions/conventionMiner.js +334 -259
- package/dist/conventions/conventionMiner.js.map +1 -1
- package/dist/coverage/coverageService.js +4 -4
- package/dist/coverage/coverageService.js.map +1 -1
- package/dist/daemon/args.d.ts +3 -0
- package/dist/daemon/args.d.ts.map +1 -1
- package/dist/daemon/args.js +3 -0
- package/dist/daemon/args.js.map +1 -1
- package/dist/daemon/core.d.ts +33 -27
- package/dist/daemon/core.d.ts.map +1 -1
- package/dist/daemon/core.js +198 -162
- package/dist/daemon/core.js.map +1 -1
- package/dist/daemon/lspServer.d.ts +8 -0
- package/dist/daemon/lspServer.d.ts.map +1 -1
- package/dist/daemon/lspServer.js +8 -0
- package/dist/daemon/lspServer.js.map +1 -1
- package/dist/daemon/main.d.ts +2 -0
- package/dist/daemon/main.d.ts.map +1 -1
- package/dist/daemon/main.js +2 -0
- package/dist/daemon/main.js.map +1 -1
- package/dist/daemon/resolve.d.ts +18 -3
- package/dist/daemon/resolve.d.ts.map +1 -1
- package/dist/daemon/resolve.js +25 -4
- package/dist/daemon/resolve.js.map +1 -1
- package/dist/daemon/socketClient.d.ts +10 -1
- package/dist/daemon/socketClient.d.ts.map +1 -1
- package/dist/daemon/socketClient.js +10 -1
- package/dist/daemon/socketClient.js.map +1 -1
- package/dist/daemon/socketServer.d.ts +13 -0
- package/dist/daemon/socketServer.d.ts.map +1 -1
- package/dist/daemon/socketServer.js +13 -0
- package/dist/daemon/socketServer.js.map +1 -1
- package/dist/dataPaths.d.ts +17 -8
- package/dist/dataPaths.d.ts.map +1 -1
- package/dist/dataPaths.js +16 -44
- package/dist/dataPaths.js.map +1 -1
- package/dist/dismissals.d.ts +22 -1
- package/dist/dismissals.d.ts.map +1 -1
- package/dist/dismissals.js +22 -1
- package/dist/dismissals.js.map +1 -1
- package/dist/enforcement/gate.d.ts +1 -0
- package/dist/enforcement/gate.d.ts.map +1 -1
- package/dist/enforcement/gate.js +1 -0
- package/dist/enforcement/gate.js.map +1 -1
- package/dist/fingerprint.d.ts +6 -0
- package/dist/fingerprint.d.ts.map +1 -1
- package/dist/fingerprint.js +46 -1
- package/dist/fingerprint.js.map +1 -1
- package/dist/functionScanner.d.ts +30 -1
- package/dist/functionScanner.d.ts.map +1 -1
- package/dist/functionScanner.js +258 -256
- package/dist/functionScanner.js.map +1 -1
- package/dist/generators/AWSQConfigGenerator.d.ts +23 -0
- package/dist/generators/AWSQConfigGenerator.d.ts.map +1 -1
- package/dist/generators/AWSQConfigGenerator.js +23 -0
- package/dist/generators/AWSQConfigGenerator.js.map +1 -1
- package/dist/generators/BaseConfigGenerator.d.ts +7 -0
- package/dist/generators/BaseConfigGenerator.d.ts.map +1 -1
- package/dist/generators/BaseConfigGenerator.js +7 -0
- package/dist/generators/BaseConfigGenerator.js.map +1 -1
- package/dist/generators/ClaudeConfigGenerator.d.ts +23 -0
- package/dist/generators/ClaudeConfigGenerator.d.ts.map +1 -1
- package/dist/generators/ClaudeConfigGenerator.js +23 -0
- package/dist/generators/ClaudeConfigGenerator.js.map +1 -1
- package/dist/generators/ClineConfigGenerator.d.ts +23 -0
- package/dist/generators/ClineConfigGenerator.d.ts.map +1 -1
- package/dist/generators/ClineConfigGenerator.js +23 -0
- package/dist/generators/ClineConfigGenerator.js.map +1 -1
- package/dist/generators/CodeiumConfigGenerator.d.ts +23 -0
- package/dist/generators/CodeiumConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CodeiumConfigGenerator.js +23 -0
- package/dist/generators/CodeiumConfigGenerator.js.map +1 -1
- package/dist/generators/CodexConfigGenerator.d.ts +23 -0
- package/dist/generators/CodexConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CodexConfigGenerator.js +23 -0
- package/dist/generators/CodexConfigGenerator.js.map +1 -1
- package/dist/generators/ConfigGeneratorFactory.d.ts +9 -0
- package/dist/generators/ConfigGeneratorFactory.d.ts.map +1 -1
- package/dist/generators/ConfigGeneratorFactory.js +9 -0
- package/dist/generators/ConfigGeneratorFactory.js.map +1 -1
- package/dist/generators/ContinueConfigGenerator.d.ts +23 -0
- package/dist/generators/ContinueConfigGenerator.d.ts.map +1 -1
- package/dist/generators/ContinueConfigGenerator.js +23 -0
- package/dist/generators/ContinueConfigGenerator.js.map +1 -1
- package/dist/generators/CopilotConfigGenerator.d.ts +23 -0
- package/dist/generators/CopilotConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CopilotConfigGenerator.js +23 -0
- package/dist/generators/CopilotConfigGenerator.js.map +1 -1
- package/dist/generators/CursorConfigGenerator.d.ts +23 -0
- package/dist/generators/CursorConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CursorConfigGenerator.js +23 -0
- package/dist/generators/CursorConfigGenerator.js.map +1 -1
- package/dist/generators/GeminiConfigGenerator.d.ts +23 -0
- package/dist/generators/GeminiConfigGenerator.d.ts.map +1 -1
- package/dist/generators/GeminiConfigGenerator.js +23 -0
- package/dist/generators/GeminiConfigGenerator.js.map +1 -1
- package/dist/generators/VSCodeConfigGenerator.d.ts +23 -0
- package/dist/generators/VSCodeConfigGenerator.d.ts.map +1 -1
- package/dist/generators/VSCodeConfigGenerator.js +23 -0
- package/dist/generators/VSCodeConfigGenerator.js.map +1 -1
- package/dist/generators/ZCodeConfigGenerator.d.ts +23 -0
- package/dist/generators/ZCodeConfigGenerator.d.ts.map +1 -1
- package/dist/generators/ZCodeConfigGenerator.js +23 -0
- package/dist/generators/ZCodeConfigGenerator.js.map +1 -1
- package/dist/graph/blastRadius.d.ts.map +1 -1
- package/dist/graph/blastRadius.js +12 -7
- package/dist/graph/blastRadius.js.map +1 -1
- package/dist/graph/callGraph.d.ts +31 -0
- package/dist/graph/callGraph.d.ts.map +1 -1
- package/dist/graph/callGraph.js +62 -32
- package/dist/graph/callGraph.js.map +1 -1
- package/dist/graph/importClassification.d.ts +28 -0
- package/dist/graph/importClassification.d.ts.map +1 -1
- package/dist/graph/importClassification.js +57 -0
- package/dist/graph/importClassification.js.map +1 -1
- package/dist/graph/importGraph.d.ts +38 -0
- package/dist/graph/importGraph.d.ts.map +1 -1
- package/dist/graph/importGraph.js +38 -0
- package/dist/graph/importGraph.js.map +1 -1
- package/dist/graph/outputFormatter.d.ts +12 -4
- package/dist/graph/outputFormatter.d.ts.map +1 -1
- package/dist/graph/outputFormatter.js +12 -62
- package/dist/graph/outputFormatter.js.map +1 -1
- package/dist/hooks/codex.d.ts +8 -1
- package/dist/hooks/codex.d.ts.map +1 -1
- package/dist/hooks/codex.js +7 -0
- package/dist/hooks/codex.js.map +1 -1
- package/dist/hooks/core.d.ts +6 -0
- package/dist/hooks/core.d.ts.map +1 -1
- package/dist/hooks/core.js +6 -1
- package/dist/hooks/core.js.map +1 -1
- package/dist/hooks/cursor.d.ts +11 -1
- package/dist/hooks/cursor.d.ts.map +1 -1
- package/dist/hooks/cursor.js +10 -0
- package/dist/hooks/cursor.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/installConfig.d.ts +23 -5
- package/dist/installConfig.d.ts.map +1 -1
- package/dist/installConfig.js +23 -5
- package/dist/installConfig.js.map +1 -1
- package/dist/installer.d.ts +6 -0
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +6 -0
- package/dist/installer.js.map +1 -1
- package/dist/invariants/ruleEngine.d.ts +3 -0
- package/dist/invariants/ruleEngine.d.ts.map +1 -1
- package/dist/invariants/ruleEngine.js +156 -119
- package/dist/invariants/ruleEngine.js.map +1 -1
- package/dist/invariants/ruleValidator.d.ts +3 -0
- package/dist/invariants/ruleValidator.d.ts.map +1 -1
- package/dist/invariants/ruleValidator.js +185 -178
- package/dist/invariants/ruleValidator.js.map +1 -1
- package/dist/languages/LanguageOrchestrator.d.ts +4 -1
- package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
- package/dist/languages/LanguageOrchestrator.js +8 -19
- package/dist/languages/LanguageOrchestrator.js.map +1 -1
- package/dist/languages/RuntimeManager.d.ts +8 -0
- package/dist/languages/RuntimeManager.d.ts.map +1 -1
- package/dist/languages/RuntimeManager.js +55 -5
- package/dist/languages/RuntimeManager.js.map +1 -1
- package/dist/languages/UniversalAnalyzer.js +1 -1
- package/dist/languages/UniversalAnalyzer.js.map +1 -1
- package/dist/languages/adapterBridge.d.ts +4 -0
- package/dist/languages/adapterBridge.d.ts.map +1 -1
- package/dist/languages/adapterBridge.js +37 -3
- package/dist/languages/adapterBridge.js.map +1 -1
- package/dist/languages/go/GoAdapter.d.ts.map +1 -1
- package/dist/languages/go/GoAdapter.js +11 -14
- package/dist/languages/go/GoAdapter.js.map +1 -1
- package/dist/languages/go/analyzer-darwin-amd64 +0 -0
- package/dist/languages/go/analyzer-darwin-arm64 +0 -0
- package/dist/languages/go/analyzer-linux-amd64 +0 -0
- package/dist/languages/go/analyzer-linux-arm64 +0 -0
- package/dist/languages/go/analyzer-src/analyzer.go +2 -0
- package/dist/languages/go/analyzer-src/dataaccess.go +388 -0
- package/dist/languages/go/analyzer-src/types.go +8 -0
- package/dist/languages/go/analyzer-windows-amd64.exe +0 -0
- package/dist/languages/go/database-packages.json +8 -0
- package/dist/languages/go/goResolution.d.ts +229 -0
- package/dist/languages/go/goResolution.d.ts.map +1 -0
- package/dist/languages/go/goResolution.js +799 -0
- package/dist/languages/go/goResolution.js.map +1 -0
- package/dist/languages/go/stdlib-packages.json +353 -0
- package/dist/languages/go/tenantInputs.d.ts +37 -0
- package/dist/languages/go/tenantInputs.d.ts.map +1 -0
- package/dist/languages/go/tenantInputs.js +121 -0
- package/dist/languages/go/tenantInputs.js.map +1 -0
- package/dist/languages/index.d.ts +1 -0
- package/dist/languages/index.d.ts.map +1 -1
- package/dist/languages/index.js +5 -0
- package/dist/languages/index.js.map +1 -1
- package/dist/languages/json/JsonAdapter.d.ts +177 -0
- package/dist/languages/json/JsonAdapter.d.ts.map +1 -0
- package/dist/languages/json/JsonAdapter.js +531 -0
- package/dist/languages/json/JsonAdapter.js.map +1 -0
- package/dist/languages/locationIndex.d.ts +41 -0
- package/dist/languages/locationIndex.d.ts.map +1 -0
- package/dist/languages/locationIndex.js +42 -0
- package/dist/languages/locationIndex.js.map +1 -0
- package/dist/languages/sql/dialectDetection.d.ts +57 -0
- package/dist/languages/sql/dialectDetection.d.ts.map +1 -0
- package/dist/languages/sql/dialectDetection.js +135 -0
- package/dist/languages/sql/dialectDetection.js.map +1 -0
- package/dist/languages/sql/sqlAst.d.ts +494 -0
- package/dist/languages/sql/sqlAst.d.ts.map +1 -0
- package/dist/languages/sql/sqlAst.js +1817 -0
- package/dist/languages/sql/sqlAst.js.map +1 -0
- package/dist/languages/testConventions.d.ts +12 -1
- package/dist/languages/testConventions.d.ts.map +1 -1
- package/dist/languages/testConventions.js +13 -1
- package/dist/languages/testConventions.js.map +1 -1
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.js +1 -1
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.js.map +1 -1
- package/dist/languages/tree-sitter/converter.js +2 -2
- package/dist/languages/tree-sitter/converter.js.map +1 -1
- package/dist/languages/tree-sitter/parser.d.ts +3 -4
- package/dist/languages/tree-sitter/parser.d.ts.map +1 -1
- package/dist/languages/tree-sitter/parser.js +10 -4
- package/dist/languages/tree-sitter/parser.js.map +1 -1
- package/dist/languages/tree-sitter/rawNode.d.ts +3 -0
- package/dist/languages/tree-sitter/rawNode.d.ts.map +1 -1
- package/dist/languages/tree-sitter/rawNode.js +3 -0
- package/dist/languages/tree-sitter/rawNode.js.map +1 -1
- package/dist/languages/types.d.ts +7 -0
- package/dist/languages/types.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +36 -0
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +168 -45
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
- package/dist/languages/typescript/database-packages.json +19 -0
- package/dist/ledger.d.ts +118 -2
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +167 -45
- package/dist/ledger.js.map +1 -1
- package/dist/legacyIndexNotice.d.ts +32 -0
- package/dist/legacyIndexNotice.d.ts.map +1 -0
- package/dist/legacyIndexNotice.js +89 -0
- package/dist/legacyIndexNotice.js.map +1 -0
- package/dist/mcp-tools/discoveryQueries.d.ts +55 -0
- package/dist/mcp-tools/discoveryQueries.d.ts.map +1 -0
- package/dist/mcp-tools/discoveryQueries.js +157 -0
- package/dist/mcp-tools/discoveryQueries.js.map +1 -0
- package/dist/mcp-tools/projectTasks.d.ts +9 -123
- package/dist/mcp-tools/projectTasks.d.ts.map +1 -1
- package/dist/mcp-tools/projectTasks.js +180 -170
- package/dist/mcp-tools/projectTasks.js.map +1 -1
- package/dist/mcp-tools/whitelistTools.d.ts +20 -0
- package/dist/mcp-tools/whitelistTools.d.ts.map +1 -1
- package/dist/mcp-tools/whitelistTools.js +20 -0
- package/dist/mcp-tools/whitelistTools.js.map +1 -1
- package/dist/mcp-tools/workflowGuide.d.ts +9 -0
- package/dist/mcp-tools/workflowGuide.d.ts.map +1 -1
- package/dist/mcp-tools/workflowGuide.js +13 -5
- package/dist/mcp-tools/workflowGuide.js.map +1 -1
- package/dist/mcp-tools-shared.d.ts +78 -0
- package/dist/mcp-tools-shared.d.ts.map +1 -1
- package/dist/mcp-tools-shared.js +203 -340
- package/dist/mcp-tools-shared.js.map +1 -1
- package/dist/mcp-ui-simple.d.ts +2 -0
- package/dist/mcp-ui-simple.d.ts.map +1 -1
- package/dist/mcp-ui-simple.js +12 -5
- package/dist/mcp-ui-simple.js.map +1 -1
- package/dist/mcp.d.ts +6 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +827 -850
- package/dist/mcp.js.map +1 -1
- package/dist/mcpAuditJobs.d.ts +29 -33
- package/dist/mcpAuditJobs.d.ts.map +1 -1
- package/dist/mcpAuditJobs.js +102 -791
- package/dist/mcpAuditJobs.js.map +1 -1
- package/dist/mcpAutoIndex.d.ts +5 -0
- package/dist/mcpAutoIndex.d.ts.map +1 -1
- package/dist/mcpAutoIndex.js +7 -2
- package/dist/mcpAutoIndex.js.map +1 -1
- package/dist/mcpDiagnostics.d.ts +7 -0
- package/dist/mcpDiagnostics.d.ts.map +1 -1
- package/dist/mcpDiagnostics.js +34 -9
- package/dist/mcpDiagnostics.js.map +1 -1
- package/dist/mcpToolErrors.d.ts +16 -0
- package/dist/mcpToolErrors.d.ts.map +1 -1
- package/dist/mcpToolErrors.js +16 -0
- package/dist/mcpToolErrors.js.map +1 -1
- package/dist/nextFile.d.ts +6 -0
- package/dist/nextFile.d.ts.map +1 -1
- package/dist/nextFile.js +6 -0
- package/dist/nextFile.js.map +1 -1
- package/dist/nextFileIncremental.d.ts +44 -4
- package/dist/nextFileIncremental.d.ts.map +1 -1
- package/dist/nextFileIncremental.js +47 -7
- package/dist/nextFileIncremental.js.map +1 -1
- package/dist/phase/batchFunctions.d.ts +38 -0
- package/dist/phase/batchFunctions.d.ts.map +1 -0
- package/dist/phase/batchFunctions.js +64 -0
- package/dist/phase/batchFunctions.js.map +1 -0
- package/dist/phase/checks.d.ts +39 -0
- package/dist/phase/checks.d.ts.map +1 -0
- package/dist/phase/checks.js +48 -0
- package/dist/phase/checks.js.map +1 -0
- package/dist/phase/codeBlocks.d.ts +25 -0
- package/dist/phase/codeBlocks.d.ts.map +1 -0
- package/dist/phase/codeBlocks.js +268 -0
- package/dist/phase/codeBlocks.js.map +1 -0
- package/dist/phase/colorMath.d.ts +41 -0
- package/dist/phase/colorMath.d.ts.map +1 -0
- package/dist/phase/colorMath.js +109 -0
- package/dist/phase/colorMath.js.map +1 -0
- package/dist/phase/config.d.ts +29 -0
- package/dist/phase/config.d.ts.map +1 -0
- package/dist/phase/config.js +54 -0
- package/dist/phase/config.js.map +1 -0
- package/dist/phase/consumed.d.ts +47 -0
- package/dist/phase/consumed.d.ts.map +1 -0
- package/dist/phase/consumed.js +22 -0
- package/dist/phase/consumed.js.map +1 -0
- package/dist/phase/conventionMining.d.ts +59 -0
- package/dist/phase/conventionMining.d.ts.map +1 -0
- package/dist/phase/conventionMining.js +152 -0
- package/dist/phase/conventionMining.js.map +1 -0
- package/dist/phase/coverage.d.ts +65 -0
- package/dist/phase/coverage.d.ts.map +1 -0
- package/dist/phase/coverage.js +121 -0
- package/dist/phase/coverage.js.map +1 -0
- package/dist/phase/dag.d.ts +49 -0
- package/dist/phase/dag.d.ts.map +1 -0
- package/dist/phase/dag.js +120 -0
- package/dist/phase/dag.js.map +1 -0
- package/dist/phase/dataAccessCalls.d.ts +33 -0
- package/dist/phase/dataAccessCalls.d.ts.map +1 -0
- package/dist/phase/dataAccessCalls.js +42 -0
- package/dist/phase/dataAccessCalls.js.map +1 -0
- package/dist/phase/dataAccessCallsCandidates.d.ts +25 -0
- package/dist/phase/dataAccessCallsCandidates.d.ts.map +1 -0
- package/dist/phase/dataAccessCallsCandidates.js +27 -0
- package/dist/phase/dataAccessCallsCandidates.js.map +1 -0
- package/dist/phase/dynamicSql.d.ts +18 -0
- package/dist/phase/dynamicSql.d.ts.map +1 -0
- package/dist/phase/dynamicSql.js +20 -0
- package/dist/phase/dynamicSql.js.map +1 -0
- package/dist/phase/exportForm.d.ts +23 -0
- package/dist/phase/exportForm.d.ts.map +1 -0
- package/dist/phase/exportForm.js +29 -0
- package/dist/phase/exportForm.js.map +1 -0
- package/dist/phase/exportSymbols.d.ts +21 -0
- package/dist/phase/exportSymbols.d.ts.map +1 -0
- package/dist/phase/exportSymbols.js +28 -0
- package/dist/phase/exportSymbols.js.map +1 -0
- package/dist/phase/fileHeader.d.ts +15 -0
- package/dist/phase/fileHeader.d.ts.map +1 -0
- package/dist/phase/fileHeader.js +33 -0
- package/dist/phase/fileHeader.js.map +1 -0
- package/dist/phase/fileImports.d.ts +39 -0
- package/dist/phase/fileImports.d.ts.map +1 -0
- package/dist/phase/fileImports.js +138 -0
- package/dist/phase/fileImports.js.map +1 -0
- package/dist/phase/fileSymbols.d.ts +25 -0
- package/dist/phase/fileSymbols.d.ts.map +1 -0
- package/dist/phase/fileSymbols.js +302 -0
- package/dist/phase/fileSymbols.js.map +1 -0
- package/dist/phase/functionBodies.d.ts +28 -0
- package/dist/phase/functionBodies.d.ts.map +1 -0
- package/dist/phase/functionBodies.js +67 -0
- package/dist/phase/functionBodies.js.map +1 -0
- package/dist/phase/functionIndex.d.ts +28 -0
- package/dist/phase/functionIndex.d.ts.map +1 -0
- package/dist/phase/functionIndex.js +152 -0
- package/dist/phase/functionIndex.js.map +1 -0
- package/dist/phase/goFunctionAnalysis.d.ts +47 -0
- package/dist/phase/goFunctionAnalysis.d.ts.map +1 -0
- package/dist/phase/goFunctionAnalysis.js +291 -0
- package/dist/phase/goFunctionAnalysis.js.map +1 -0
- package/dist/phase/goFunctions.d.ts +38 -0
- package/dist/phase/goFunctions.d.ts.map +1 -0
- package/dist/phase/goFunctions.js +154 -0
- package/dist/phase/goFunctions.js.map +1 -0
- package/dist/phase/goImports.d.ts +29 -0
- package/dist/phase/goImports.d.ts.map +1 -0
- package/dist/phase/goImports.js +68 -0
- package/dist/phase/goImports.js.map +1 -0
- package/dist/phase/goPackageBindings.d.ts +22 -0
- package/dist/phase/goPackageBindings.d.ts.map +1 -0
- package/dist/phase/goPackageBindings.js +30 -0
- package/dist/phase/goPackageBindings.js.map +1 -0
- package/dist/phase/goSwitches.d.ts +28 -0
- package/dist/phase/goSwitches.d.ts.map +1 -0
- package/dist/phase/goSwitches.js +52 -0
- package/dist/phase/goSwitches.js.map +1 -0
- package/dist/phase/importForm.d.ts +20 -0
- package/dist/phase/importForm.d.ts.map +1 -0
- package/dist/phase/importForm.js +27 -0
- package/dist/phase/importForm.js.map +1 -0
- package/dist/phase/importSpecifiers.d.ts +20 -0
- package/dist/phase/importSpecifiers.d.ts.map +1 -0
- package/dist/phase/importSpecifiers.js +30 -0
- package/dist/phase/importSpecifiers.js.map +1 -0
- package/dist/phase/imports.d.ts +18 -0
- package/dist/phase/imports.d.ts.map +1 -0
- package/dist/phase/imports.js +25 -0
- package/dist/phase/imports.js.map +1 -0
- package/dist/phase/jsonDocument.d.ts +26 -0
- package/dist/phase/jsonDocument.d.ts.map +1 -0
- package/dist/phase/jsonDocument.js +36 -0
- package/dist/phase/jsonDocument.js.map +1 -0
- package/dist/phase/localBinding.d.ts +33 -0
- package/dist/phase/localBinding.d.ts.map +1 -0
- package/dist/phase/localBinding.js +208 -0
- package/dist/phase/localBinding.js.map +1 -0
- package/dist/phase/loopQueries.d.ts +31 -0
- package/dist/phase/loopQueries.d.ts.map +1 -0
- package/dist/phase/loopQueries.js +44 -0
- package/dist/phase/loopQueries.js.map +1 -0
- package/dist/phase/loopQueryCandidates.d.ts +26 -0
- package/dist/phase/loopQueryCandidates.d.ts.map +1 -0
- package/dist/phase/loopQueryCandidates.js +28 -0
- package/dist/phase/loopQueryCandidates.js.map +1 -0
- package/dist/phase/oracles.d.ts +216 -0
- package/dist/phase/oracles.d.ts.map +1 -0
- package/dist/phase/oracles.js +484 -0
- package/dist/phase/oracles.js.map +1 -0
- package/dist/phase/phaseModel.d.ts +190 -0
- package/dist/phase/phaseModel.d.ts.map +1 -0
- package/dist/phase/phaseModel.js +468 -0
- package/dist/phase/phaseModel.js.map +1 -0
- package/dist/phase/producers.d.ts +450 -0
- package/dist/phase/producers.d.ts.map +1 -0
- package/dist/phase/producers.js +1027 -0
- package/dist/phase/producers.js.map +1 -0
- package/dist/phase/querySiteCandidates.d.ts +27 -0
- package/dist/phase/querySiteCandidates.d.ts.map +1 -0
- package/dist/phase/querySiteCandidates.js +66 -0
- package/dist/phase/querySiteCandidates.js.map +1 -0
- package/dist/phase/querySites.d.ts +30 -0
- package/dist/phase/querySites.d.ts.map +1 -0
- package/dist/phase/querySites.js +93 -0
- package/dist/phase/querySites.js.map +1 -0
- package/dist/phase/reachability.d.ts +61 -0
- package/dist/phase/reachability.d.ts.map +1 -0
- package/dist/phase/reachability.js +104 -0
- package/dist/phase/reachability.js.map +1 -0
- package/dist/phase/reactComponents.d.ts +33 -0
- package/dist/phase/reactComponents.d.ts.map +1 -0
- package/dist/phase/reactComponents.js +47 -0
- package/dist/phase/reactComponents.js.map +1 -0
- package/dist/phase/receiverActivity.d.ts +35 -0
- package/dist/phase/receiverActivity.d.ts.map +1 -0
- package/dist/phase/receiverActivity.js +66 -0
- package/dist/phase/receiverActivity.js.map +1 -0
- package/dist/phase/receiverConsumers.d.ts +200 -0
- package/dist/phase/receiverConsumers.d.ts.map +1 -0
- package/dist/phase/receiverConsumers.js +1025 -0
- package/dist/phase/receiverConsumers.js.map +1 -0
- package/dist/phase/receiverProvenance.d.ts +97 -0
- package/dist/phase/receiverProvenance.d.ts.map +1 -0
- package/dist/phase/receiverProvenance.js +337 -0
- package/dist/phase/receiverProvenance.js.map +1 -0
- package/dist/phase/routing.d.ts +47 -0
- package/dist/phase/routing.d.ts.map +1 -0
- package/dist/phase/routing.js +62 -0
- package/dist/phase/routing.js.map +1 -0
- package/dist/phase/rules/conventions.d.ts +61 -0
- package/dist/phase/rules/conventions.d.ts.map +1 -0
- package/dist/phase/rules/conventions.js +403 -0
- package/dist/phase/rules/conventions.js.map +1 -0
- package/dist/phase/rules/crossDomain.d.ts +102 -0
- package/dist/phase/rules/crossDomain.d.ts.map +1 -0
- package/dist/phase/rules/crossDomain.js +918 -0
- package/dist/phase/rules/crossDomain.js.map +1 -0
- package/dist/phase/rules/dataAccess.d.ts +62 -0
- package/dist/phase/rules/dataAccess.d.ts.map +1 -0
- package/dist/phase/rules/dataAccess.js +480 -0
- package/dist/phase/rules/dataAccess.js.map +1 -0
- package/dist/phase/rules/dependencyGraph.d.ts +44 -0
- package/dist/phase/rules/dependencyGraph.d.ts.map +1 -0
- package/dist/phase/rules/dependencyGraph.js +260 -0
- package/dist/phase/rules/dependencyGraph.js.map +1 -0
- package/dist/phase/rules/documentation.d.ts +45 -0
- package/dist/phase/rules/documentation.d.ts.map +1 -0
- package/dist/phase/rules/documentation.js +340 -0
- package/dist/phase/rules/documentation.js.map +1 -0
- package/dist/phase/rules/dry.d.ts +109 -0
- package/dist/phase/rules/dry.d.ts.map +1 -0
- package/dist/phase/rules/dry.js +803 -0
- package/dist/phase/rules/dry.js.map +1 -0
- package/dist/phase/rules/fileDocumentation.d.ts +36 -0
- package/dist/phase/rules/fileDocumentation.d.ts.map +1 -0
- package/dist/phase/rules/fileDocumentation.js +134 -0
- package/dist/phase/rules/fileDocumentation.js.map +1 -0
- package/dist/phase/rules/functionBodies.d.ts +33 -0
- package/dist/phase/rules/functionBodies.d.ts.map +1 -0
- package/dist/phase/rules/functionBodies.js +71 -0
- package/dist/phase/rules/functionBodies.js.map +1 -0
- package/dist/phase/rules/goRules.d.ts +49 -0
- package/dist/phase/rules/goRules.d.ts.map +1 -0
- package/dist/phase/rules/goRules.js +209 -0
- package/dist/phase/rules/goRules.js.map +1 -0
- package/dist/phase/rules/querySites.d.ts +29 -0
- package/dist/phase/rules/querySites.d.ts.map +1 -0
- package/dist/phase/rules/querySites.js +94 -0
- package/dist/phase/rules/querySites.js.map +1 -0
- package/dist/phase/rules/react.d.ts +38 -0
- package/dist/phase/rules/react.d.ts.map +1 -0
- package/dist/phase/rules/react.js +202 -0
- package/dist/phase/rules/react.js.map +1 -0
- package/dist/phase/rules/registry.d.ts +170 -0
- package/dist/phase/rules/registry.d.ts.map +1 -0
- package/dist/phase/rules/registry.js +103 -0
- package/dist/phase/rules/registry.js.map +1 -0
- package/dist/phase/rules/schema.d.ts +79 -0
- package/dist/phase/rules/schema.d.ts.map +1 -0
- package/dist/phase/rules/schema.js +273 -0
- package/dist/phase/rules/schema.js.map +1 -0
- package/dist/phase/rules/schemaJson.d.ts +32 -0
- package/dist/phase/rules/schemaJson.d.ts.map +1 -0
- package/dist/phase/rules/schemaJson.js +81 -0
- package/dist/phase/rules/schemaJson.js.map +1 -0
- package/dist/phase/rules/schemaValidator.d.ts +38 -0
- package/dist/phase/rules/schemaValidator.d.ts.map +1 -0
- package/dist/phase/rules/schemaValidator.js +107 -0
- package/dist/phase/rules/schemaValidator.js.map +1 -0
- package/dist/phase/rules/secrets.d.ts +34 -0
- package/dist/phase/rules/secrets.d.ts.map +1 -0
- package/dist/phase/rules/secrets.js +208 -0
- package/dist/phase/rules/secrets.js.map +1 -0
- package/dist/phase/rules/security.d.ts +30 -0
- package/dist/phase/rules/security.d.ts.map +1 -0
- package/dist/phase/rules/security.js +85 -0
- package/dist/phase/rules/security.js.map +1 -0
- package/dist/phase/rules/securityDefects.d.ts +37 -0
- package/dist/phase/rules/securityDefects.d.ts.map +1 -0
- package/dist/phase/rules/securityDefects.js +167 -0
- package/dist/phase/rules/securityDefects.js.map +1 -0
- package/dist/phase/rules/solid.d.ts +61 -0
- package/dist/phase/rules/solid.d.ts.map +1 -0
- package/dist/phase/rules/solid.js +562 -0
- package/dist/phase/rules/solid.js.map +1 -0
- package/dist/phase/rules/styles.d.ts +77 -0
- package/dist/phase/rules/styles.d.ts.map +1 -0
- package/dist/phase/rules/styles.js +868 -0
- package/dist/phase/rules/styles.js.map +1 -0
- package/dist/phase/rules/unreferencedModule.d.ts +26 -0
- package/dist/phase/rules/unreferencedModule.d.ts.map +1 -0
- package/dist/phase/rules/unreferencedModule.js +61 -0
- package/dist/phase/rules/unreferencedModule.js.map +1 -0
- package/dist/phase/runner.d.ts +549 -0
- package/dist/phase/runner.d.ts.map +1 -0
- package/dist/phase/runner.js +1098 -0
- package/dist/phase/runner.js.map +1 -0
- package/dist/phase/schemaCode.d.ts +34 -0
- package/dist/phase/schemaCode.d.ts.map +1 -0
- package/dist/phase/schemaCode.js +53 -0
- package/dist/phase/schemaCode.js.map +1 -0
- package/dist/phase/schemaObjects.d.ts +37 -0
- package/dist/phase/schemaObjects.d.ts.map +1 -0
- package/dist/phase/schemaObjects.js +139 -0
- package/dist/phase/schemaObjects.js.map +1 -0
- package/dist/phase/schemaUsage.d.ts +30 -0
- package/dist/phase/schemaUsage.d.ts.map +1 -0
- package/dist/phase/schemaUsage.js +99 -0
- package/dist/phase/schemaUsage.js.map +1 -0
- package/dist/phase/schemaUsageCandidates.d.ts +32 -0
- package/dist/phase/schemaUsageCandidates.d.ts.map +1 -0
- package/dist/phase/schemaUsageCandidates.js +165 -0
- package/dist/phase/schemaUsageCandidates.js.map +1 -0
- package/dist/phase/schemaValidations.d.ts +34 -0
- package/dist/phase/schemaValidations.d.ts.map +1 -0
- package/dist/phase/schemaValidations.js +53 -0
- package/dist/phase/schemaValidations.js.map +1 -0
- package/dist/phase/secretCandidates.d.ts +28 -0
- package/dist/phase/secretCandidates.d.ts.map +1 -0
- package/dist/phase/secretCandidates.js +132 -0
- package/dist/phase/secretCandidates.js.map +1 -0
- package/dist/phase/securityCandidates.d.ts +29 -0
- package/dist/phase/securityCandidates.d.ts.map +1 -0
- package/dist/phase/securityCandidates.js +469 -0
- package/dist/phase/securityCandidates.js.map +1 -0
- package/dist/phase/seeded-defects.d.ts +18 -0
- package/dist/phase/seeded-defects.d.ts.map +1 -0
- package/dist/phase/seeded-defects.js +61 -0
- package/dist/phase/seeded-defects.js.map +1 -0
- package/dist/phase/stringLiterals.d.ts +24 -0
- package/dist/phase/stringLiterals.d.ts.map +1 -0
- package/dist/phase/stringLiterals.js +35 -0
- package/dist/phase/stringLiterals.js.map +1 -0
- package/dist/phase/stylesCss.d.ts +25 -0
- package/dist/phase/stylesCss.d.ts.map +1 -0
- package/dist/phase/stylesCss.js +34 -0
- package/dist/phase/stylesCss.js.map +1 -0
- package/dist/phase/stylesMarkup.d.ts +31 -0
- package/dist/phase/stylesMarkup.d.ts.map +1 -0
- package/dist/phase/stylesMarkup.js +48 -0
- package/dist/phase/stylesMarkup.js.map +1 -0
- package/dist/phase/stylesSource.d.ts +36 -0
- package/dist/phase/stylesSource.d.ts.map +1 -0
- package/dist/phase/stylesSource.js +50 -0
- package/dist/phase/stylesSource.js.map +1 -0
- package/dist/phase/typeDeclarations.d.ts +33 -0
- package/dist/phase/typeDeclarations.d.ts.map +1 -0
- package/dist/phase/typeDeclarations.js +76 -0
- package/dist/phase/typeDeclarations.js.map +1 -0
- package/dist/phase/types.d.ts +2191 -0
- package/dist/phase/types.d.ts.map +1 -0
- package/dist/phase/types.js +27 -0
- package/dist/phase/types.js.map +1 -0
- package/dist/phase/withinFileProvenance.d.ts +50 -0
- package/dist/phase/withinFileProvenance.d.ts.map +1 -0
- package/dist/phase/withinFileProvenance.js +112 -0
- package/dist/phase/withinFileProvenance.js.map +1 -0
- package/dist/pipeline.d.ts +37 -30
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +131 -326
- package/dist/pipeline.js.map +1 -1
- package/dist/pipelineAdapters.d.ts +104 -21
- package/dist/pipelineAdapters.d.ts.map +1 -1
- package/dist/pipelineAdapters.js +1144 -984
- package/dist/pipelineAdapters.js.map +1 -1
- package/dist/pipelineTypes.d.ts +21 -0
- package/dist/pipelineTypes.d.ts.map +1 -1
- package/dist/pipelineTypes.js +21 -10
- package/dist/pipelineTypes.js.map +1 -1
- package/dist/presets/presets.d.ts +9 -2
- package/dist/presets/presets.d.ts.map +1 -1
- package/dist/presets/presets.js +15 -26
- package/dist/presets/presets.js.map +1 -1
- package/dist/reporting/csvReportGenerator.d.ts +14 -3
- package/dist/reporting/csvReportGenerator.d.ts.map +1 -1
- package/dist/reporting/csvReportGenerator.js +14 -3
- package/dist/reporting/csvReportGenerator.js.map +1 -1
- package/dist/reporting/htmlReportGenerator.d.ts +5 -1
- package/dist/reporting/htmlReportGenerator.d.ts.map +1 -1
- package/dist/reporting/htmlReportGenerator.js +5 -1
- package/dist/reporting/htmlReportGenerator.js.map +1 -1
- package/dist/reporting/jsonReportGenerator.d.ts +9 -2
- package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
- package/dist/reporting/jsonReportGenerator.js +19 -5
- package/dist/reporting/jsonReportGenerator.js.map +1 -1
- package/dist/reporting/reportGenerator.d.ts +9 -2
- package/dist/reporting/reportGenerator.d.ts.map +1 -1
- package/dist/reporting/reportGenerator.js +9 -2
- package/dist/reporting/reportGenerator.js.map +1 -1
- package/dist/reporting/sarifReportGenerator.d.ts +14 -0
- package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
- package/dist/reporting/sarifReportGenerator.js +14 -0
- package/dist/reporting/sarifReportGenerator.js.map +1 -1
- package/dist/reporting/sizeDistribution.d.ts +19 -2
- package/dist/reporting/sizeDistribution.d.ts.map +1 -1
- package/dist/reporting/sizeDistribution.js +23 -5
- package/dist/reporting/sizeDistribution.js.map +1 -1
- package/dist/ruleAliases.d.ts +6 -8
- package/dist/ruleAliases.d.ts.map +1 -1
- package/dist/ruleAliases.js +29 -4
- package/dist/ruleAliases.js.map +1 -1
- package/dist/search/QueryParser.d.ts +1 -0
- package/dist/search/QueryParser.d.ts.map +1 -1
- package/dist/search/QueryParser.js +32 -91
- package/dist/search/QueryParser.js.map +1 -1
- package/dist/services/CodeMapGenerator.d.ts +19 -20
- package/dist/services/CodeMapGenerator.d.ts.map +1 -1
- package/dist/services/CodeMapGenerator.js +214 -194
- package/dist/services/CodeMapGenerator.js.map +1 -1
- package/dist/services/ProjectTaskRepository.d.ts +65 -3
- package/dist/services/ProjectTaskRepository.d.ts.map +1 -1
- package/dist/services/ProjectTaskRepository.js +94 -25
- package/dist/services/ProjectTaskRepository.js.map +1 -1
- package/dist/services/auditJobService.d.ts +17 -1
- package/dist/services/auditJobService.d.ts.map +1 -1
- package/dist/services/auditJobService.js +16 -0
- package/dist/services/auditJobService.js.map +1 -1
- package/dist/services/fileAccounting.d.ts +43 -5
- package/dist/services/fileAccounting.d.ts.map +1 -1
- package/dist/services/fileAccounting.js +48 -9
- package/dist/services/fileAccounting.js.map +1 -1
- package/dist/services/projectTaskHelpers.d.ts +45 -0
- package/dist/services/projectTaskHelpers.d.ts.map +1 -1
- package/dist/services/projectTaskHelpers.js +45 -0
- package/dist/services/projectTaskHelpers.js.map +1 -1
- package/dist/services/whitelistService.d.ts +40 -5
- package/dist/services/whitelistService.d.ts.map +1 -1
- package/dist/services/whitelistService.js +87 -46
- package/dist/services/whitelistService.js.map +1 -1
- package/dist/sqlite/betterSqlite3.d.ts +35 -1
- package/dist/sqlite/betterSqlite3.d.ts.map +1 -1
- package/dist/sqlite/betterSqlite3.js +35 -1
- package/dist/sqlite/betterSqlite3.js.map +1 -1
- package/dist/sqlite/driver.d.ts +23 -0
- package/dist/sqlite/driver.d.ts.map +1 -1
- package/dist/sqlite/driver.js +23 -0
- package/dist/sqlite/driver.js.map +1 -1
- package/dist/sqlite/nodeSqlite.d.ts +35 -1
- package/dist/sqlite/nodeSqlite.d.ts.map +1 -1
- package/dist/sqlite/nodeSqlite.js +35 -1
- package/dist/sqlite/nodeSqlite.js.map +1 -1
- package/dist/structuralSignature.d.ts +9 -0
- package/dist/structuralSignature.d.ts.map +1 -1
- package/dist/structuralSignature.js +9 -0
- package/dist/structuralSignature.js.map +1 -1
- package/dist/styles/cssAstExtractor.d.ts +15 -0
- package/dist/styles/cssAstExtractor.d.ts.map +1 -1
- package/dist/styles/cssAstExtractor.js +15 -0
- package/dist/styles/cssAstExtractor.js.map +1 -1
- package/dist/styles/normalizer.d.ts +6 -0
- package/dist/styles/normalizer.d.ts.map +1 -1
- package/dist/styles/normalizer.js +6 -0
- package/dist/styles/normalizer.js.map +1 -1
- package/dist/styles/styleExtractor.d.ts +29 -2
- package/dist/styles/styleExtractor.d.ts.map +1 -1
- package/dist/styles/styleExtractor.js +67 -48
- package/dist/styles/styleExtractor.js.map +1 -1
- package/dist/styles/styleIndexer.d.ts +8 -0
- package/dist/styles/styleIndexer.d.ts.map +1 -1
- package/dist/styles/styleIndexer.js +135 -138
- package/dist/styles/styleIndexer.js.map +1 -1
- package/dist/styles/tailwindConfigLoader.d.ts +12 -0
- package/dist/styles/tailwindConfigLoader.d.ts.map +1 -1
- package/dist/styles/tailwindConfigLoader.js +12 -0
- package/dist/styles/tailwindConfigLoader.js.map +1 -1
- package/dist/styles/tailwindExpander.d.ts +3 -0
- package/dist/styles/tailwindExpander.d.ts.map +1 -1
- package/dist/styles/tailwindExpander.js +8 -4
- package/dist/styles/tailwindExpander.js.map +1 -1
- package/dist/styles/tailwindProbe.d.ts +9 -0
- package/dist/styles/tailwindProbe.d.ts.map +1 -1
- package/dist/styles/tailwindProbe.js +181 -166
- package/dist/styles/tailwindProbe.js.map +1 -1
- package/dist/styles/tailwindUtilityExpander.d.ts +13 -0
- package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
- package/dist/styles/tailwindUtilityExpander.js +13 -0
- package/dist/styles/tailwindUtilityExpander.js.map +1 -1
- package/dist/symbols.d.ts +13 -11
- package/dist/symbols.d.ts.map +1 -1
- package/dist/symbols.js +14 -21
- package/dist/symbols.js.map +1 -1
- package/dist/telemetry.d.ts +31 -3
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +31 -3
- package/dist/telemetry.js.map +1 -1
- package/dist/tool-registry.d.ts +37 -1
- package/dist/tool-registry.d.ts.map +1 -1
- package/dist/tool-registry.js +37 -1
- package/dist/tool-registry.js.map +1 -1
- package/dist/types.d.ts +101 -71
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -16
- package/dist/types.js.map +1 -1
- package/dist/ui/InteractivePrompts.d.ts +12 -0
- package/dist/ui/InteractivePrompts.d.ts.map +1 -1
- package/dist/ui/InteractivePrompts.js +12 -0
- package/dist/ui/InteractivePrompts.js.map +1 -1
- package/dist/utils/astParser.d.ts +15 -0
- package/dist/utils/astParser.d.ts.map +1 -1
- package/dist/utils/astParser.js +15 -0
- package/dist/utils/astParser.js.map +1 -1
- package/dist/utils/astUtils.d.ts +60 -0
- package/dist/utils/astUtils.d.ts.map +1 -1
- package/dist/utils/astUtils.js +350 -362
- package/dist/utils/astUtils.js.map +1 -1
- package/dist/utils/contentHash.d.ts +22 -0
- package/dist/utils/contentHash.d.ts.map +1 -0
- package/dist/utils/contentHash.js +26 -0
- package/dist/utils/contentHash.js.map +1 -0
- package/dist/utils/dependencyExtractor.d.ts +28 -0
- package/dist/utils/dependencyExtractor.d.ts.map +1 -1
- package/dist/utils/dependencyExtractor.js +28 -1
- package/dist/utils/dependencyExtractor.js.map +1 -1
- package/dist/utils/errorMessage.d.ts +10 -0
- package/dist/utils/errorMessage.d.ts.map +1 -0
- package/dist/utils/errorMessage.js +12 -0
- package/dist/utils/errorMessage.js.map +1 -0
- package/dist/utils/fileDiscovery.d.ts +61 -2
- package/dist/utils/fileDiscovery.d.ts.map +1 -1
- package/dist/utils/fileDiscovery.js +131 -23
- package/dist/utils/fileDiscovery.js.map +1 -1
- package/dist/utils/reactDetection.d.ts +34 -5
- package/dist/utils/reactDetection.d.ts.map +1 -1
- package/dist/utils/reactDetection.js +73 -52
- package/dist/utils/reactDetection.js.map +1 -1
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/workers/auditWorker.js +7 -3
- package/dist/workers/auditWorker.js.map +1 -1
- package/examples/.auditrc.example.json +0 -8
- package/examples/.auditrc.json +0 -11
- package/examples/.code-auditor.json +0 -2
- package/examples/nextjs.auditrc.json +0 -8
- package/examples/node-api.auditrc.json +0 -7
- package/examples/react.auditrc.json +0 -6
- package/examples/tolerant-solid.auditrc.json +0 -5
- package/package.json +11 -5
- package/plugin/.claude-plugin/manifest.json +16 -0
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/hooks/hooks.json +6 -2
- package/plugin/scripts/hook-audit.sh +41 -14
- package/plugin/scripts/hook-common.sh +50 -5
- package/plugin/scripts/hook-drift-check.sh +72 -0
- package/plugin/scripts/hook-self-audit.sh +17 -12
- package/plugin/skills/code-auditor/SKILL.md +1 -1
- package/server.json +2 -2
|
@@ -3,21 +3,27 @@
|
|
|
3
3
|
* Works across multiple programming languages using the adapter pattern
|
|
4
4
|
* Analyzes database access patterns and data layer interactions
|
|
5
5
|
*/
|
|
6
|
-
import { UniversalAnalyzer } from '../../languages/UniversalAnalyzer.js';
|
|
7
6
|
import { withRuleTiming } from '../ruleTiming.js';
|
|
8
|
-
import { buildProvenanceContext,
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*
|
|
7
|
+
import { buildProvenanceContext, getCallExpressionCallee, getMemberExpressionReceiver, extractMemberExpressionProperty, resolveSiteDialect, } from '../provenance.js';
|
|
8
|
+
import { extractStaticSql, extractGoStaticSql, isTemplateLiteral, isTaggedTemplateSqlCall, stripSqlQuotes } from '../sqlLiteral.js';
|
|
9
|
+
import { isOrmMethod, handleTypesForPackage } from '../tsEcosystem.js';
|
|
10
|
+
import { identifyHandle } from '../handleIdentification.js';
|
|
11
|
+
import { buildBindingEnv, extractInterfaceFields, resolveReceiverRoot, isDbShapedRoot, findEnclosingClassHeritage, } from '../receiverRoot.js';
|
|
12
|
+
import { buildGoBindingEnv, buildGoImportMap, classifyGoRootIdentifier, } from '../../languages/go/goResolution.js';
|
|
13
|
+
import { DB_BINDING_NAMES, DB_WRAPPER_NAMES, SQL_TAG_NAMES, } from './UniversalSchemaAnalyzer.js';
|
|
14
|
+
import { findEnclosingFunctionIdentity, functionIdentityLabel } from './schema/codeAnalysis.js';
|
|
15
|
+
import { buildOrgFilterTierSet, tableRequiresOrgFilter, DEFAULT_ORG_PREDICATE_PATTERNS, hasOrganizationFilter, } from '../orgFilterTiers.js';
|
|
16
|
+
import { resolveWhereBinding } from '../../phase/localBinding.js';
|
|
17
|
+
import { parseSql, DEFAULT_SQL_DIALECT, extractTableNames, whereFacts, isWriteStatement, isMassWriteStatement, isUpsertStatement, whereColumnRefs, isDdlStatementKind, } from '../../languages/sql/sqlAst.js';
|
|
18
|
+
/**
|
|
19
|
+
* SQL keywords recognized as evidence that a *dynamically-constructed* string is
|
|
20
|
+
* an injection surface — the host-language dataflow gate in `checkQuerySecurity`
|
|
21
|
+
* (site #11, not converted by R2). This list is NOT the SQL-ness admission gate:
|
|
22
|
+
* that decision is provenance/shape/tag-based (see `isSqlPosition` in
|
|
23
|
+
* `buildDatabaseCall`), and a keyword name list must not decide whether a string
|
|
24
|
+
* is SQL — it would silently drop keyword-less SQL (`PRAGMA`, `VACUUM`,
|
|
25
|
+
* `ANALYZE`) that provenance already establishes as SQL. `containsSQLStructure`
|
|
26
|
+
* is gone (R2 site #4); this list now feeds only `checkQuerySecurity`.
|
|
21
27
|
*/
|
|
22
28
|
const SQL_KEYWORDS = [
|
|
23
29
|
'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'FROM', 'WHERE', 'JOIN',
|
|
@@ -27,39 +33,20 @@ export const DEFAULT_DATA_ACCESS_CONFIG = {
|
|
|
27
33
|
checkOrgFilters: true,
|
|
28
34
|
checkSQLInjection: true,
|
|
29
35
|
skipTestFiles: true,
|
|
30
|
-
|
|
31
|
-
'primary': {
|
|
32
|
-
name: 'Primary Database',
|
|
33
|
-
importPatterns: ['/database/', '/db/', './db', './schema', 'drizzle', 'prisma', 'typeorm', 'knex', 'sequelize'],
|
|
34
|
-
queryPatterns: ['select', 'insert', 'update', 'delete', 'query', 'execute'],
|
|
35
|
-
ormPatterns: ['from', 'where', 'join', 'orderBy', 'groupBy']
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
organizationPatterns: [
|
|
39
|
-
'organizationId',
|
|
40
|
-
'organization_id',
|
|
41
|
-
'orgId',
|
|
42
|
-
'org_id',
|
|
43
|
-
'tenantId',
|
|
44
|
-
'tenant_id',
|
|
45
|
-
'companyId',
|
|
46
|
-
'company_id'
|
|
47
|
-
],
|
|
36
|
+
organizationPatterns: [...DEFAULT_ORG_PREDICATE_PATTERNS],
|
|
48
37
|
// Spec 21 R6.2: three-tier org-filter detection
|
|
49
38
|
orgFilterTables: [], // Tier 1: empty — user must declare
|
|
50
39
|
orgFilterColumns: ['org_id', 'tenant_id', 'organization_id', 'workspace_id'], // Tier 2
|
|
51
40
|
schemas: [], // Tier 2: schema definitions for column-based inference
|
|
52
41
|
tablePatterns: {
|
|
42
|
+
// Only the non-structural `table: 'users'` / `table = "users"` shape remains
|
|
43
|
+
// here as a user-tunable text pattern. The `.from(x)` / join / Kysely-verb
|
|
44
|
+
// shapes are read structurally by `collectOrmTableReferences`, so they are
|
|
45
|
+
// NOT duplicated as regexes — a `.from(…)` inside a string argument must not
|
|
46
|
+
// read as a table.
|
|
53
47
|
orm: [
|
|
54
|
-
/
|
|
55
|
-
/table\s*[:=]\s*["'`]?([\p{L}\p{N}_]+)["'`]?/giu,
|
|
56
|
-
/\.from\s*\(\s*([\p{L}\p{N}_]+)\s*\)/giu, // Handle .from(users) where users is a variable
|
|
57
|
-
/join\s*\(\s*([\p{L}\p{N}_]+)\s*,/giu, // Handle joins
|
|
58
|
-
/leftJoin\s*\(\s*([\p{L}\p{N}_]+)\s*,/giu,
|
|
59
|
-
/rightJoin\s*\(\s*([\p{L}\p{N}_]+)\s*,/giu,
|
|
60
|
-
/innerJoin\s*\(\s*([\p{L}\p{N}_]+)\s*,/giu
|
|
48
|
+
/table\s*[:=]\s*["'`]?([\p{L}\p{N}_]+)["'`]?/giu
|
|
61
49
|
],
|
|
62
|
-
sql: [/\b(?:INSERT(?:\s+OR\s+(?:IGNORE|REPLACE))?|REPLACE)\s+INTO\s+["'`]?([\p{L}\p{N}_]+)["'`]?/giu, /DELETE\s+FROM\s+["'`]?([\p{L}\p{N}_]+)["'`]?/giu, /FROM\s+["'`]?([\p{L}\p{N}_]+)["'`]?/giu, /JOIN\s+["'`]?([\p{L}\p{N}_]+)["'`]?/giu, /UPDATE\s+["'`]?([\p{L}\p{N}_]+)["'`]?/giu],
|
|
63
50
|
queryBuilder: [/\.from\s*\(\s*["'`]?([\p{L}\p{N}_]+)["'`]?\s*\)/giu]
|
|
64
51
|
},
|
|
65
52
|
performanceThresholds: {
|
|
@@ -77,10 +64,12 @@ export const DEFAULT_DATA_ACCESS_CONFIG = {
|
|
|
77
64
|
dbWrapperNames: [...DB_WRAPPER_NAMES],
|
|
78
65
|
// DB detection patterns — values shared with the schema analyzer, but owned by
|
|
79
66
|
// THIS analyzer's namespace (no cross-analyzer fallback in analyzeAST).
|
|
80
|
-
dbReceiverNames: [...DB_RECEIVER_NAMES],
|
|
81
|
-
dbCallMethods: [...DB_CALL_METHOD_NAMES],
|
|
82
67
|
dbBindingNames: [...DB_BINDING_NAMES],
|
|
68
|
+
sqlTagNames: [...SQL_TAG_NAMES],
|
|
83
69
|
detection: { mode: 'hybrid' },
|
|
70
|
+
// Spec 70 R1 — no dialect until the corpus names one; the SQL-content facts
|
|
71
|
+
// then `cannot-fire` rather than being regex-derived.
|
|
72
|
+
dialect: null,
|
|
84
73
|
// SQL sanitizer functions — interpolation via escapeSql(x) is not raw.
|
|
85
74
|
sanitizerNames: ['escapeSql'],
|
|
86
75
|
};
|
|
@@ -101,47 +90,406 @@ function makeViolation(file, location, message, classification) {
|
|
|
101
90
|
analyzer: 'data-access'
|
|
102
91
|
};
|
|
103
92
|
if (classification.symbol)
|
|
104
|
-
v.
|
|
93
|
+
v.symbol = classification.symbol;
|
|
105
94
|
if (classification.resolution)
|
|
106
95
|
v.resolution = classification.resolution;
|
|
107
96
|
return v;
|
|
108
97
|
}
|
|
109
98
|
/**
|
|
110
|
-
*
|
|
99
|
+
* The four companion pairs of the query-builder chain grammar (Spec 68 Thing 2,
|
|
100
|
+
* #312). A verb admits a chain only when one of its required companions appears
|
|
101
|
+
* LATER in the same callee — this is the Drizzle/Kysely grammar and is pure
|
|
102
|
+
* syntax (no catalog, no name list).
|
|
103
|
+
*/
|
|
104
|
+
const QUERY_BUILDER_COMPANIONS = [
|
|
105
|
+
// `.select` also admits Drizzle's `.selectDistinct(…)` / `.selectDistinctOn(…)`
|
|
106
|
+
// — the same select verb followed by `.from(`; `.select\b` alone would miss the
|
|
107
|
+
// `Distinct` suffix (a word character breaks the boundary).
|
|
108
|
+
{ verb: /\.select(?:DistinctOn|Distinct)?\b/, companions: [/\.from\b/] },
|
|
109
|
+
{ verb: /\.insert\b/, companions: [/\.values\b/] },
|
|
110
|
+
{ verb: /\.update\b/, companions: [/\.set\b/, /\.where\b/] },
|
|
111
|
+
{ verb: /\.delete\b/, companions: [/\.where\b/] },
|
|
112
|
+
];
|
|
113
|
+
/**
|
|
114
|
+
* Admitter 1 — verb plus required companion, tested on the CALLEE text. The
|
|
115
|
+
* callee of the outermost call in a chain carries every earlier verb with its
|
|
116
|
+
* argument (e.g. `db.select({…}).from(users).where(…)` → callee text
|
|
117
|
+
* `db.select({…}).from(users).where`), so a single substring scan over the
|
|
118
|
+
* callee sees the whole grammar. The trailing verb appears without its paren
|
|
119
|
+
* (the arguments belong to the outermost call), so companions are matched with
|
|
120
|
+
* a word boundary, not `\(`. `map.delete(k)`, `createHash().update(b)`,
|
|
121
|
+
* `cookies.delete(n)` and `stripe.customers.update(id, data)` all lack the
|
|
122
|
+
* companion and fail.
|
|
123
|
+
*/
|
|
124
|
+
/**
|
|
125
|
+
* Collect the method names of a callee's member chain, left-to-right (base
|
|
126
|
+
* first), reading each property_identifier node. String arguments and
|
|
127
|
+
* subscripts are structural — a `.from(…)` inside a string argument is NOT a
|
|
128
|
+
* chain member, so it is not collected.
|
|
111
129
|
*/
|
|
112
|
-
function
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
130
|
+
function collectChainMethods(node, adapter, sourceCode, out = []) {
|
|
131
|
+
const t = adapter.getNodeType(node);
|
|
132
|
+
if (t === 'member_expression' || t === 'selector_expression') {
|
|
133
|
+
const children = adapter.getChildren(node);
|
|
134
|
+
const object = children.find((c) => {
|
|
135
|
+
const ct = adapter.getNodeType(c);
|
|
136
|
+
return ct !== '.' && ct !== 'property_identifier' && ct !== 'field_identifier';
|
|
119
137
|
});
|
|
138
|
+
if (object)
|
|
139
|
+
collectChainMethods(object, adapter, sourceCode, out);
|
|
140
|
+
const prop = extractMemberExpressionProperty(node, adapter, sourceCode);
|
|
141
|
+
if (prop)
|
|
142
|
+
out.push(prop);
|
|
143
|
+
}
|
|
144
|
+
else if (t === 'call_expression') {
|
|
145
|
+
const callee = getCallExpressionCallee(node, adapter);
|
|
146
|
+
if (callee)
|
|
147
|
+
collectChainMethods(callee, adapter, sourceCode, out);
|
|
148
|
+
}
|
|
149
|
+
else if (t === 'subscript_expression') {
|
|
150
|
+
const children = adapter.getChildren(node);
|
|
151
|
+
const object = children.find((c) => {
|
|
152
|
+
const ct = adapter.getNodeType(c);
|
|
153
|
+
return ct !== '[' && ct !== ']';
|
|
154
|
+
});
|
|
155
|
+
if (object)
|
|
156
|
+
collectChainMethods(object, adapter, sourceCode, out);
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
function hasChainCompanion(methods) {
|
|
161
|
+
for (const { verb, companions } of QUERY_BUILDER_COMPANIONS) {
|
|
162
|
+
const verbIdx = methods.findIndex((m) => verb.test('.' + m));
|
|
163
|
+
if (verbIdx < 0)
|
|
164
|
+
continue;
|
|
165
|
+
const rest = methods.slice(verbIdx + 1);
|
|
166
|
+
if (rest.some((m) => companions.some((companion) => companion.test('.' + m))))
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
/** Prisma CRUD verbs admitted by the object-form shape (admitter 2). */
|
|
172
|
+
const PRISMA_VERBS = new Set([
|
|
173
|
+
'create', 'createMany', 'update', 'updateMany', 'upsert',
|
|
174
|
+
'delete', 'deleteMany', 'findUnique', 'findFirst', 'findMany',
|
|
175
|
+
'findUniqueOrThrow', 'findFirstOrThrow', 'findRaw', 'count', 'groupBy', 'aggregate',
|
|
176
|
+
]);
|
|
177
|
+
/** Collect the property keys of an object literal, including its nested objects. */
|
|
178
|
+
function collectObjectLiteralKeys(node, adapter, sourceCode) {
|
|
179
|
+
const keys = [];
|
|
180
|
+
const stack = [node];
|
|
181
|
+
while (stack.length) {
|
|
182
|
+
const n = stack.pop();
|
|
183
|
+
const t = adapter.getNodeType(n);
|
|
184
|
+
if (t === 'shorthand_property_identifier' || t === 'property_identifier') {
|
|
185
|
+
const txt = adapter.getNodeText(n, sourceCode)?.trim();
|
|
186
|
+
if (txt)
|
|
187
|
+
keys.push(txt);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
for (const c of adapter.getChildren(n) ?? [])
|
|
191
|
+
stack.push(c);
|
|
192
|
+
}
|
|
193
|
+
return keys;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Admitter 2 — Prisma's object form: `<recv>.<Model>.<verb>({ … })` where the
|
|
197
|
+
* object-literal argument carries a `where` or `data` key. Option 1 (chained
|
|
198
|
+
* companions) misses this because the `where`/`data` are keys inside an object
|
|
199
|
+
* literal, not chained calls (blitz `prisma.user.update({ where, data })`).
|
|
200
|
+
*/
|
|
201
|
+
function isPrismaObjectForm(node, adapter, sourceCode) {
|
|
202
|
+
const callee = getCallExpressionCallee(node, adapter);
|
|
203
|
+
if (!callee || adapter.getNodeType(callee) !== 'member_expression')
|
|
204
|
+
return false;
|
|
205
|
+
// The chain must be `<recv>.<Model>.<verb>` — at least two member segments
|
|
206
|
+
// (the Model and the verb) read from their property nodes. A subscript like
|
|
207
|
+
// `obj["a.b"]` is not a Model segment, so `obj["a.b"].update(…)` has only one
|
|
208
|
+
// segment and fails.
|
|
209
|
+
const methods = collectChainMethods(callee, adapter, sourceCode);
|
|
210
|
+
if (methods.length < 2)
|
|
211
|
+
return false;
|
|
212
|
+
if (!PRISMA_VERBS.has(methods[methods.length - 1]))
|
|
213
|
+
return false;
|
|
214
|
+
const args = adapter.getChildren(node).find((c) => adapter.getNodeType(c) === 'arguments');
|
|
215
|
+
if (!args)
|
|
216
|
+
return false;
|
|
217
|
+
for (const arg of adapter.getChildren(args)) {
|
|
218
|
+
const keys = collectObjectLiteralKeys(arg, adapter, sourceCode);
|
|
219
|
+
if (keys.includes('where') || keys.includes('data'))
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Spec 68 Thing 2 (#312) — the query-builder shape test.
|
|
226
|
+
*
|
|
227
|
+
* Answers "is this a DB query?" structurally, from the call's own grammar,
|
|
228
|
+
* instead of by the receiver's name. A chain that shapes like a Drizzle/Kysely
|
|
229
|
+
* builder or a Prisma CRUD call is a query regardless of whether its receiver
|
|
230
|
+
* is named `db`, `appDb`, `tx`, or `prisma`. This replaces the receiver-name
|
|
231
|
+
* blind spot (`DB_RECEIVER_NAMES`) without growing the name list — the raw-SQL
|
|
232
|
+
* path (`db.exec(sql)`) still relies on receiver/import provenance because it
|
|
233
|
+
* has no chain grammar to lean on.
|
|
234
|
+
*
|
|
235
|
+
* Two admitting conditions, union (not intersection):
|
|
236
|
+
* 1. verb + companion chain grammar — {@link hasChainCompanion};
|
|
237
|
+
* 2. Prisma object form — {@link isPrismaObjectForm}.
|
|
238
|
+
*
|
|
239
|
+
* A third admitter — catalog-resolved argument (a bare `.from(table)` /
|
|
240
|
+
* `.query(table)` whose verb argument resolves to a `resolution` table) —
|
|
241
|
+
* was evaluated and REJECTED. The residual gap after admitter 1+2 contains no
|
|
242
|
+
* such chain: the real misses are *variable-split* builders (the companion verb
|
|
243
|
+
* lives in a prior statement, e.g. `const q = baseQuery.where(…)`), which
|
|
244
|
+
* catalog resolution does not address. Gating on an under-discovered catalog
|
|
245
|
+
* would just rebuild the receiver-name failure one layer up. See the
|
|
246
|
+
* extraction-completeness gate (scripts/verify-extraction-completeness.ts) for
|
|
247
|
+
* the pinned residual.
|
|
248
|
+
*
|
|
249
|
+
* @param node the candidate call node to classify
|
|
250
|
+
* @param adapter the language adapter used to read the callee
|
|
251
|
+
* @param sourceCode the file source text for reading callee text
|
|
252
|
+
* @returns true when the call is query-builder shaped (chain companion or Prisma object form)
|
|
253
|
+
*/
|
|
254
|
+
export function isQueryBuilderShape(node, adapter, sourceCode) {
|
|
255
|
+
if (node.type !== 'call_expression')
|
|
256
|
+
return false;
|
|
257
|
+
const callee = getCallExpressionCallee(node, adapter);
|
|
258
|
+
if (!callee)
|
|
259
|
+
return false;
|
|
260
|
+
const methods = collectChainMethods(callee, adapter, sourceCode);
|
|
261
|
+
return hasChainCompanion(methods) || isPrismaObjectForm(node, adapter, sourceCode);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* The single admission seam for a candidate node: resolve its handle-ness through
|
|
265
|
+
* `identifyHandle` (Spec 70 R1.2) and return the tri-state verdict, or `null`
|
|
266
|
+
* when the node is not a query-shaped DB/ORM call in the TypeScript family.
|
|
267
|
+
*
|
|
268
|
+
* The candidate *filter* (a DB/ORM method, or a provenanced wrapper name) is
|
|
269
|
+
* query-shape vocabulary — the same surface `isDBMethodCall` admitted — not a
|
|
270
|
+
* handle test. Handle-ness is decided once, here: a `handle` verdict admits the
|
|
271
|
+
* site (carrying `via`); `not-handle` rejects it; `unproven` admits it *as
|
|
272
|
+
* unproven* (carrying the reason) so downstream rules see the site and report
|
|
273
|
+
* `cannot-fire` rather than never seeing it. Admission is tri-state, not boolean.
|
|
274
|
+
*/
|
|
275
|
+
function handleVerdictForCall(node, scan) {
|
|
276
|
+
const { adapter, sourceCode, config, provenanceContext, handleEnv } = scan;
|
|
277
|
+
if (!provenanceContext)
|
|
278
|
+
return null;
|
|
279
|
+
// Go: declaration resolution runs through Go's own binding/import environment
|
|
280
|
+
// (provenance seed + bindings + imports), routed through the same
|
|
281
|
+
// `identifyHandle` seam. `classifyGoRootIdentifier` checks the provenance seed
|
|
282
|
+
// first, so a cross-file-resolved `db *sql.DB` proves `handle`; the seed also
|
|
283
|
+
// carries the receiver's package, so an unrecognized import stays `unproven`
|
|
284
|
+
// (cannot-fire) rather than being guessed `not-handle` (Spec 70 R4).
|
|
285
|
+
if (adapter.name === 'go') {
|
|
286
|
+
return goHandleVerdictForCall(node, scan);
|
|
287
|
+
}
|
|
288
|
+
if (!handleEnv)
|
|
289
|
+
return null;
|
|
290
|
+
// A template literal is admitted via the call whose argument it is.
|
|
291
|
+
const callNode = isTemplateLiteral(node, adapter) ? enclosingCallOf(node, adapter) : node;
|
|
292
|
+
if (!callNode || !isFunctionCall(callNode, adapter))
|
|
293
|
+
return null;
|
|
294
|
+
// A constructor call (`new Pool()`) is an instantiation — how a handle is
|
|
295
|
+
// *created* — not a data-access call itself. Its provenanced constructor
|
|
296
|
+
// proves the *binding* (`const db = new Pool()`); the constructor expression
|
|
297
|
+
// is never a query site, so admit nothing here. (Spec 70 R4)
|
|
298
|
+
if (callNode.type === 'new_expression')
|
|
299
|
+
return null;
|
|
300
|
+
const callee = getCallExpressionCallee(callNode, adapter);
|
|
301
|
+
if (!callee)
|
|
302
|
+
return null;
|
|
303
|
+
const dialect = resolveSiteDialect(callNode, adapter, sourceCode, provenanceContext) ??
|
|
304
|
+
config.dialect ?? null;
|
|
305
|
+
const facts = () => ({
|
|
306
|
+
imports: new Map(),
|
|
307
|
+
typeAnnotations: new Map(),
|
|
308
|
+
bindings: new Map(),
|
|
309
|
+
withinFileProvenance: new Map(),
|
|
310
|
+
sqlDialect: dialect,
|
|
311
|
+
resolution: { dialect: 'ts', env: handleEnv },
|
|
312
|
+
});
|
|
313
|
+
// Bare-identifier call (`query(…)` / `d1(…)`): admit a provenanced wrapper or a
|
|
314
|
+
// type-annotated handle (`const query: D1Database = …`), and let `identifyHandle`
|
|
315
|
+
// decide handle / not-handle / unproven on the one entry point. A truly unbound
|
|
316
|
+
// name (an ambient global like `someFunction`) is rejected here — not because its
|
|
317
|
+
// name proves non-handle, but because its SQL argument must not be allowed to
|
|
318
|
+
// *prove* handle-ness (R3) on a name with no DB signal at all. The candidate
|
|
319
|
+
// filter is a DB/ORM *shape* test (provenance or an explicit type annotation),
|
|
320
|
+
// never a handle decision.
|
|
321
|
+
if (callee.type === 'identifier') {
|
|
322
|
+
const name = adapter.getNodeText(callee, sourceCode);
|
|
323
|
+
if (!name)
|
|
324
|
+
return null;
|
|
325
|
+
const binding = handleEnv.bindings.get(name);
|
|
326
|
+
const isProvenanced = provenanceContext.dbProvenanced.has(name);
|
|
327
|
+
const isTypeAnnotated = !!binding &&
|
|
328
|
+
(binding.kind === 'variable' || binding.kind === 'field' || binding.kind === 'parameter') &&
|
|
329
|
+
!!binding.typeText;
|
|
330
|
+
// A named import from a manifest DB package (`import { eq } from
|
|
331
|
+
// 'drizzle-orm'`) is a DB *signal* even when the name isn't a handle — it
|
|
332
|
+
// must reach `identifyHandle` so it can be proven `not-handle`, not dropped
|
|
333
|
+
// here on the negative for want of evidence (Spec 70 — a site that stops
|
|
334
|
+
// being reported has to be proven not-a-handle).
|
|
335
|
+
const isDbPackageImport = !!binding && binding.kind === 'import' && handleTypesForPackage(binding.source ?? '') !== undefined;
|
|
336
|
+
if (!isProvenanced && !isTypeAnnotated && !isDbPackageImport)
|
|
337
|
+
return null;
|
|
338
|
+
return identifyHandle({
|
|
339
|
+
format: 'typescript',
|
|
340
|
+
root: name,
|
|
341
|
+
receiver: name,
|
|
342
|
+
method: name,
|
|
343
|
+
sqlArgument: extractStaticSql(callNode, adapter, sourceCode),
|
|
344
|
+
thisField: false,
|
|
345
|
+
}, facts());
|
|
346
|
+
}
|
|
347
|
+
if (callee.type !== 'member_expression' && callee.type !== 'selector_expression') {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
const method = extractMemberExpressionProperty(callee, adapter, sourceCode);
|
|
351
|
+
if (!method)
|
|
352
|
+
return null;
|
|
353
|
+
const root = resolveReceiverRoot(callee, adapter, sourceCode);
|
|
354
|
+
if (root === null)
|
|
355
|
+
return null;
|
|
356
|
+
const sqlArg = extractStaticSql(callNode, adapter, sourceCode);
|
|
357
|
+
const thisRooted = receiverIsThisRooted(callee, adapter);
|
|
358
|
+
// Candidacy is the package discriminant — the receiver root's disposition, not
|
|
359
|
+
// the method name (`join` is `Array.prototype.join` and also `SQL JOIN`). A
|
|
360
|
+
// call is query-shaped when its receiver resolves handle/unproven, OR its
|
|
361
|
+
// argument is a static SQL literal (which `identifyHandle`'s sql-argument arm
|
|
362
|
+
// can prove). A provably non-DB receiver (JS global, primitive, Node builtin)
|
|
363
|
+
// with no static SQL is rejected here. A `this.<field>` root is a field
|
|
364
|
+
// reference, not an ambient global, so it stays `unproven` (admitted) rather
|
|
365
|
+
// than dropped as a phantom global.
|
|
366
|
+
if (sqlArg === null && !isDbShapedRoot(root, handleEnv, { thisField: thisRooted }))
|
|
367
|
+
return null;
|
|
368
|
+
const receiver = getMemberExpressionReceiver(callee, adapter, sourceCode) ?? root;
|
|
369
|
+
return identifyHandle({
|
|
370
|
+
format: 'typescript',
|
|
371
|
+
root,
|
|
372
|
+
receiver,
|
|
373
|
+
method,
|
|
374
|
+
sqlArgument: sqlArg,
|
|
375
|
+
thisField: thisRooted,
|
|
376
|
+
}, facts());
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* The Go arm of the admission seam: resolve a Go `selector_expression` call
|
|
380
|
+
* (`db.Query(…)`) through `identifyHandle` with Go's own resolution environment,
|
|
381
|
+
* instead of abstaining. Candidacy is a string-resolvable argument with a
|
|
382
|
+
* query-shaped-method backstop for the no-argument case; handle-ness is then
|
|
383
|
+
* decided once by the seam (`handle` via provenance seed, `unproven` via an
|
|
384
|
+
* unrecognized package, `not-handle` via a non-DB binding), never by a name list
|
|
385
|
+
* here.
|
|
386
|
+
*/
|
|
387
|
+
function goHandleVerdictForCall(node, scan) {
|
|
388
|
+
const { adapter, sourceCode, config, goEnv } = scan;
|
|
389
|
+
if (!goEnv)
|
|
390
|
+
return null;
|
|
391
|
+
if (!isFunctionCall(node, adapter))
|
|
392
|
+
return null;
|
|
393
|
+
const callee = getCallExpressionCallee(node, adapter);
|
|
394
|
+
if (!callee || callee.type !== 'selector_expression')
|
|
395
|
+
return null;
|
|
396
|
+
const method = extractMemberExpressionProperty(callee, adapter, sourceCode);
|
|
397
|
+
if (!method)
|
|
398
|
+
return null;
|
|
399
|
+
const root = resolveReceiverRoot(callee, adapter, sourceCode);
|
|
400
|
+
if (root === null)
|
|
401
|
+
return null;
|
|
402
|
+
const sqlArg = extractGoStaticSql(node, adapter, sourceCode, goEnv);
|
|
403
|
+
// Candidacy is the package discriminant — the receiver root's disposition, not
|
|
404
|
+
// the method name. A call is query-shaped when its receiver resolves
|
|
405
|
+
// handle/unproven, OR its argument is a static SQL literal (which
|
|
406
|
+
// `identifyHandle`'s sql-argument arm can prove). A provably non-DB receiver
|
|
407
|
+
// (stdlib package, primitive, non-DB binding) with no static SQL is rejected.
|
|
408
|
+
if (sqlArg === null && classifyGoRootIdentifier(root, goEnv) === 'not-handle')
|
|
409
|
+
return null;
|
|
410
|
+
const receiver = getMemberExpressionReceiver(callee, adapter, sourceCode) ?? root;
|
|
411
|
+
return identifyHandle({
|
|
412
|
+
format: 'go',
|
|
413
|
+
root,
|
|
414
|
+
receiver,
|
|
415
|
+
method,
|
|
416
|
+
sqlArgument: sqlArg,
|
|
417
|
+
thisField: false,
|
|
418
|
+
}, {
|
|
419
|
+
imports: new Map(),
|
|
420
|
+
typeAnnotations: new Map(),
|
|
421
|
+
bindings: new Map(),
|
|
422
|
+
withinFileProvenance: new Map(),
|
|
423
|
+
sqlDialect: config.dialect ?? null,
|
|
424
|
+
resolution: { dialect: 'go', env: goEnv },
|
|
120
425
|
});
|
|
121
|
-
|
|
426
|
+
}
|
|
427
|
+
/** Walk parents until an enclosing call/new expression, or null. */
|
|
428
|
+
function enclosingCallOf(node, adapter) {
|
|
429
|
+
// A template literal is the SQL argument of a call only when it sits directly
|
|
430
|
+
// in that call's argument list (`db.prepare(\`…\`)`). A literal nested inside an
|
|
431
|
+
// arrow body, object literal, or array (`{ message: () => \`…is written (…)\` }`)
|
|
432
|
+
// is prose/config, not a query — walking past those boundaries attributes it to
|
|
433
|
+
// an unrelated enclosing call (`runUsageDirectionCheck(…, { message: … })`) and
|
|
434
|
+
// lets human-readable text reach the SQL parser. Only the immediate `arguments`
|
|
435
|
+
// admits the enclosing call; anything else is not a direct query argument.
|
|
436
|
+
let cur = adapter.getParent(node);
|
|
437
|
+
while (cur) {
|
|
438
|
+
const t = adapter.getNodeType(cur);
|
|
439
|
+
if (t === 'arguments') {
|
|
440
|
+
const call = adapter.getParent(cur);
|
|
441
|
+
return call && (adapter.getNodeType(call) === 'call_expression' || adapter.getNodeType(call) === 'new_expression')
|
|
442
|
+
? call
|
|
443
|
+
: null;
|
|
444
|
+
}
|
|
445
|
+
// Tagged templates carry the literal as a direct child of the call (no
|
|
446
|
+
// `arguments`), but they are admitted separately by `isTaggedTemplateSqlCall`,
|
|
447
|
+
// never through this path — a literal not directly in `arguments` is not a
|
|
448
|
+
// positional SQL argument.
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
/** True when a receiver chain bottoms out at `this`/`super` (a field reference). */
|
|
454
|
+
function receiverIsThisRooted(callee, adapter) {
|
|
455
|
+
let current = callee;
|
|
456
|
+
while (current.type === 'member_expression' || current.type === 'selector_expression') {
|
|
457
|
+
const children = adapter.getChildren(current);
|
|
458
|
+
const firstChild = children.find((c) => c.type !== '.' && c.type !== 'property_identifier' && c.type !== 'field_identifier');
|
|
459
|
+
if (!firstChild)
|
|
460
|
+
return false;
|
|
461
|
+
current = firstChild;
|
|
462
|
+
}
|
|
463
|
+
return current.type === 'this' || current.type === 'super';
|
|
122
464
|
}
|
|
123
465
|
/**
|
|
124
466
|
* Predicate for the node-discovery pass of extractDatabaseCalls. A node is a
|
|
125
|
-
* candidate when it is a DB-
|
|
126
|
-
*
|
|
127
|
-
*
|
|
467
|
+
* candidate when it is a DB/ORM-method call whose receiver is handle-or-unproven,
|
|
468
|
+
* a tagged-template SQL call, a variable assignment holding SQL-shaped text
|
|
469
|
+
* (Spec 17 R2 — content scanning is removed in favour of provenance), or a
|
|
470
|
+
* query-builder chain discovered by shape (Spec 68 Thing 2, #312).
|
|
128
471
|
*/
|
|
129
|
-
function isDbCallCandidate(node,
|
|
130
|
-
const
|
|
131
|
-
//
|
|
132
|
-
|
|
472
|
+
function isDbCallCandidate(node, scan, tagNames = SQL_TAG_NAMES) {
|
|
473
|
+
const { adapter, sourceCode } = scan;
|
|
474
|
+
// Spec 70 R1.2: a DB/ORM-method call whose receiver resolves handle-or-unproven.
|
|
475
|
+
// `not-handle` (e.g. lodash `get`) and `null` (not query-shaped) are rejected.
|
|
476
|
+
const verdict = handleVerdictForCall(node, scan);
|
|
477
|
+
if (verdict !== null && verdict.kind !== 'not-handle')
|
|
478
|
+
return true;
|
|
479
|
+
// A query-builder chain is discovered by shape, not receiver name.
|
|
480
|
+
if (isQueryBuilderShape(node, adapter, sourceCode)) {
|
|
133
481
|
return true;
|
|
134
482
|
}
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return isTemplateInDBProvenancedCall(node, adapter, sourceCode, provenanceContext);
|
|
483
|
+
// Tagged-template SQL is a data-access call by tag name — no provenance needed.
|
|
484
|
+
if (isTaggedTemplateSqlCall(node, adapter, sourceCode, tagNames)) {
|
|
485
|
+
return true;
|
|
139
486
|
}
|
|
140
|
-
// Variable assignment
|
|
141
|
-
//
|
|
142
|
-
//
|
|
487
|
+
// Variable assignment whose RHS is a static string/template literal — the
|
|
488
|
+
// string parses as a statement or it does not (Spec 70 R2 site #4, obviated):
|
|
489
|
+
// the candidate check is now the presence of a static literal, and
|
|
490
|
+
// `buildDatabaseCall` parses it. No SQL-keyword content scan here.
|
|
143
491
|
if (isVariableAssignment(node, adapter)) {
|
|
144
|
-
return
|
|
492
|
+
return extractStaticSql(node, adapter, sourceCode) !== null;
|
|
145
493
|
}
|
|
146
494
|
return false;
|
|
147
495
|
}
|
|
@@ -176,12 +524,60 @@ function dedupeCandidateNodes(nodes, adapter) {
|
|
|
176
524
|
});
|
|
177
525
|
return uniqueNodes;
|
|
178
526
|
}
|
|
527
|
+
/** The explicit lowercased column list of a raw-SQL INSERT/REPLACE, or null for
|
|
528
|
+
* a positional INSERT (`INSERT INTO t VALUES (…)`, no column list). Mirrors
|
|
529
|
+
* sqlAst's `insertColumns` but preserves the null-vs-empty distinction the
|
|
530
|
+
* tenant-predicate rule needs.
|
|
531
|
+
* @param sqlAst - The parsed SQL statement to read the column list from.
|
|
532
|
+
* @returns The lowercased explicit column list, or `null` for a positional
|
|
533
|
+
* insert or a non-INSERT/REPLACE statement. */
|
|
534
|
+
export function rawInsertColumnsFromAst(sqlAst) {
|
|
535
|
+
if (sqlAst.type !== 'insert' && sqlAst.type !== 'replace')
|
|
536
|
+
return null;
|
|
537
|
+
const columns = sqlAst.columns;
|
|
538
|
+
if (!Array.isArray(columns))
|
|
539
|
+
return null;
|
|
540
|
+
return columns.filter((c) => typeof c === 'string').map((c) => c.toLowerCase());
|
|
541
|
+
}
|
|
542
|
+
/** The Kysely builder write verb carried as a camelCase method name — a
|
|
543
|
+
* host-language chain shape, not SQL. `selectFrom`/`selectAll` are reads.
|
|
544
|
+
* Read from the callee's member chain, not the source text: a `.insertInto(…)`
|
|
545
|
+
* inside a string argument (`db.raw(".insertInto(x)")`) is not a chain member.
|
|
546
|
+
*
|
|
547
|
+
* @param node the candidate call-expression node
|
|
548
|
+
* @param adapter the language adapter used to read the member chain
|
|
549
|
+
* @param sourceCode the file source text for reading node text
|
|
550
|
+
* @returns the Kysely write verb, or null when the chain is not a write */
|
|
551
|
+
export function builderWriteVerb(node, adapter, sourceCode) {
|
|
552
|
+
const methods = collectChainMethods(node, adapter, sourceCode);
|
|
553
|
+
if (methods.includes('insertInto'))
|
|
554
|
+
return 'insert';
|
|
555
|
+
if (methods.includes('updateTable'))
|
|
556
|
+
return 'update';
|
|
557
|
+
if (methods.includes('deleteFrom'))
|
|
558
|
+
return 'delete';
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
/** True when an ORM chain carries a row-limiting shape (`.where(...)`, `.having(...)`,
|
|
562
|
+
* `.limit(...)`, `.andWhere(...)`, `.orWhere(...)`) — the host-language analog of
|
|
563
|
+
* SQL's WHERE/HAVING/LIMIT that the AST walk cannot see because there is no SQL.
|
|
564
|
+
* Read from the member chain, not the source text: a `.where(…)` inside a string
|
|
565
|
+
* argument is a string, not a chain member.
|
|
566
|
+
*
|
|
567
|
+
* @param node the candidate call-expression node
|
|
568
|
+
* @param adapter the language adapter used to read the member chain
|
|
569
|
+
* @param sourceCode the file source text for reading node text
|
|
570
|
+
* @returns true when the chain carries a row-limiting shape */
|
|
571
|
+
export function hasOrmFilterShape(node, adapter, sourceCode) {
|
|
572
|
+
const methods = collectChainMethods(node, adapter, sourceCode);
|
|
573
|
+
return methods.some((m) => m === 'where' || m === 'andWhere' || m === 'orWhere' || m === 'having' || m === 'limit');
|
|
574
|
+
}
|
|
179
575
|
/**
|
|
180
576
|
* Classify a single candidate node into a DatabaseCall, or null when it does not
|
|
181
577
|
* look like a DB-related query.
|
|
182
578
|
*/
|
|
183
579
|
function buildDatabaseCall(node, ast, scan) {
|
|
184
|
-
const { adapter, sourceCode,
|
|
580
|
+
const { adapter, sourceCode, config, provenanceContext } = scan;
|
|
185
581
|
const nodeText = stripComments(adapter.getNodeText(node, sourceCode));
|
|
186
582
|
if (!nodeText || nodeText.trim().length < 10)
|
|
187
583
|
return null;
|
|
@@ -189,15 +585,83 @@ function buildDatabaseCall(node, ast, scan) {
|
|
|
189
585
|
// its template argument (path 2) — the template string is the precise target.
|
|
190
586
|
if (shouldSkipCallForTemplateArg(node, adapter))
|
|
191
587
|
return null;
|
|
192
|
-
|
|
588
|
+
// The candidate's static SQL argument (unquoted), or null when it has none
|
|
589
|
+
// or is dynamically interpolated.
|
|
590
|
+
const sqlArg = extractStaticSql(node, adapter, sourceCode);
|
|
591
|
+
// Spec 70 R2 — parse the static SQL once and derive every SQL-content fact from
|
|
592
|
+
// the AST. The dialect is per-call-site (Spec 70): `pool.query(…)` where `pool`
|
|
593
|
+
// resolves to `pg` parses as postgres even in a repo that also names `mysql2`;
|
|
594
|
+
// the repo-level detection result is only the fallback when the receiver
|
|
595
|
+
// doesn't resolve to a single-dialect package. R2 does not condition parsing on
|
|
596
|
+
// proving the dialect, so when none resolves the static SQL is still attempted
|
|
597
|
+
// under {@link DEFAULT_SQL_DIALECT}; a literal that parses yields its facts, and
|
|
598
|
+
// only a genuine parse failure (under the resolved dialect, or the default with
|
|
599
|
+
// the dialect undetermined) leaves the facts absent (`cannot-fire`).
|
|
600
|
+
const siteDialect = resolveSiteDialect(node, adapter, sourceCode, provenanceContext);
|
|
601
|
+
const dialect = siteDialect ?? config.dialect ?? null;
|
|
602
|
+
const parsed = sqlArg !== null ? parseSql(sqlArg, dialect ?? DEFAULT_SQL_DIALECT) : null;
|
|
603
|
+
const sqlOk = parsed && parsed.ok ? parsed : null;
|
|
604
|
+
// DDL statements (CREATE/ALTER/DROP/USE) are schema definition, not a
|
|
605
|
+
// row-level data-access query: they carry no rows to filter and no tenant
|
|
606
|
+
// predicate to check. A variable assignment holding a migration body
|
|
607
|
+
// (`const up = \`CREATE TABLE …\``) must not surface as a data-access call —
|
|
608
|
+
// schema is read separately by the `.sql` visitor and Durable Object DDL.
|
|
609
|
+
// Interpolated DDL still parses to null here, so this guard only excludes
|
|
610
|
+
// *static* schema definitions and leaves the injection surface untouched.
|
|
611
|
+
if (sqlOk && isDdlStatementKind(sqlOk.kind))
|
|
612
|
+
return null;
|
|
193
613
|
const isOrmCall = isOrmPattern(nodeText);
|
|
614
|
+
const tagNames = config.sqlTagNames ?? SQL_TAG_NAMES;
|
|
615
|
+
// A tagged-template SQL call (`sql\`…\`` / `this.sql\`…\``) is a SQL query by
|
|
616
|
+
// tag name, whether its template is static or interpolated. Its static body
|
|
617
|
+
// parses normally; an interpolated body is `cannot-fire` on the SQL facts but
|
|
618
|
+
// is still the injection surface sql-injection-risk (site #11) reads.
|
|
619
|
+
const isTaggedSqlCall = isTaggedTemplateSqlCall(node, adapter, sourceCode, tagNames);
|
|
620
|
+
// A call is a SQL candidate when its static argument parsed as a statement, or
|
|
621
|
+
// it sits in a SQL position — a handle-or-unproven receiver, a query-builder
|
|
622
|
+
// chain, or a tagged template — regardless of whether its argument is static or
|
|
623
|
+
// dynamically interpolated. A parse-failed static argument in a SQL position is
|
|
624
|
+
// `cannot-fire` (facts absent), never silently dropped. (Spec 70 R2 — the
|
|
625
|
+
// keyword name-list gate `containsSQLKeywords` is deleted from this decision: it
|
|
626
|
+
// dropped keyword-less SQL like `PRAGMA`/`VACUUM`/`ANALYZE` that provenance
|
|
627
|
+
// already establishes as SQL, the opposite of honest `cannot-fire`.)
|
|
628
|
+
//
|
|
629
|
+
// Spec 70 R1.2 — handle-ness is decided once, by `identifyHandle`, and the site
|
|
630
|
+
// is admitted on the tri-state verdict: `handle` and `unproven` both admit (the
|
|
631
|
+
// latter carrying its reason, recorded on the call below); only `not-handle`
|
|
632
|
+
// rejects. `null` means "not a query-shaped DB/ORM call", not "not a handle" —
|
|
633
|
+
// a type-annotated `db: D1Database` with a dynamic template argument now stays
|
|
634
|
+
// visible instead of being dropped before the rules can see it.
|
|
635
|
+
const handleVerdict = handleVerdictForCall(node, scan);
|
|
636
|
+
const handleAdmits = handleVerdict !== null && handleVerdict.kind !== 'not-handle';
|
|
637
|
+
const isSqlPosition = handleAdmits || isQueryBuilderShape(node, adapter, sourceCode);
|
|
638
|
+
const isSqlQuery = sqlOk !== null || isSqlPosition || isTaggedSqlCall;
|
|
194
639
|
if (!isSqlQuery && !isOrmCall)
|
|
195
640
|
return null;
|
|
196
|
-
const
|
|
641
|
+
const sqlAstNode = sqlOk ? sqlOk.ast : null;
|
|
642
|
+
const builderVerb = sqlAstNode ? null : builderWriteVerb(node, adapter, sourceCode);
|
|
643
|
+
// Tables: SQL relations from the AST ∪ ORM-shaped references from the node.
|
|
644
|
+
const sqlTables = sqlAstNode ? extractTableNames(sqlAstNode) : [];
|
|
645
|
+
const ormTables = isOrmCall ? extractOrmTables(node, adapter, sourceCode, config) : [];
|
|
646
|
+
const tables = [...new Set([...sqlTables, ...ormTables])];
|
|
647
|
+
// Row-limiting filter: SQL WHERE/HAVING/LIMIT from the AST, or the ORM chain
|
|
648
|
+
// shape when there is no SQL to walk.
|
|
649
|
+
const facts = sqlAstNode ? whereFacts(sqlAstNode) : null;
|
|
650
|
+
const hasFilter = (facts
|
|
651
|
+
? (facts.hasWhere && !facts.whereIsTautology) || facts.hasHaving || facts.hasLimit
|
|
652
|
+
: false)
|
|
653
|
+
|| (isOrmCall && hasOrmFilterShape(node, adapter, sourceCode));
|
|
197
654
|
const hasOrgFilter = hasOrganizationFilter(nodeText, config);
|
|
198
|
-
const hasFilter = hasQueryFilter(nodeText);
|
|
199
655
|
const security = withRuleTiming('sql-injection-risk', () => checkQuerySecurity(node, nodeText, ast, scan));
|
|
200
|
-
|
|
656
|
+
// Spec 70 R4 — a sql-injection finding asserts the receiver is actually a DB
|
|
657
|
+
// handle. `unproven` (resolution reached a declaration whose origin it cannot
|
|
658
|
+
// tie to a database client, e.g. a Worker env binding like `D1Database`) is
|
|
659
|
+
// `cannot-fire`: the site stays visible but no vulnerability is asserted. Only
|
|
660
|
+
// a proven `handle` — a parsed SQL argument (R3) or a package in the manifest
|
|
661
|
+
// (R4) — fires. A type name proves nothing under criterion 9, so there is no
|
|
662
|
+
// type-annotation signal to consult.
|
|
663
|
+
const injectionRisk = security.injectionRisk && handleVerdict?.kind === 'handle';
|
|
664
|
+
const callType = classifyCallType(isSqlQuery, isOrmCall);
|
|
201
665
|
return {
|
|
202
666
|
type: callType,
|
|
203
667
|
method: extractMethodName(node, adapter, sourceCode),
|
|
@@ -208,26 +672,33 @@ function buildDatabaseCall(node, ast, scan) {
|
|
|
208
672
|
queryText: nodeText,
|
|
209
673
|
hasOrganizationFilter: hasOrgFilter,
|
|
210
674
|
hasFilter,
|
|
675
|
+
isWrite: sqlAstNode ? isWriteStatement(sqlAstNode) : builderVerb !== null,
|
|
676
|
+
isMassWrite: sqlAstNode ? isMassWriteStatement(sqlAstNode) : builderVerb === 'update',
|
|
677
|
+
isUpsert: sqlAstNode
|
|
678
|
+
? isUpsertStatement(sqlAstNode) || sqlOk?.conflictClauseTruncated === true
|
|
679
|
+
: false,
|
|
680
|
+
isRawInsert: sqlAstNode ? (sqlAstNode.type === 'insert' || sqlAstNode.type === 'replace') : false,
|
|
681
|
+
insertColumns: sqlAstNode ? rawInsertColumnsFromAst(sqlAstNode) : null,
|
|
682
|
+
sqlWhereColumns: sqlAstNode ? [...whereColumnRefs(sqlAstNode)] : null,
|
|
211
683
|
hasParameterizedQuery: security.parameterized,
|
|
212
|
-
hasSqlInjectionRisk:
|
|
684
|
+
hasSqlInjectionRisk: injectionRisk,
|
|
213
685
|
sqlEscaped: security.escaped,
|
|
214
686
|
enclosingFunction: enclosingIdentity(node, adapter, ast.filePath),
|
|
687
|
+
resolvedWhere: resolveWhereBinding(node, sourceCode, adapter) ?? undefined,
|
|
688
|
+
handleVerdict: handleVerdict ?? undefined,
|
|
215
689
|
};
|
|
216
690
|
}
|
|
217
691
|
/**
|
|
218
|
-
* Classify a SQL/ORM candidate as
|
|
219
|
-
*
|
|
220
|
-
*
|
|
692
|
+
* Classify a SQL/ORM candidate as a broad call-type label: `sql` for a raw SQL
|
|
693
|
+
* query, `orm` for a query-builder-shaped call, `unknown` otherwise. This is a
|
|
694
|
+
* non-decision label (no rule reads it); the receiver's DB provenance — resolved
|
|
695
|
+
* separately — is what decides whether the call is a data-access violation.
|
|
221
696
|
*/
|
|
222
|
-
function classifyCallType(isSqlQuery, isOrmCall
|
|
697
|
+
function classifyCallType(isSqlQuery, isOrmCall) {
|
|
223
698
|
if (isSqlQuery)
|
|
224
699
|
return 'sql';
|
|
225
|
-
if (isOrmCall)
|
|
226
|
-
|
|
227
|
-
if (importInfo.hasImports)
|
|
228
|
-
return dbType;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
700
|
+
if (isOrmCall)
|
|
701
|
+
return 'orm';
|
|
231
702
|
return 'unknown';
|
|
232
703
|
}
|
|
233
704
|
/**
|
|
@@ -246,9 +717,10 @@ function shouldSkipCallForTemplateArg(node, adapter) {
|
|
|
246
717
|
* Extract database calls from AST
|
|
247
718
|
*/
|
|
248
719
|
function extractDatabaseCalls(ast, scan) {
|
|
249
|
-
const { adapter,
|
|
720
|
+
const { adapter, config } = scan;
|
|
721
|
+
const tagNames = config.sqlTagNames ?? SQL_TAG_NAMES;
|
|
250
722
|
const allNodes = adapter.findNodes(ast, {
|
|
251
|
-
custom: (node) => isDbCallCandidate(node,
|
|
723
|
+
custom: (node) => isDbCallCandidate(node, scan, tagNames),
|
|
252
724
|
});
|
|
253
725
|
const uniqueNodes = dedupeCandidateNodes(allNodes, adapter);
|
|
254
726
|
const calls = [];
|
|
@@ -311,7 +783,7 @@ function analyzeQuery(call, config) {
|
|
|
311
783
|
function checkViolations(call, analysis, ctx) {
|
|
312
784
|
const { filePath, config, symbolOrdinals, skipTestRules } = ctx;
|
|
313
785
|
const violations = [];
|
|
314
|
-
const symbol = nextSymbol(call.enclosingFunction ?? 'top-level', call.method, symbolOrdinals);
|
|
786
|
+
const symbol = nextSymbol(call.enclosingFunction ?? 'top-level', call.method ?? '', symbolOrdinals);
|
|
315
787
|
const push = (message, opts) => violations.push(makeViolation(filePath, { line: call.line, column: call.column }, message, { ...opts, symbol }));
|
|
316
788
|
// Security: SQL injection. Raw unescaped interpolation of input is a live
|
|
317
789
|
// vulnerability now → `critical`. Manual quote-escaping
|
|
@@ -327,7 +799,7 @@ function checkViolations(call, analysis, ctx) {
|
|
|
327
799
|
resolution: {
|
|
328
800
|
action: 'parameterize',
|
|
329
801
|
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.`,
|
|
330
|
-
symbols: [call.method],
|
|
802
|
+
symbols: [call.method ?? ''],
|
|
331
803
|
files: [filePath],
|
|
332
804
|
lines: [call.line],
|
|
333
805
|
},
|
|
@@ -340,7 +812,7 @@ function checkViolations(call, analysis, ctx) {
|
|
|
340
812
|
resolution: {
|
|
341
813
|
action: 'parameterize',
|
|
342
814
|
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.`,
|
|
343
|
-
symbols: [call.method],
|
|
815
|
+
symbols: [call.method ?? ''],
|
|
344
816
|
files: [filePath],
|
|
345
817
|
lines: [call.line],
|
|
346
818
|
},
|
|
@@ -406,13 +878,27 @@ function isFunctionCall(node, adapter) {
|
|
|
406
878
|
return node.type === 'call_expression' ||
|
|
407
879
|
node.type === 'new_expression';
|
|
408
880
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
881
|
+
/**
|
|
882
|
+
* The node types that name a variable assignment whose RHS could be a static
|
|
883
|
+
* SQL literal. The previous check compared against `variable_declaration` (TS
|
|
884
|
+
* `var` only) and `binary_expression` (arithmetic/comparison — never an
|
|
885
|
+
* assignment), so `const`/`let`/`:=` declarations and every reassignment were
|
|
886
|
+
* dead. The type is the whole signal; `extractStaticSql` then reads the RHS.
|
|
887
|
+
*/
|
|
888
|
+
const VARIABLE_ASSIGNMENT_TYPES = new Set([
|
|
889
|
+
// TS/JS
|
|
890
|
+
'variable_declaration', // var q = …
|
|
891
|
+
'lexical_declaration', // const/let q = …
|
|
892
|
+
'variable_declarator', // the declarator inside either
|
|
893
|
+
'assignment_expression', // q = …
|
|
894
|
+
// Go
|
|
895
|
+
'short_var_declaration', // q := …
|
|
896
|
+
'var_declaration', // var q = …
|
|
897
|
+
'var_spec', // the spec inside var_declaration
|
|
898
|
+
'assignment_statement', // q = …
|
|
899
|
+
]);
|
|
412
900
|
function isVariableAssignment(node, adapter) {
|
|
413
|
-
|
|
414
|
-
return node.type === 'variable_declaration' ||
|
|
415
|
-
node.type === 'binary_expression' && (node.children?.some(child => adapter.getNodeText(child, '').includes('=')) ?? false);
|
|
901
|
+
return VARIABLE_ASSIGNMENT_TYPES.has(adapter.getNodeType(node));
|
|
416
902
|
}
|
|
417
903
|
function containsSQLKeywords(text) {
|
|
418
904
|
const upperText = text.toUpperCase();
|
|
@@ -475,54 +961,6 @@ function stripComments(text) {
|
|
|
475
961
|
}
|
|
476
962
|
return out;
|
|
477
963
|
}
|
|
478
|
-
/**
|
|
479
|
-
* Spec 22 R4.2: Requires ≥2 SQL keywords for variable-assignment detection.
|
|
480
|
-
*
|
|
481
|
-
* Single-keyword substring matches (e.g. "FROM" inside "Array.from") produce
|
|
482
|
-
* ~120 false positives on the recall corpus. Genuine SQL in variable
|
|
483
|
-
* assignments (string literals, ORM chains) almost always has ≥2 keywords
|
|
484
|
-
* (SELECT+FROM, INSERT+INTO, DELETE+FROM, etc.).
|
|
485
|
-
*
|
|
486
|
-
* This is only used for the variable-assignment fallback path — template
|
|
487
|
-
* literals and function calls use separate, context-aware gating.
|
|
488
|
-
*/
|
|
489
|
-
function containsSQLStructure(text) {
|
|
490
|
-
const upperText = text.toUpperCase();
|
|
491
|
-
const found = SQL_KEYWORDS.filter(keyword => upperText.includes(keyword));
|
|
492
|
-
return found.length >= 2;
|
|
493
|
-
}
|
|
494
|
-
/**
|
|
495
|
-
* Spec 17 R2 provenance gate: a template literal is a SQL candidate
|
|
496
|
-
* because of where it sits (inside a DB-provenanced call's arguments),
|
|
497
|
-
* NOT because its body contains SQL-shaped substrings.
|
|
498
|
-
*
|
|
499
|
-
* Content scanning with substring matching is removed — template bodies
|
|
500
|
-
* containing natural-language words like "from" or "select" are no longer
|
|
501
|
-
* misclassified. The cost: template literals assigned to variables whose
|
|
502
|
-
* values eventually flow to DB calls are not detected (requires dataflow
|
|
503
|
-
* analysis, which is outside the product's stated scope per Spec 15 R3).
|
|
504
|
-
*/
|
|
505
|
-
function isTemplateInDBProvenancedCall(node, adapter, sourceCode, provenanceContext) {
|
|
506
|
-
const parent = adapter.getParent(node);
|
|
507
|
-
if (!parent)
|
|
508
|
-
return false;
|
|
509
|
-
const parentType = adapter.getNodeType(parent);
|
|
510
|
-
// Only the arguments-of-DB-call path survives the Spec 17 R2 cut.
|
|
511
|
-
// Variable-assignment and statement-level conservative passes are
|
|
512
|
-
// removed — they were the primary source of false positives.
|
|
513
|
-
if (parentType === 'arguments') {
|
|
514
|
-
const callExpr = adapter.getParent(parent);
|
|
515
|
-
if (!callExpr || adapter.getNodeType(callExpr) !== 'call_expression')
|
|
516
|
-
return false;
|
|
517
|
-
if (provenanceContext) {
|
|
518
|
-
return isDBProvenanced(callExpr, { adapter, sourceCode, context: provenanceContext, methods: DB_CALL_METHODS });
|
|
519
|
-
}
|
|
520
|
-
// Without provenance context, can't determine DB association —
|
|
521
|
-
// do not speculate.
|
|
522
|
-
return false;
|
|
523
|
-
}
|
|
524
|
-
return false;
|
|
525
|
-
}
|
|
526
964
|
/**
|
|
527
965
|
* Detect that a node sits inside a D1 .prepare() call — the standard safe
|
|
528
966
|
* pattern for SQL in Cloudflare Workers.
|
|
@@ -561,11 +999,14 @@ function isInPrepareBindChain(node, adapter, sourceCode) {
|
|
|
561
999
|
function findCallFromEntry(node, adapter) {
|
|
562
1000
|
const type = adapter.getNodeType(node);
|
|
563
1001
|
if (type === 'template_string') {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
1002
|
+
// A nested template (`${cond ? \` WHERE x = ?\` : ''}`) is a fragment of its
|
|
1003
|
+
// enclosing query, not a query in its own right. Resolve it through the
|
|
1004
|
+
// substitution/outer-template chain to the enclosing call (as
|
|
1005
|
+
// `enclosingCallOf` does) so it inherits that call's parameterization —
|
|
1006
|
+
// `.query(sql, params)` / `.prepare().bind()` — instead of being re-read as
|
|
1007
|
+
// a bare interpolation whose direct parent is `template_substitution` (not
|
|
1008
|
+
// `arguments`) and therefore wrongly reported as raw injection ("in unknown").
|
|
1009
|
+
return enclosingCallOf(node, adapter);
|
|
569
1010
|
}
|
|
570
1011
|
if (type === 'call_expression')
|
|
571
1012
|
return node;
|
|
@@ -597,15 +1038,23 @@ function memberPropertyName(node, adapter, sourceCode) {
|
|
|
597
1038
|
* "all" property for the eager D1 read method (Spec 52 R1). `Promise` is the
|
|
598
1039
|
* only collision in the eager set — none of run/first/raw/exec/batch are
|
|
599
1040
|
* Promise methods.
|
|
1041
|
+
*
|
|
1042
|
+
* @param memberExpr the member-expression node to classify
|
|
1043
|
+
* @param adapter the language adapter used to read the member property
|
|
1044
|
+
* @param sourceCode the file source text for reading node text
|
|
1045
|
+
* @returns true when the member is the global `Promise.all`
|
|
600
1046
|
*/
|
|
601
|
-
function isPromiseAllMember(memberExpr, adapter, sourceCode) {
|
|
1047
|
+
export function isPromiseAllMember(memberExpr, adapter, sourceCode) {
|
|
602
1048
|
if (memberPropertyName(memberExpr, adapter, sourceCode) !== 'all')
|
|
603
1049
|
return false;
|
|
604
1050
|
const objectNode = adapter.getChildren(memberExpr).find(c => adapter.getNodeType(c) !== 'property_identifier');
|
|
605
1051
|
if (!objectNode)
|
|
606
1052
|
return false;
|
|
607
|
-
|
|
608
|
-
|
|
1053
|
+
// The object must be the bare `Promise` global identifier. `foo.Promise` is a
|
|
1054
|
+
// different binding, so a `.Promise` text suffix must not read as `Promise.all`.
|
|
1055
|
+
if (adapter.getNodeType(objectNode) !== 'identifier')
|
|
1056
|
+
return false;
|
|
1057
|
+
return adapter.getNodeText(objectNode, sourceCode) === 'Promise';
|
|
609
1058
|
}
|
|
610
1059
|
/**
|
|
611
1060
|
* Resolve a db-call node to its call_expression. A template_string db node (the
|
|
@@ -643,8 +1092,16 @@ function dbCallMethodName(node, adapter, sourceCode) {
|
|
|
643
1092
|
* DB methods that execute I/O immediately (as opposed to `.prepare()`/`.bind()`,
|
|
644
1093
|
* which only construct a statement object). Used to decide whether a
|
|
645
1094
|
* prepare/bind call is chained into an eager execution.
|
|
1095
|
+
*
|
|
1096
|
+
* `.get()` is the single-row read shared by better-sqlite3, `node:sqlite`, and
|
|
1097
|
+
* `bun:sqlite` that D1 does not expose (D1's closest is `.first()`). It was
|
|
1098
|
+
* absent, so `db.prepare(…).get(…)` in a loop was misread as statement
|
|
1099
|
+
* construction and skipped — the exact false negative the corpus pins at
|
|
1100
|
+
* `n-plus-one.ts:51`/`:62`. The wider driver-method gap (`.iterate()`, bun's
|
|
1101
|
+
* `.values()`) is documented in specs/rule-evidence-corpus/REPORT.md; it is
|
|
1102
|
+
* deliberately left out until it is measured, not silently dropped.
|
|
646
1103
|
*/
|
|
647
|
-
const EAGER_DB_METHODS = new Set(['run', 'all', 'first', 'raw', 'exec', 'batch', 'query']);
|
|
1104
|
+
const EAGER_DB_METHODS = new Set(['run', 'all', 'first', 'raw', 'exec', 'batch', 'query', 'get']);
|
|
648
1105
|
/**
|
|
649
1106
|
* True when `node` (a `.prepare()`/`.bind()` call) is chained into an eager
|
|
650
1107
|
* method — e.g. `db.prepare(sql).bind(x).run()`. The `.run()` at the end of the
|
|
@@ -681,8 +1138,8 @@ function hasEagerMethodInCallChain(node, adapter, sourceCode) {
|
|
|
681
1138
|
/**
|
|
682
1139
|
* True when a db-call node is statement construction only — a `.prepare()` /
|
|
683
1140
|
* `.bind()` that is not chained into an eager method (`.run()` / `.all()` /
|
|
684
|
-
* `.first()` / `.raw()` / `.exec()` / `.batch()`). Such a call
|
|
685
|
-
* so it is not a query-in-loop on its own (Spec 52 R1).
|
|
1141
|
+
* `.first()` / `.raw()` / `.exec()` / `.batch()` / `.get()`). Such a call
|
|
1142
|
+
* performs no I/O, so it is not a query-in-loop on its own (Spec 52 R1).
|
|
686
1143
|
*/
|
|
687
1144
|
function isStatementConstructionOnly(node, adapter, sourceCode) {
|
|
688
1145
|
const method = dbCallMethodName(node, adapter, sourceCode);
|
|
@@ -703,7 +1160,7 @@ function isSqlStringConstruction(node, adapter, sourceCode) {
|
|
|
703
1160
|
const method = dbCallMethodName(node, adapter, sourceCode);
|
|
704
1161
|
if (!method || EAGER_DB_METHODS.has(method))
|
|
705
1162
|
return false;
|
|
706
|
-
if (!
|
|
1163
|
+
if (!isOrmMethod(method))
|
|
707
1164
|
return false;
|
|
708
1165
|
const call = resolveDbCallNode(node, adapter);
|
|
709
1166
|
if (!call)
|
|
@@ -795,15 +1252,25 @@ function isInExecChain(node, adapter, sourceCode) {
|
|
|
795
1252
|
return hasSpreadArgument(execCall, adapter);
|
|
796
1253
|
}
|
|
797
1254
|
/**
|
|
798
|
-
* Detect
|
|
799
|
-
*
|
|
1255
|
+
* Detect eager execution methods called with bind parameters as a second
|
|
1256
|
+
* argument — D1's `.all()`/`.first()`/`.run()`, plus the parameterized
|
|
1257
|
+
* `.query()`/`.execute()` form shared by node-postgres/mysql2 and the tool's
|
|
1258
|
+
* own `IndexHandle`.
|
|
1259
|
+
*
|
|
1260
|
+
* `db.run(query, ...params)` is shorthand for
|
|
1261
|
+
* `db.prepare(query).bind(...params).run()`. If there's a second argument
|
|
1262
|
+
* (the bind params), the call is fully parameterized and safe — the `${}`
|
|
1263
|
+
* interpolations in the SQL are structural (placeholder fragments, identifiers,
|
|
1264
|
+
* fixed value-lists), never bound values.
|
|
800
1265
|
*
|
|
801
|
-
* `
|
|
802
|
-
* `
|
|
803
|
-
* (
|
|
1266
|
+
* `IndexHandle.query(sql, params)` (the tool's own parameterized index-query
|
|
1267
|
+
* abstraction, `src/types.ts`) is this exact contract: the interpolations carry
|
|
1268
|
+
* `?`-placeholder clauses (`${fp.clause}`, `${inList(types)}`) and `params`
|
|
1269
|
+
* binds the values out-of-band. A single-arg `.query(\`…${input}\`)` — the raw
|
|
1270
|
+
* injection shape — stays flagged because it carries no bind argument.
|
|
804
1271
|
*/
|
|
805
1272
|
function isD1ConvenienceCall(node, adapter, sourceCode) {
|
|
806
|
-
const D1_CONVENIENCE = new Set(['all', 'first', 'run']);
|
|
1273
|
+
const D1_CONVENIENCE = new Set(['all', 'first', 'run', 'query', 'execute']);
|
|
807
1274
|
const call = findCallFromEntry(node, adapter);
|
|
808
1275
|
if (!call)
|
|
809
1276
|
return false;
|
|
@@ -866,8 +1333,13 @@ function isWrapperFunctionWithBindParams(node, adapter, sourceCode, wrapperNames
|
|
|
866
1333
|
* single-expression form `db.prepare(sql).bind(x).all()`. This method
|
|
867
1334
|
* catches the common idiom where the prepared statement is stored in a local
|
|
868
1335
|
* before being bound.
|
|
1336
|
+
*
|
|
1337
|
+
* @param prepareCall the `db.prepare(...)` call-expression node
|
|
1338
|
+
* @param adapter the language adapter used to read the parent assignment
|
|
1339
|
+
* @param sourceCode the file source text for reading node text
|
|
1340
|
+
* @returns true when the prepare result is stored then `.bind()`ed in scope
|
|
869
1341
|
*/
|
|
870
|
-
function isPrepareAssignedToVariable(prepareCall, adapter, sourceCode) {
|
|
1342
|
+
export function isPrepareAssignedToVariable(prepareCall, adapter, sourceCode) {
|
|
871
1343
|
// The parent of the prepare call_expression reveals how the result is used.
|
|
872
1344
|
const parent = adapter.getParent(prepareCall);
|
|
873
1345
|
if (!parent)
|
|
@@ -893,14 +1365,30 @@ function isPrepareAssignedToVariable(prepareCall, adapter, sourceCode) {
|
|
|
893
1365
|
}
|
|
894
1366
|
if (!varName)
|
|
895
1367
|
return false;
|
|
896
|
-
// Scan the enclosing function scope for
|
|
1368
|
+
// Scan the enclosing function scope for a `<varName>.bind(…)` member call.
|
|
1369
|
+
// Walk the subtree structurally: a `.bind(` inside a comment or string is text,
|
|
1370
|
+
// not a member expression, so it is not seen (the old regex over the function
|
|
1371
|
+
// text matched `// stmt.bind(x)` comments).
|
|
897
1372
|
const fnNode = findEnclosingFunctionNode(prepareCall, adapter);
|
|
898
1373
|
if (!fnNode)
|
|
899
1374
|
return false;
|
|
900
|
-
const
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1375
|
+
const stack = [fnNode];
|
|
1376
|
+
while (stack.length) {
|
|
1377
|
+
const n = stack.pop();
|
|
1378
|
+
const t = adapter.getNodeType(n);
|
|
1379
|
+
if (t === 'member_expression' || t === 'selector_expression') {
|
|
1380
|
+
if (extractMemberExpressionProperty(n, adapter, sourceCode) === 'bind') {
|
|
1381
|
+
const receiver = memberObjectNode(n, adapter);
|
|
1382
|
+
if (receiver && adapter.getNodeType(receiver) === 'identifier'
|
|
1383
|
+
&& adapter.getNodeText(receiver, sourceCode) === varName) {
|
|
1384
|
+
return true;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
for (const c of adapter.getChildren(n) ?? [])
|
|
1389
|
+
stack.push(c);
|
|
1390
|
+
}
|
|
1391
|
+
return false;
|
|
904
1392
|
}
|
|
905
1393
|
/**
|
|
906
1394
|
* Walk up the AST to find the enclosing function node (function_declaration,
|
|
@@ -968,7 +1456,17 @@ function isOrmPattern(text) {
|
|
|
968
1456
|
/\.createMany\s*\(/,
|
|
969
1457
|
/\.aggregate\s*\(/,
|
|
970
1458
|
/\.count\s*\(/,
|
|
971
|
-
/\.distinct\s*\(
|
|
1459
|
+
/\.distinct\s*\(/,
|
|
1460
|
+
// Kysely builder verbs (camelCase SQL — not matched by the SQL-keyword path).
|
|
1461
|
+
/\.selectFrom\s*\(/,
|
|
1462
|
+
/\.selectAll\s*\(/,
|
|
1463
|
+
/\.insertInto\s*\(/,
|
|
1464
|
+
/\.updateTable\s*\(/,
|
|
1465
|
+
/\.deleteFrom\s*\(/,
|
|
1466
|
+
/\.executeTakeFirst\s*\(/,
|
|
1467
|
+
/\.executeTakeFirstOrThrow\s*\(/,
|
|
1468
|
+
/\.onConflict\s*\(/,
|
|
1469
|
+
/\.returning\s*\(/
|
|
972
1470
|
];
|
|
973
1471
|
return ormPatterns.some(pattern => pattern.test(scrubNonTableCalls(text)));
|
|
974
1472
|
}
|
|
@@ -982,152 +1480,158 @@ function collectPatternTables(text, patterns, tables) {
|
|
|
982
1480
|
}
|
|
983
1481
|
});
|
|
984
1482
|
}
|
|
1483
|
+
/** The immediate object (receiver) child of a member/selector expression —
|
|
1484
|
+
* the child that is neither `.` nor the property identifier. */
|
|
1485
|
+
function memberObjectNode(node, adapter) {
|
|
1486
|
+
const children = adapter.getChildren(node);
|
|
1487
|
+
return children.find((c) => {
|
|
1488
|
+
const ct = adapter.getNodeType(c);
|
|
1489
|
+
return ct !== '.' && ct !== 'property_identifier' && ct !== 'field_identifier';
|
|
1490
|
+
}) ?? null;
|
|
1491
|
+
}
|
|
1492
|
+
/** The first real (non-punctuation) argument node of an `arguments` node. */
|
|
1493
|
+
function firstArgumentNode(argsNode, adapter) {
|
|
1494
|
+
for (const c of adapter.getChildren(argsNode)) {
|
|
1495
|
+
const ct = adapter.getNodeType(c);
|
|
1496
|
+
if (ct === '(' || ct === ')' || ct === ',')
|
|
1497
|
+
continue;
|
|
1498
|
+
return c;
|
|
1499
|
+
}
|
|
1500
|
+
return null;
|
|
1501
|
+
}
|
|
1502
|
+
/** The unquoted body of a string/template literal node, or null. */
|
|
1503
|
+
function stringLiteralValue(node, adapter, sourceCode) {
|
|
1504
|
+
const t = adapter.getNodeType(node);
|
|
1505
|
+
if (t === 'template_string') {
|
|
1506
|
+
const children = adapter.getChildren(node) ?? [];
|
|
1507
|
+
if (children.some((c) => adapter.getNodeType(c) === 'template_substitution'))
|
|
1508
|
+
return null;
|
|
1509
|
+
}
|
|
1510
|
+
if (t === 'string' || t === 'template_string' ||
|
|
1511
|
+
t === 'interpreted_string_literal' || t === 'raw_string_literal') {
|
|
1512
|
+
const raw = adapter.getNodeText(node, sourceCode);
|
|
1513
|
+
if (raw == null)
|
|
1514
|
+
return null;
|
|
1515
|
+
return stripSqlQuotes(raw, { raw: t === 'raw_string_literal' });
|
|
1516
|
+
}
|
|
1517
|
+
return null;
|
|
1518
|
+
}
|
|
1519
|
+
/** The table name an ORM table-taking verb's first argument carries: a bare
|
|
1520
|
+
* identifier's name or an unquoted string literal. Null for anything else (a
|
|
1521
|
+
* dynamic expression, a template with substitutions, a number, …). */
|
|
1522
|
+
function ormTableNameFromArgument(node, adapter, sourceCode) {
|
|
1523
|
+
if (adapter.getNodeType(node) === 'identifier') {
|
|
1524
|
+
const text = adapter.getNodeText(node, sourceCode);
|
|
1525
|
+
return text ? text : null;
|
|
1526
|
+
}
|
|
1527
|
+
const literal = stringLiteralValue(node, adapter, sourceCode);
|
|
1528
|
+
return literal && literal.length > 0 ? literal : null;
|
|
1529
|
+
}
|
|
1530
|
+
/** Kysely builder verbs whose first argument is the table name. */
|
|
1531
|
+
const ORM_TABLE_VERBS = new Set(['selectFrom', 'deleteFrom', 'insertInto', 'updateTable']);
|
|
1532
|
+
/** ORM join verbs whose first argument is the joined table. */
|
|
1533
|
+
const ORM_JOIN_VERBS = new Set(['join', 'leftJoin', 'rightJoin', 'innerJoin']);
|
|
985
1534
|
/**
|
|
986
|
-
*
|
|
987
|
-
*
|
|
988
|
-
*
|
|
1535
|
+
* Walk the callee chain of an ORM candidate, reading table references from the
|
|
1536
|
+
* *structure* (member names + argument nodes) rather than the source text. A
|
|
1537
|
+
* `.from(…)` / verb / `db.<table>.<method>()` shape inside a string argument is
|
|
1538
|
+
* not a chain member, so it is never read as a table.
|
|
1539
|
+
*
|
|
1540
|
+
* Three shapes are read:
|
|
1541
|
+
* - `.from(table)` / `.from('table')` (except JS `Array.from`/`Buffer.from`/
|
|
1542
|
+
* `String.from`/`*Array.from` construction),
|
|
1543
|
+
* - a Kysely/join verb carrying the table as its first argument,
|
|
1544
|
+
* - Drizzle's `db.<table>.<method>()` table-access idiom.
|
|
989
1545
|
*/
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1546
|
+
function collectOrmTableReferences(node, adapter, sourceCode, tables) {
|
|
1547
|
+
const t = adapter.getNodeType(node);
|
|
1548
|
+
if (t === 'call_expression') {
|
|
1549
|
+
const callee = getCallExpressionCallee(node, adapter);
|
|
1550
|
+
if (callee) {
|
|
1551
|
+
const calleeType = adapter.getNodeType(callee);
|
|
1552
|
+
if (calleeType === 'member_expression' || calleeType === 'selector_expression') {
|
|
1553
|
+
const method = extractMemberExpressionProperty(callee, adapter, sourceCode);
|
|
1554
|
+
const receiverNode = memberObjectNode(callee, adapter);
|
|
1555
|
+
const receiverName = receiverNode && adapter.getNodeType(receiverNode) === 'identifier'
|
|
1556
|
+
? adapter.getNodeText(receiverNode, sourceCode)
|
|
1557
|
+
: null;
|
|
1558
|
+
const args = adapter.getChildren(node).find((c) => adapter.getNodeType(c) === 'arguments');
|
|
1559
|
+
const firstArg = args ? firstArgumentNode(args, adapter) : null;
|
|
1560
|
+
if (method && firstArg) {
|
|
1561
|
+
const isFrom = method === 'from'
|
|
1562
|
+
&& !(receiverName && (receiverName === 'Buffer' || receiverName === 'String'
|
|
1563
|
+
|| receiverName.endsWith('Array')));
|
|
1564
|
+
const isVerb = ORM_TABLE_VERBS.has(method);
|
|
1565
|
+
const isJoin = ORM_JOIN_VERBS.has(method) && receiverName !== 'sql';
|
|
1566
|
+
if (isFrom || isVerb || isJoin) {
|
|
1567
|
+
const name = ormTableNameFromArgument(firstArg, adapter, sourceCode);
|
|
1568
|
+
if (name)
|
|
1569
|
+
tables.add(name);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
collectOrmTableReferences(callee, adapter, sourceCode, tables);
|
|
1006
1574
|
}
|
|
1007
1575
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1576
|
+
else if (t === 'member_expression' || t === 'selector_expression') {
|
|
1577
|
+
// db.<table>.<method>() — Drizzle's table-access idiom. The receiver is the
|
|
1578
|
+
// member `db.<table>`, whose property names the table; the root must be the
|
|
1579
|
+
// bare `db` identifier (not `appDb`/`foo`), mirroring the old `db\.…` regex.
|
|
1580
|
+
const object = memberObjectNode(node, adapter);
|
|
1581
|
+
if (object && (adapter.getNodeType(object) === 'member_expression'
|
|
1582
|
+
|| adapter.getNodeType(object) === 'selector_expression')) {
|
|
1583
|
+
const innerProp = extractMemberExpressionProperty(object, adapter, sourceCode);
|
|
1584
|
+
const innerObject = memberObjectNode(object, adapter);
|
|
1585
|
+
if (innerProp && innerObject && adapter.getNodeType(innerObject) === 'identifier'
|
|
1586
|
+
&& adapter.getNodeText(innerObject, sourceCode) === 'db') {
|
|
1587
|
+
tables.add(innerProp);
|
|
1588
|
+
}
|
|
1014
1589
|
}
|
|
1590
|
+
if (object)
|
|
1591
|
+
collectOrmTableReferences(object, adapter, sourceCode, tables);
|
|
1015
1592
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
//
|
|
1042
|
-
const
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
* True when a query applies a *row-limiting* filter: a WHERE carrying a real
|
|
1047
|
-
* predicate, a HAVING, or a LIMIT. The `unfiltered-query` rule flags a write
|
|
1048
|
-
* (DELETE/UPDATE) that lacks such a clause — a mass mutation that touches every
|
|
1049
|
-
* row. A `JOIN ... ON` predicate scopes *how* rows match, not *which* rows come
|
|
1050
|
-
* back, so it is not a filter; and a tautological `WHERE 1=1` (the placeholder
|
|
1051
|
-
* prepended so callers can append `AND x = ?`) limits nothing, so it is not a
|
|
1052
|
-
* filter either. Evaluated on comment-stripped text so prose in `//` or `/* *`/
|
|
1053
|
-
* comments cannot fabricate a filter.
|
|
1054
|
-
*/
|
|
1055
|
-
function hasQueryFilter(text) {
|
|
1056
|
-
const upper = text.toUpperCase();
|
|
1057
|
-
return (/\bWHERE\b/.test(upper) && !whereClauseIsTautology(text))
|
|
1058
|
-
|| /\bHAVING\b/.test(upper)
|
|
1059
|
-
|| /\bLIMIT\b/.test(upper);
|
|
1060
|
-
}
|
|
1061
|
-
/**
|
|
1062
|
-
* True when the WHERE clause is a bare tautology — `WHERE 1=1` (or `1 = 1`,
|
|
1063
|
-
* `TRUE`, or the same repeated under `AND`) — that limits nothing. A WHERE
|
|
1064
|
-
* body carrying any real predicate (`WHERE id = ?`, or `WHERE 1=1 AND
|
|
1065
|
-
* active = ?`) is not a tautology.
|
|
1066
|
-
*/
|
|
1067
|
-
function whereClauseIsTautology(text) {
|
|
1068
|
-
const upper = text.toUpperCase();
|
|
1069
|
-
const whereMatch = /\bWHERE\b/.exec(upper);
|
|
1070
|
-
if (!whereMatch)
|
|
1071
|
-
return false;
|
|
1072
|
-
const afterWhere = upper.slice(whereMatch.index + 'WHERE'.length);
|
|
1073
|
-
// Body up to the next row-limiting clause keyword.
|
|
1074
|
-
const body = afterWhere.split(/\b(?:GROUP\s+BY|ORDER\s+BY|HAVING|LIMIT|OFFSET|UNION)\b/)[0];
|
|
1075
|
-
// Drop the SQL-string terminator + call-argument closer (`"), "); …) so the
|
|
1076
|
-
// predicate is judged on the clause text alone.
|
|
1077
|
-
const stripped = body.replace(/["'`)\s;]+$/g, '').replace(/^\s+/, '').replace(/\s+$/, '');
|
|
1078
|
-
// A dangling AND/OR left by a dynamic builder still filters nothing.
|
|
1079
|
-
const clean = stripped.replace(/^(?:AND|OR)\s+/i, '').replace(/\s+(?:AND|OR)$/i, '');
|
|
1080
|
-
const alwaysTrue = '(?:1\\s*=\\s*1|TRUE)';
|
|
1081
|
-
return new RegExp(`^${alwaysTrue}(?:\\s+AND\\s+${alwaysTrue})*$`, 'i').test(clean);
|
|
1082
|
-
}
|
|
1083
|
-
/**
|
|
1084
|
-
* True when a SQL statement carries a write verb (INSERT/DELETE/UPDATE/REPLACE).
|
|
1085
|
-
* Spec 52 R2: `REPLACE INTO` is an upsert write — matched as a two-word clause
|
|
1086
|
-
* (not a bare `REPLACE` word) so the `REPLACE()` string function is not misread
|
|
1087
|
-
* as a write.
|
|
1088
|
-
*/
|
|
1089
|
-
export function hasWriteVerb(text) {
|
|
1090
|
-
const upper = text.toUpperCase();
|
|
1091
|
-
return /\bINSERT\b/.test(upper) || /\bDELETE\b/.test(upper) || /\bUPDATE\b/.test(upper)
|
|
1092
|
-
|| /\bREPLACE\s+INTO\b/.test(upper);
|
|
1093
|
-
}
|
|
1094
|
-
/**
|
|
1095
|
-
* True when a statement mutates or deletes existing rows — `DELETE` or `UPDATE`.
|
|
1096
|
-
* These are the verbs whose mass effect is a foot-gun when unconstrained;
|
|
1097
|
-
* `INSERT` / `REPLACE INTO` always target specific rows and are not "unfiltered"
|
|
1098
|
-
* in the dangerous sense.
|
|
1099
|
-
*/
|
|
1100
|
-
function hasMassWriteVerb(text) {
|
|
1101
|
-
const upper = text.toUpperCase();
|
|
1102
|
-
return /\bDELETE\b/.test(upper) || /\bUPDATE\b/.test(upper);
|
|
1103
|
-
}
|
|
1104
|
-
/**
|
|
1105
|
-
* True when a statement is an upsert — one of the four write spellings Spec 52
|
|
1106
|
-
* R2 taught the write classifier, plus MySQL's `ON DUPLICATE KEY UPDATE`. An
|
|
1107
|
-
* upsert is keyed by construction: its conflict target / unique key is what
|
|
1108
|
-
* scopes the row, and an `INSERT` has no `WHERE` by definition. So an upsert is
|
|
1109
|
-
* never an "unfiltered" mass write, even though `INSERT … ON CONFLICT … DO UPDATE`
|
|
1110
|
-
* carries the word `UPDATE` (in `DO UPDATE`) with no WHERE clause.
|
|
1111
|
-
*/
|
|
1112
|
-
function isUpsertForm(text) {
|
|
1113
|
-
const upper = text.toUpperCase();
|
|
1114
|
-
return /\bINSERT\s+OR\s+(?:IGNORE|REPLACE)\b/.test(upper)
|
|
1115
|
-
|| /\bREPLACE\s+INTO\b/.test(upper)
|
|
1116
|
-
|| /\bON\s+CONFLICT\b/.test(upper)
|
|
1117
|
-
|| /\bON\s+DUPLICATE\s+KEY\b/.test(upper);
|
|
1593
|
+
else if (t === 'subscript_expression') {
|
|
1594
|
+
const object = memberObjectNode(node, adapter);
|
|
1595
|
+
if (object)
|
|
1596
|
+
collectOrmTableReferences(object, adapter, sourceCode, tables);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Extract the set of table names referenced by the ORM chain shapes — read
|
|
1601
|
+
* structurally from the callee chain ({@link collectOrmTableReferences}) plus
|
|
1602
|
+
* the user-configured `orm` table patterns. This is the host-language shape
|
|
1603
|
+
* detector only; SQL relations are derived structurally by sqlAst's
|
|
1604
|
+
* `extractTableNames`, not here (Spec 70 R2 site #1). The default `orm` patterns
|
|
1605
|
+
* carry only the non-structural `table: 'users'` / `table = "users"` shape; the
|
|
1606
|
+
* `.from(x)` / join / Kysely-verb shapes that used to live there as regexes are
|
|
1607
|
+
* now read structurally, so a `.from(…)` inside a string argument is not a table.
|
|
1608
|
+
* @param node The ORM/query-builder candidate node.
|
|
1609
|
+
* @param adapter The language adapter (drives node traversal).
|
|
1610
|
+
* @param sourceCode The file source text.
|
|
1611
|
+
* @param config The data-access analyzer config holding table patterns.
|
|
1612
|
+
* @returns The set of table names referenced by ORM shapes.
|
|
1613
|
+
*/
|
|
1614
|
+
export function extractOrmTables(node, adapter, sourceCode, config) {
|
|
1615
|
+
const tables = new Set();
|
|
1616
|
+
collectOrmTableReferences(node, adapter, sourceCode, tables);
|
|
1617
|
+
// User-configured `orm` patterns are arbitrary text shapes with no node to
|
|
1618
|
+
// read (e.g. `table: 'users'`), so they still run over the source text.
|
|
1619
|
+
const nodeText = stripComments(adapter.getNodeText(node, sourceCode));
|
|
1620
|
+
const scrubbed = scrubNonTableCalls(nodeText);
|
|
1621
|
+
collectPatternTables(scrubbed, config.tablePatterns?.orm, tables);
|
|
1622
|
+
return Array.from(tables);
|
|
1118
1623
|
}
|
|
1119
1624
|
/**
|
|
1120
1625
|
* True when a call should be surfaced by the `unfiltered-query` rule (Spec 55
|
|
1121
|
-
* R5, Spec 56 R1): a write statement (`
|
|
1122
|
-
* clause
|
|
1123
|
-
* `
|
|
1124
|
-
*
|
|
1125
|
-
*
|
|
1126
|
-
* spellings) is keyed by construction, so it never fires.
|
|
1626
|
+
* R5, Spec 56 R1): a mass-write statement (`UPDATE … SET`) with no row-limiting
|
|
1627
|
+
* clause. The write/upsert facts are AST-derived (Spec 70 R2 sites #2/#3): a
|
|
1628
|
+
* Kysely `updateTable` builder verb is folded into `isWrite`/`isMassWrite` by
|
|
1629
|
+
* the producer (host-language shape, not SQL). `DELETE FROM t` is not a mass
|
|
1630
|
+
* write (disposition (a)); an upsert is keyed by construction, so it never fires.
|
|
1127
1631
|
*/
|
|
1128
1632
|
function isUnfilteredWrite(call) {
|
|
1129
|
-
return !
|
|
1130
|
-
&&
|
|
1633
|
+
return !call.isUpsert
|
|
1634
|
+
&& call.isMassWrite
|
|
1131
1635
|
&& !call.hasFilter;
|
|
1132
1636
|
}
|
|
1133
1637
|
/**
|
|
@@ -1136,14 +1640,12 @@ function isUnfilteredWrite(call) {
|
|
|
1136
1640
|
* table that carries declared tenancy. A filterless full-table read of a
|
|
1137
1641
|
* tenant table is the same tenant-leak surface the write rule guards — it
|
|
1138
1642
|
* sweeps every tenant's rows. Non-tenant tables (lookups, config) stay out of
|
|
1139
|
-
* scope
|
|
1140
|
-
*
|
|
1141
|
-
* the tenancy determination so the write case and this read case cannot drift
|
|
1142
|
-
* to two different tenancy definitions.
|
|
1643
|
+
* scope. Reuses `requiresOrgFilter` for the tenancy determination so the write
|
|
1644
|
+
* case and this read case cannot drift to two different tenancy definitions.
|
|
1143
1645
|
*/
|
|
1144
1646
|
function isUnfilteredRead(call, config) {
|
|
1145
1647
|
return !call.hasFilter
|
|
1146
|
-
&& !
|
|
1648
|
+
&& !call.isWrite
|
|
1147
1649
|
&& requiresOrgFilter(call.tables, config);
|
|
1148
1650
|
}
|
|
1149
1651
|
/**
|
|
@@ -1186,15 +1688,28 @@ function effectiveWrapperNames(scan) {
|
|
|
1186
1688
|
* config-driven sanitizer allowlist, the adapter's cross-function safety
|
|
1187
1689
|
* analysis, or a static-constant resolution for bare identifiers. Otherwise
|
|
1188
1690
|
* the part counts as unresolved (a candidate injection).
|
|
1691
|
+
*
|
|
1692
|
+
* @param part the dynamic interpolation part to classify
|
|
1693
|
+
* @param ast the file's parsed AST
|
|
1694
|
+
* @param scan the data-access scan context (adapter, source, config)
|
|
1695
|
+
* @returns true when the part is provably safe, false when unresolved
|
|
1189
1696
|
*/
|
|
1190
|
-
function isSafeDynamicPart(part, ast, scan) {
|
|
1697
|
+
export function isSafeDynamicPart(part, ast, scan) {
|
|
1191
1698
|
const { adapter, sourceCode, config } = scan;
|
|
1192
1699
|
// Config-driven sanitizer allowlist (escapeSql(x), …) applies to
|
|
1193
1700
|
// non-identifier expressions — an interpolation wrapped in a known
|
|
1194
1701
|
// sanitizer isn't raw (same pattern as dbWrapperNames for provenance).
|
|
1702
|
+
// The sanitizer name is read from the part's call node, not from a text
|
|
1703
|
+
// prefix: `mySanitizer (x)` (two spaces) still names `mySanitizer`, but
|
|
1704
|
+
// `startsWith(name + '(')` would not see it through the extra space.
|
|
1195
1705
|
if (!part.isIdentifier) {
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1706
|
+
const callee = part.node && adapter.getNodeType(part.node) === 'call_expression'
|
|
1707
|
+
? getCallExpressionCallee(part.node, adapter)
|
|
1708
|
+
: null;
|
|
1709
|
+
const calleeName = callee && adapter.getNodeType(callee) === 'identifier'
|
|
1710
|
+
? adapter.getNodeText(callee, sourceCode)
|
|
1711
|
+
: null;
|
|
1712
|
+
const sanitized = calleeName != null && (config.sanitizerNames ?? []).includes(calleeName);
|
|
1198
1713
|
if (sanitized)
|
|
1199
1714
|
return true;
|
|
1200
1715
|
}
|
|
@@ -1222,6 +1737,54 @@ function isSafeDynamicPart(part, ast, scan) {
|
|
|
1222
1737
|
// prove it safe, so treat it as unresolved.
|
|
1223
1738
|
return false;
|
|
1224
1739
|
}
|
|
1740
|
+
/**
|
|
1741
|
+
* The tagged-template call node when `node` is (or wraps) a drizzle `sql`/`db`
|
|
1742
|
+
* tagged template; null otherwise. Handles both candidate shapes: a bare
|
|
1743
|
+
* `sql\`…\`` (the candidate node IS the tag) and `db.execute(sql\`…\`)` (the
|
|
1744
|
+
* tag is the enclosing call's argument).
|
|
1745
|
+
*/
|
|
1746
|
+
function findTaggedTemplateCall(node, adapter, sourceCode, tagNames) {
|
|
1747
|
+
if (isTaggedTemplateSqlCall(node, adapter, sourceCode, tagNames))
|
|
1748
|
+
return node;
|
|
1749
|
+
const args = adapter.getChildren(node).find((c) => adapter.getNodeType(c) === 'arguments');
|
|
1750
|
+
if (!args)
|
|
1751
|
+
return null;
|
|
1752
|
+
for (const arg of adapter.getChildren(args)) {
|
|
1753
|
+
const t = adapter.getNodeType(arg);
|
|
1754
|
+
if (t === '(' || t === ')' || t === ',')
|
|
1755
|
+
continue;
|
|
1756
|
+
if (isTaggedTemplateSqlCall(arg, adapter, sourceCode, tagNames))
|
|
1757
|
+
return arg;
|
|
1758
|
+
}
|
|
1759
|
+
return null;
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* True when `node` is (or wraps) a drizzle `sql`/`db` tagged template whose
|
|
1763
|
+
* `${…}` interpolations are all bare identifiers — the parameterized-by-
|
|
1764
|
+
* construction form. Drizzle's `sql` tag turns a clean `${id}` into a `?`
|
|
1765
|
+
* placeholder, so a bare-identifier interpolation is not an injection vector.
|
|
1766
|
+
*
|
|
1767
|
+
* A non-identifier interpolation — string concatenation (`${'%' + x + '%'}`), a
|
|
1768
|
+
* call (`${fn(x)}`), a member access (`${session.id}`) — is raw assembly, NOT a
|
|
1769
|
+
* bound parameter, so it is deliberately excluded and falls through to the
|
|
1770
|
+
* dynamic-string construction path (§69 Fix 2).
|
|
1771
|
+
*/
|
|
1772
|
+
function isParameterizedTaggedTemplate(node, adapter, sourceCode, tagNames) {
|
|
1773
|
+
const tagCall = findTaggedTemplateCall(node, adapter, sourceCode, tagNames);
|
|
1774
|
+
if (!tagCall)
|
|
1775
|
+
return false;
|
|
1776
|
+
const template = adapter.getChildren(tagCall).find((c) => isTemplateLiteral(c, adapter));
|
|
1777
|
+
if (!template)
|
|
1778
|
+
return false;
|
|
1779
|
+
for (const child of adapter.getChildren(template)) {
|
|
1780
|
+
if (adapter.getNodeType(child) !== 'template_substitution')
|
|
1781
|
+
continue;
|
|
1782
|
+
const inner = adapter.getChildren(child).find((c) => adapter.getNodeType(c) !== 'template_substitution');
|
|
1783
|
+
if (!inner || adapter.getNodeType(inner) !== 'identifier')
|
|
1784
|
+
return false;
|
|
1785
|
+
}
|
|
1786
|
+
return true;
|
|
1787
|
+
}
|
|
1225
1788
|
function checkQuerySecurity(node, text, ast, scan) {
|
|
1226
1789
|
const { adapter, sourceCode, config } = scan;
|
|
1227
1790
|
// Parameterized chains (.prepare().bind(), .exec() spread, D1 convenience,
|
|
@@ -1229,6 +1792,12 @@ function checkQuerySecurity(node, text, ast, scan) {
|
|
|
1229
1792
|
if (isParameterizedByChain(node, adapter, sourceCode, config, effectiveWrapperNames(scan))) {
|
|
1230
1793
|
return { parameterized: true, injectionRisk: false, escaped: false };
|
|
1231
1794
|
}
|
|
1795
|
+
// A drizzle `sql`/`db` tagged template parameterizes bare `${id}` interpolations
|
|
1796
|
+
// into `?` placeholders by construction — the "right reason" a clean tagged
|
|
1797
|
+
// template is quiet (§69 Fix 2), not the broken recursion that used to hide it.
|
|
1798
|
+
if (isParameterizedTaggedTemplate(node, adapter, sourceCode, config.sqlTagNames ?? SQL_TAG_NAMES)) {
|
|
1799
|
+
return { parameterized: true, injectionRisk: false, escaped: false };
|
|
1800
|
+
}
|
|
1232
1801
|
if ((config.securityPatterns?.parameterizedQueries || []).some(p => text.includes(p))) {
|
|
1233
1802
|
return { parameterized: true, injectionRisk: false, escaped: false };
|
|
1234
1803
|
}
|
|
@@ -1256,39 +1825,39 @@ function checkQuerySecurity(node, text, ast, scan) {
|
|
|
1256
1825
|
};
|
|
1257
1826
|
}
|
|
1258
1827
|
function extractCallExpressionMethod(callExpr, adapter, sourceCode) {
|
|
1259
|
-
//
|
|
1260
|
-
//
|
|
1261
|
-
//
|
|
1262
|
-
//
|
|
1263
|
-
//
|
|
1264
|
-
//
|
|
1265
|
-
//
|
|
1266
|
-
//
|
|
1267
|
-
// than falling through to the "unknown" regex fallback.
|
|
1828
|
+
// Walk the AST to the callee and read the method name as a node. The name is
|
|
1829
|
+
// `property_identifier` on a `member_expression` (or `field_identifier` on a Go
|
|
1830
|
+
// `selector_expression`); a bare `identifier` callee is a named function call.
|
|
1831
|
+
// There is no shape where scanning the call text for `name(` is the right
|
|
1832
|
+
// answer — an unrecognised callee shape is "no method name" (null), never a
|
|
1833
|
+
// guessed name. `getCallExpressionCallee` recurses through `await_expression`
|
|
1834
|
+
// and stops before `arguments`, so a keyword inside a template-literal argument
|
|
1835
|
+
// is never read as the name.
|
|
1268
1836
|
const callee = getCallExpressionCallee(callExpr, adapter);
|
|
1269
|
-
if (callee)
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
return adapter.getNodeText(prop, sourceCode);
|
|
1276
|
-
}
|
|
1277
|
-
else {
|
|
1278
|
-
// Bare identifier call (e.g. `exec(...)`)
|
|
1279
|
-
return adapter.getNodeText(callee, sourceCode);
|
|
1280
|
-
}
|
|
1837
|
+
if (!callee)
|
|
1838
|
+
return null;
|
|
1839
|
+
const calleeType = adapter.getNodeType(callee);
|
|
1840
|
+
if (calleeType === 'member_expression') {
|
|
1841
|
+
const prop = adapter.getChildren(callee).find(c => adapter.getNodeType(c) === 'property_identifier');
|
|
1842
|
+
return prop ? adapter.getNodeText(prop, sourceCode) : null;
|
|
1281
1843
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1844
|
+
if (calleeType === 'selector_expression') {
|
|
1845
|
+
const field = adapter.getChildren(callee).find(c => adapter.getNodeType(c) === 'field_identifier');
|
|
1846
|
+
return field ? adapter.getNodeText(field, sourceCode) : null;
|
|
1847
|
+
}
|
|
1848
|
+
if (calleeType === 'identifier') {
|
|
1849
|
+
return adapter.getNodeText(callee, sourceCode);
|
|
1850
|
+
}
|
|
1851
|
+
if (calleeType === 'call_expression') {
|
|
1852
|
+
return extractCallExpressionMethod(callee, adapter, sourceCode);
|
|
1853
|
+
}
|
|
1854
|
+
return null;
|
|
1286
1855
|
}
|
|
1287
1856
|
function extractMethodName(node, adapter, sourceCode) {
|
|
1288
1857
|
const nodeType = adapter.getNodeType(node);
|
|
1289
1858
|
// For template literals inside a DB-provenanced call, extract the method
|
|
1290
1859
|
// name from the enclosing call expression instead of the template body.
|
|
1291
|
-
if (nodeType === 'template_string'
|
|
1860
|
+
if (nodeType === 'template_string') {
|
|
1292
1861
|
const parent = adapter.getParent(node);
|
|
1293
1862
|
if (parent && adapter.getNodeType(parent) === 'arguments') {
|
|
1294
1863
|
const callExpr = adapter.getParent(parent);
|
|
@@ -1296,17 +1865,15 @@ function extractMethodName(node, adapter, sourceCode) {
|
|
|
1296
1865
|
return extractCallExpressionMethod(callExpr, adapter, sourceCode);
|
|
1297
1866
|
}
|
|
1298
1867
|
}
|
|
1868
|
+
return null;
|
|
1299
1869
|
}
|
|
1300
|
-
// For call expressions themselves, extract from the callee child. The
|
|
1301
|
-
// full node text includes the arguments (which may contain template
|
|
1302
|
-
// literals with SQL keywords), so a naive regex scan of the whole text
|
|
1303
|
-
// can match COUNT, JOIN, WHERE, etc. instead of the real method name.
|
|
1304
1870
|
if (nodeType === 'call_expression') {
|
|
1305
1871
|
return extractCallExpressionMethod(node, adapter, sourceCode);
|
|
1306
1872
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1873
|
+
// Any other node shape has no callee to walk to: there is no method name. The
|
|
1874
|
+
// site abstains (null) rather than scanning the node's text for a `name(` that
|
|
1875
|
+
// would turn "I don't recognise this shape" into a confident wrong name.
|
|
1876
|
+
return null;
|
|
1310
1877
|
}
|
|
1311
1878
|
/**
|
|
1312
1879
|
* Spec 21 R6.2 (reworked for Spec 44 — rule authenticity, and Spec 62 Amendment
|
|
@@ -1394,14 +1961,32 @@ const MESSAGE_LIFECYCLE_METHODS = new Set(['ack', 'nack', 'acknowledge', 'delete
|
|
|
1394
1961
|
* Each finding carries the query call location (never line 1).
|
|
1395
1962
|
*/
|
|
1396
1963
|
function checkLoopQueries(ast, scan) {
|
|
1397
|
-
|
|
1398
|
-
|
|
1964
|
+
return collectLoopQueryCandidates(ast, scan).map((c) => {
|
|
1965
|
+
const depthMsg = c.depth > 1 ? ` (nested ${c.depth} levels deep)` : '';
|
|
1966
|
+
return makeViolation(c.file, { line: c.line, column: c.column }, `Database query inside loop${depthMsg} ` +
|
|
1967
|
+
`(loop at line ${c.loopLine}). ` +
|
|
1968
|
+
`This may cause N+1 performance issues. Consider batching queries or using a join.`, { severity: 'severe', rule: 'loop-query', symbol: c.symbol });
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* R4.1: the candidate-collection half of `checkLoopQueries`, factored out so the
|
|
1973
|
+
* `loop-queries` producer projects the same loop→query candidates without
|
|
1974
|
+
* producing violations. Dedup (one candidate per loop, anchored at the first
|
|
1975
|
+
* query) and the LLM/queue suppression live here — they need the AST.
|
|
1976
|
+
*/
|
|
1977
|
+
function collectLoopQueryCandidates(ast, scan) {
|
|
1978
|
+
const { adapter, sourceCode } = scan;
|
|
1979
|
+
const candidates = [];
|
|
1399
1980
|
// Spec 21: provenance-gated detection of database calls.
|
|
1400
1981
|
const dbNodes = adapter.findNodes(ast, {
|
|
1401
|
-
custom: (node) => isDbCallNode(node,
|
|
1982
|
+
custom: (node) => isDbCallNode(node, scan),
|
|
1402
1983
|
});
|
|
1403
1984
|
const reported = new Set();
|
|
1404
1985
|
const loopOrdinals = new Map();
|
|
1986
|
+
// §13.1: functions/methods this file invokes inside a `db.transaction(fn)`
|
|
1987
|
+
// callback have their writes already batched by the caller's transaction (see
|
|
1988
|
+
// the helper) — provided their statements were prepared outside the loop.
|
|
1989
|
+
const transactionWrappedFunctions = collectTransactionWrappedFunctions(ast, adapter, sourceCode);
|
|
1405
1990
|
for (const node of dbNodes) {
|
|
1406
1991
|
// A DB call and its template-literal SQL argument both satisfy isDbCallNode —
|
|
1407
1992
|
// the call via provenance, the literal via the template-literal branch — so one
|
|
@@ -1424,9 +2009,63 @@ function checkLoopQueries(ast, scan) {
|
|
|
1424
2009
|
// fragment, not a query result (knex `sql.push(...this.aggregate(stmt))`).
|
|
1425
2010
|
if (isSqlStringConstruction(node, adapter, sourceCode))
|
|
1426
2011
|
continue;
|
|
1427
|
-
const loopInfo = findEnclosingLoop(node, adapter);
|
|
2012
|
+
const loopInfo = findEnclosingLoop(node, adapter, sourceCode);
|
|
1428
2013
|
if (!loopInfo)
|
|
1429
2014
|
continue;
|
|
2015
|
+
// §13.3 (for-of-iterable discriminator): a DB call in the iterable/header of
|
|
2016
|
+
// a `for…of` loop — `for (const c of loadCatalog())` where `loadCatalog`
|
|
2017
|
+
// memoizes a single `indexHandle.query` — is evaluated once to produce the
|
|
2018
|
+
// iterated array, then the body iterates it in memory. That is not
|
|
2019
|
+
// per-iteration I/O, so it is not an N+1 — the same subject-vs-callback
|
|
2020
|
+
// distinction `isIteratorCallback` draws for `db.all().map(…)`. (A DB call in
|
|
2021
|
+
// the *body* still fires; only the header position is exempted.)
|
|
2022
|
+
if (isForOfIterableDbCall(node, loopInfo.loopNode, adapter)) {
|
|
2023
|
+
continue;
|
|
2024
|
+
}
|
|
2025
|
+
// §13.1 (hoisted-reuse discriminator): a prepared statement executed inside a
|
|
2026
|
+
// loop is not an N+1 when the statement is prepared *outside* the loop — the
|
|
2027
|
+
// loop re-runs one compiled statement with bound parameters, which is exactly
|
|
2028
|
+
// the "batch the queries" remediation the finding would prescribe, so flagging
|
|
2029
|
+
// it is a false positive. Two shapes of "prepared outside the loop" are
|
|
2030
|
+
// recognized, both keyed on the *prepare* (a relation between two located
|
|
2031
|
+
// facts), never on a property of the call shape:
|
|
2032
|
+
// (1) transaction-batched — the loop is enclosed by a single
|
|
2033
|
+
// `db.transaction(fn)` callback (lexically, or via a helper invoked
|
|
2034
|
+
// inside it), so every write defers to one commit;
|
|
2035
|
+
// (2) hoisted re-run without a transaction — the loop re-runs a statement
|
|
2036
|
+
// object prepared outside it (`const stmt = db.prepare(…);
|
|
2037
|
+
// for (…) stmt.run(x)` or `stmt.bind(x).run()`). The in-loop call must
|
|
2038
|
+
// be a member call whose base identifier is bound to a `.prepare()`
|
|
2039
|
+
// result declared *outside* the loop (see `isHoistedStatementReRun`).
|
|
2040
|
+
// Both require the loop body to NOT `.prepare(...)` per iteration — a loop that
|
|
2041
|
+
// still calls `.prepare(...)` re-prepares every pass and remains a genuine N+1,
|
|
2042
|
+
// so it keeps firing. Crucially, shape (2) does NOT key on "no SQL string
|
|
2043
|
+
// argument": an ORM builder chain (`db.select().from(…).where(…)`,
|
|
2044
|
+
// `prisma.user.findUnique({ where: … })`) and a connection call whose SQL is a
|
|
2045
|
+
// hoisted variable both carry no SQL literal, yet the builder is a genuine
|
|
2046
|
+
// per-iteration N+1 and the receiver is not prepare-bound, so neither is a
|
|
2047
|
+
// statement re-run — they keep firing. (A per-iteration
|
|
2048
|
+
// `db.transaction(() => …)` *inside* the loop also still fires — that does not
|
|
2049
|
+
// wrap the loop, and each iteration commits separately.)
|
|
2050
|
+
const enclosingFnName = findEnclosingFunctionIdentity(loopInfo.loopNode, adapter, ast.filePath).name;
|
|
2051
|
+
const transactionEnclosesLoop = isInsideDbTransaction(loopInfo.loopNode, adapter, sourceCode) ||
|
|
2052
|
+
(enclosingFnName !== null && transactionWrappedFunctions.has(enclosingFnName));
|
|
2053
|
+
const preparesInLoop = loopBodyContainsPrepare(loopInfo.loopNode, adapter, sourceCode);
|
|
2054
|
+
const hoistedReuse = !preparesInLoop &&
|
|
2055
|
+
(transactionEnclosesLoop ||
|
|
2056
|
+
isHoistedStatementReRun(node, loopInfo.loopNode, adapter, sourceCode));
|
|
2057
|
+
if (hoistedReuse) {
|
|
2058
|
+
continue;
|
|
2059
|
+
}
|
|
2060
|
+
// §13.2 (per-batch binding discriminator): a DB call whose bound parameters
|
|
2061
|
+
// are a *spread of a collection-derived expression* (a `.slice()`/`.map()`/
|
|
2062
|
+
// chunk of the iterated set) executes one statement per batch, not per row —
|
|
2063
|
+
// a chunked `id IN (…)` write is not an N+1 at any chunk size. Only a scalar
|
|
2064
|
+
// or property read off a single loop element (`run(row.id)`) is per-row. See
|
|
2065
|
+
// `bindsBatchArgument`.
|
|
2066
|
+
if (bindsBatchArgument(node, loopInfo.loopNode, adapter, sourceCode)) {
|
|
2067
|
+
continue;
|
|
2068
|
+
}
|
|
1430
2069
|
// R4.1 (Spec 46): LLM-pipeline discriminator. A loop whose body invokes an
|
|
1431
2070
|
// LLM/agent (embedding, model completion, corpus extraction) is an intentional
|
|
1432
2071
|
// *sequential pipeline* — its per-item DB calls are persistence steps gated by
|
|
@@ -1451,13 +2090,25 @@ function checkLoopQueries(ast, scan) {
|
|
|
1451
2090
|
continue;
|
|
1452
2091
|
reported.add(dedupKey);
|
|
1453
2092
|
const sym = nextSymbol(enclosingIdentity(node, adapter, ast.filePath), 'loop-query', loopOrdinals);
|
|
1454
|
-
//
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
2093
|
+
// Anchor to the resolved callee, not the raw node. For `await db.prepare(…)
|
|
2094
|
+
// .bind(…).all<T>(…)` tree-sitter emits an OUTER call_expression at the `await`
|
|
2095
|
+
// keyword (its function is an await_expression wrapping the chain) alongside the
|
|
2096
|
+
// inner call at `db`. Parent-before-child traversal hands the outer node to this
|
|
2097
|
+
// loop first, so the caret would land on `await` instead of the query. Resolving
|
|
2098
|
+
// the callee (which already recurses through await_expression) re-anchors to the
|
|
2099
|
+
// `db` receiver; for every non-await shape the callee starts at the same column
|
|
2100
|
+
// as the node, so nothing else moves.
|
|
2101
|
+
const anchor = getCallExpressionCallee(node, adapter) ?? node;
|
|
2102
|
+
candidates.push({
|
|
2103
|
+
file: ast.filePath,
|
|
2104
|
+
line: anchor.location.start.line,
|
|
2105
|
+
column: anchor.location.start.column,
|
|
2106
|
+
symbol: sym,
|
|
2107
|
+
loopLine: loopInfo.loopNode.location.start.line,
|
|
2108
|
+
depth: loopInfo.depth,
|
|
2109
|
+
});
|
|
1459
2110
|
}
|
|
1460
|
-
return
|
|
2111
|
+
return candidates;
|
|
1461
2112
|
}
|
|
1462
2113
|
/**
|
|
1463
2114
|
* R4.1 (Spec 46): True when the loop's subtree contains an LLM/agent invocation.
|
|
@@ -1498,36 +2149,587 @@ function loopBodyContainsMessageLifecycleCall(loopNode, adapter, sourceCode) {
|
|
|
1498
2149
|
});
|
|
1499
2150
|
return found;
|
|
1500
2151
|
}
|
|
1501
|
-
/**
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
2152
|
+
/**
|
|
2153
|
+
* §13.1 (prepare-outside-loop guard): true when the loop's subtree issues a
|
|
2154
|
+
* `.prepare(...)` call — i.e. the statement is (re)built on every iteration, not
|
|
2155
|
+
* prepared once outside the loop. This is the second half of the
|
|
2156
|
+
* transaction-batched discriminator: a loop wrapped in a transaction is only a
|
|
2157
|
+
* non-N+1 when it reuses a statement prepared outside; a per-iteration
|
|
2158
|
+
* `db.prepare(sql).run(…)` (or `const stmt = db.prepare(…); stmt.run(…)`) still
|
|
2159
|
+
* re-prepares every pass and remains a genuine N+1 under a transaction. Walking
|
|
2160
|
+
* the whole loop subtree (header + body) is safe: a `.prepare` is only ever in
|
|
2161
|
+
* the body, never the header.
|
|
2162
|
+
*/
|
|
2163
|
+
function loopBodyContainsPrepare(loopNode, adapter, sourceCode) {
|
|
2164
|
+
let found = false;
|
|
2165
|
+
walkSubtree(loopNode, adapter, (node) => {
|
|
2166
|
+
if (found || adapter.getNodeType(node) !== 'call_expression')
|
|
2167
|
+
return;
|
|
2168
|
+
if (dbCallMethodName(node, adapter, sourceCode) === 'prepare')
|
|
2169
|
+
found = true;
|
|
2170
|
+
});
|
|
2171
|
+
return found;
|
|
2172
|
+
}
|
|
2173
|
+
/**
|
|
2174
|
+
* §13.1 (hoisted-reuse discriminator, non-transaction shape): true when the
|
|
2175
|
+
* in-loop DB call re-runs a statement object prepared *outside* the loop —
|
|
2176
|
+
* `const stmt = db.prepare(…); for (…) stmt.run(x)` or `stmt.bind(x).run()`.
|
|
2177
|
+
*
|
|
2178
|
+
* The signal is the *prepare* — a relation between two located facts — not a
|
|
2179
|
+
* property of the call shape. The in-loop eager call's member chain must be
|
|
2180
|
+
* rooted at an identifier (`stmt`) that is declared, in an enclosing scope
|
|
2181
|
+
* *outside* the loop, with a `.prepare()` initializer. An ORM builder chain
|
|
2182
|
+
* (`db.select().from(…).where(…)`, `prisma.user.findUnique({ where: … })`),
|
|
2183
|
+
* a direct connection call (`db.exec("…")`), a bare DB-provenanced helper
|
|
2184
|
+
* (`resolveHero(db, slug)`), and a Map/Array method that provenance over-matched
|
|
2185
|
+
* (`counts.get(x)`, `adjudication.find(x)`) are all rooted at an identifier that
|
|
2186
|
+
* is NOT prepare-bound, so none of them reads as a statement re-run and each
|
|
2187
|
+
* keeps firing.
|
|
2188
|
+
*/
|
|
2189
|
+
function isHoistedStatementReRun(node, loopNode, adapter, sourceCode) {
|
|
2190
|
+
const base = memberChainBaseIdentifier(node, adapter, sourceCode);
|
|
2191
|
+
if (base && identifierBoundToPrepareOutsideLoop(base, loopNode, adapter, sourceCode)) {
|
|
2192
|
+
return true;
|
|
1506
2193
|
}
|
|
2194
|
+
return isBarePrepareBoundHelperCall(node, loopNode, adapter, sourceCode);
|
|
1507
2195
|
}
|
|
1508
2196
|
/**
|
|
1509
|
-
*
|
|
1510
|
-
*
|
|
1511
|
-
*
|
|
1512
|
-
*
|
|
2197
|
+
* §13.1 (hoisted-reuse discriminator, bare-wrapper shape): true when the in-loop
|
|
2198
|
+
* DB call is a bare local-helper invocation (`deleteFileEntries(clears, path)`)
|
|
2199
|
+
* whose argument is an identifier bound, outside the loop, to a prepare result —
|
|
2200
|
+
* `clears = prepareStyleClearStatements(rawDb)`, then the loop body forwards
|
|
2201
|
+
* `clears` into a helper that runs `clears.decl.run(path)` on every pass. The
|
|
2202
|
+
* helper re-runs pre-prepared statements; the prepare is the signal, located via
|
|
2203
|
+
* the *argument's* binding rather than the call's own member chain (a bare
|
|
2204
|
+
* identifier callee has none). Member-shape re-runs are handled above; this is
|
|
2205
|
+
* the bare-call complement.
|
|
1513
2206
|
*/
|
|
1514
|
-
function
|
|
1515
|
-
const
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
2207
|
+
function isBarePrepareBoundHelperCall(node, loopNode, adapter, sourceCode) {
|
|
2208
|
+
const call = resolveDbCallNode(node, adapter);
|
|
2209
|
+
if (!call)
|
|
2210
|
+
return false;
|
|
2211
|
+
const callee = getCallExpressionCallee(call, adapter);
|
|
2212
|
+
if (!callee || adapter.getNodeType(callee) !== 'identifier')
|
|
2213
|
+
return false;
|
|
2214
|
+
const argsNode = adapter.getChildren(call).find((c) => adapter.getNodeType(c) === 'arguments');
|
|
2215
|
+
if (!argsNode)
|
|
2216
|
+
return false;
|
|
2217
|
+
for (const arg of adapter.getChildren(argsNode)) {
|
|
2218
|
+
if (adapter.getNodeType(arg) !== 'identifier')
|
|
2219
|
+
continue;
|
|
2220
|
+
if (identifierBoundToPrepareOutsideLoop(adapter.getNodeText(arg, sourceCode), loopNode, adapter, sourceCode)) {
|
|
1520
2221
|
return true;
|
|
1521
|
-
// Member callee whose object is an LLM client: `model.chat(…)`, `llm.invoke(…)`.
|
|
1522
|
-
if (adapter.getNodeType(callee) === 'member_expression') {
|
|
1523
|
-
const obj = adapter.getChildren(callee).find(c => adapter.getNodeType(c) === 'identifier');
|
|
1524
|
-
if (obj && LLM_CLIENT_ARG_NAMES.has(adapter.getNodeText(obj, sourceCode)))
|
|
1525
|
-
return true;
|
|
1526
2222
|
}
|
|
1527
2223
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
2224
|
+
return false;
|
|
2225
|
+
}
|
|
2226
|
+
/**
|
|
2227
|
+
* The base identifier a member-call chain is rooted at — `stmt` for `stmt.run(x)`
|
|
2228
|
+
* and for `stmt.bind(x).run()` (the `.bind()` call's receiver), `db` for
|
|
2229
|
+
* `db.select().from(…).where(…)`, `prisma` for `prisma.user.findUnique(…)`.
|
|
2230
|
+
* Returns null when the chain is not a member call (a bare `helper(db, …)`) or
|
|
2231
|
+
* has no identifier root (an indexed access `x[i]()`).
|
|
2232
|
+
*/
|
|
2233
|
+
function memberChainBaseIdentifier(node, adapter, sourceCode) {
|
|
2234
|
+
const eagerCall = findEagerExecutionCall(node, adapter, sourceCode);
|
|
2235
|
+
if (!eagerCall)
|
|
2236
|
+
return null;
|
|
2237
|
+
const memberExpr = findMemberCallee(eagerCall, adapter);
|
|
2238
|
+
if (!memberExpr)
|
|
2239
|
+
return null;
|
|
2240
|
+
return baseIdentifierOfMemberExpr(memberExpr, adapter, sourceCode);
|
|
2241
|
+
}
|
|
2242
|
+
/**
|
|
2243
|
+
* Recurse down a member_expression's object side to the base identifier: the
|
|
2244
|
+
* object of `a.b.c` is `a.b`, whose object is `a`; the object of `stmt.bind(x)`
|
|
2245
|
+
* is the call, whose own member callee is `stmt.bind`, whose object is `stmt`.
|
|
2246
|
+
*/
|
|
2247
|
+
function baseIdentifierOfMemberExpr(memberExpr, adapter, sourceCode) {
|
|
2248
|
+
const object = adapter.getChildren(memberExpr).find((c) => {
|
|
2249
|
+
const t = adapter.getNodeType(c);
|
|
2250
|
+
return t !== 'property_identifier' && t !== 'field_identifier' && t !== '.';
|
|
2251
|
+
});
|
|
2252
|
+
if (!object)
|
|
2253
|
+
return null;
|
|
2254
|
+
const type = adapter.getNodeType(object);
|
|
2255
|
+
if (type === 'identifier')
|
|
2256
|
+
return adapter.getNodeText(object, sourceCode);
|
|
2257
|
+
if (type === 'member_expression')
|
|
2258
|
+
return baseIdentifierOfMemberExpr(object, adapter, sourceCode);
|
|
2259
|
+
if (type === 'call_expression') {
|
|
2260
|
+
const inner = findMemberCallee(object, adapter);
|
|
2261
|
+
return inner ? baseIdentifierOfMemberExpr(inner, adapter, sourceCode) : null;
|
|
2262
|
+
}
|
|
2263
|
+
return null;
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2266
|
+
* True when `name` is declared, in the enclosing function (or program), with a
|
|
2267
|
+
* `.prepare()` initializer, at a position *outside* the loop. Walking the whole
|
|
2268
|
+
* enclosing function is safe for this signal: the caller has already established
|
|
2269
|
+
* `preparesInLoop` is false, so the only `.prepare()` sites are outside the loop,
|
|
2270
|
+
* and requiring the declarator to sit before the loop excludes any same-named
|
|
2271
|
+
* declaration in a sibling scope after it. (A same-named variable in a *nested*
|
|
2272
|
+
* function would be a false match; that shadowing shape is not present in the
|
|
2273
|
+
* measured corpora and is recorded here rather than silently dropped.)
|
|
2274
|
+
*/
|
|
2275
|
+
function identifierBoundToPrepareOutsideLoop(name, loopNode, adapter, sourceCode) {
|
|
2276
|
+
const scope = findEnclosingFunctionNode(loopNode, adapter) ?? loopNode;
|
|
2277
|
+
const loopStart = loopNode.range[0];
|
|
2278
|
+
let found = false;
|
|
2279
|
+
walkSubtree(scope, adapter, (n) => {
|
|
2280
|
+
if (found)
|
|
2281
|
+
return;
|
|
2282
|
+
if (n === loopNode)
|
|
2283
|
+
return;
|
|
2284
|
+
if (adapter.getNodeType(n) !== 'variable_declarator')
|
|
2285
|
+
return;
|
|
2286
|
+
if (n.range[0] >= loopStart)
|
|
2287
|
+
return;
|
|
2288
|
+
const { name: declaredName, value } = splitDeclarator(n, adapter, sourceCode);
|
|
2289
|
+
if (declaredName !== name)
|
|
2290
|
+
return;
|
|
2291
|
+
if (value && subtreeContainsPrepare(value, adapter, sourceCode))
|
|
2292
|
+
found = true;
|
|
2293
|
+
});
|
|
2294
|
+
if (found)
|
|
2295
|
+
return true;
|
|
2296
|
+
// A parameter typed as a prepared-statement bundle (`stmts: StyleStatements`,
|
|
2297
|
+
// `stmt: SqliteStatement`) is prepare-bound by construction — its members are
|
|
2298
|
+
// the `.prepare()` results the caller hoisted before the loop. Re-running
|
|
2299
|
+
// `stmts.decl.run(x)` is therefore the same hoisted-reuse signal as a local
|
|
2300
|
+
// `const stmt = db.prepare(…)` re-run. A handle parameter (`SqliteDatabase`,
|
|
2301
|
+
// `IndexHandle`, `any`) does not name a Statement type and keeps firing.
|
|
2302
|
+
return scope !== loopNode && parameterBoundToStatementType(name, scope, adapter, sourceCode);
|
|
2303
|
+
}
|
|
2304
|
+
/**
|
|
2305
|
+
* True when `name` is a parameter of `fnNode` whose type annotation names a
|
|
2306
|
+
* prepared-statement type. The type is the signal (mirroring how a local
|
|
2307
|
+
* declarator's `.prepare()` initializer is), never the parameter's name: a
|
|
2308
|
+
* `*Statement`/`*Statements` annotation means the value is a compiled statement
|
|
2309
|
+
* (or a bundle of them), so a loop that re-runs it re-runs pre-prepared SQL.
|
|
2310
|
+
*
|
|
2311
|
+
* @param name the parameter name to match
|
|
2312
|
+
* @param fnNode the function node whose parameters are inspected
|
|
2313
|
+
* @param adapter the language adapter used to read parameter nodes
|
|
2314
|
+
* @param sourceCode the file source text for reading node text
|
|
2315
|
+
* @returns true when the named parameter is annotated with a statement type
|
|
2316
|
+
*/
|
|
2317
|
+
export function parameterBoundToStatementType(name, fnNode, adapter, sourceCode) {
|
|
2318
|
+
const params = adapter.getChildren(fnNode).find((c) => adapter.getNodeType(c) === 'formal_parameters');
|
|
2319
|
+
if (!params)
|
|
2320
|
+
return false;
|
|
2321
|
+
for (const p of adapter.getChildren(params)) {
|
|
2322
|
+
const t = adapter.getNodeType(p);
|
|
2323
|
+
if (t !== 'required_parameter' && t !== 'optional_parameter')
|
|
2324
|
+
continue;
|
|
2325
|
+
// The parameter name is the `identifier` child; the type is the
|
|
2326
|
+
// `type_annotation` child's text, read structurally (not by regex over the
|
|
2327
|
+
// whole parameter text, which split `x: StatementFactory` into a name/type
|
|
2328
|
+
// pair but then matched any `Statement…` prefix).
|
|
2329
|
+
const nameNode = adapter.getChildren(p).find((c) => adapter.getNodeType(c) === 'identifier');
|
|
2330
|
+
if (!nameNode)
|
|
2331
|
+
continue;
|
|
2332
|
+
if (adapter.getNodeText(nameNode, sourceCode) !== name)
|
|
2333
|
+
continue;
|
|
2334
|
+
const typeAnn = adapter.getChildren(p).find((c) => adapter.getNodeType(c) === 'type_annotation');
|
|
2335
|
+
if (!typeAnn)
|
|
2336
|
+
continue;
|
|
2337
|
+
const typeText = adapter.getNodeText(typeAnn, sourceCode)?.trim() ?? '';
|
|
2338
|
+
// A statement type *ends* in `Statement`/`Statements` (`SqliteStatement`,
|
|
2339
|
+
// `StyleStatements`). `StatementFactory` embeds "Statement" but is a factory,
|
|
2340
|
+
// not a statement type, so it does not qualify.
|
|
2341
|
+
if (/Statement(s)?$/.test(typeText))
|
|
2342
|
+
return true;
|
|
2343
|
+
}
|
|
2344
|
+
return false;
|
|
2345
|
+
}
|
|
2346
|
+
/** True when a subtree contains a `.prepare()` call (an initializer that is or
|
|
2347
|
+
* chains into `db.prepare(…)`, e.g. `db.prepare(sql).bind(…)`) or a `prepare*`
|
|
2348
|
+
* factory call (`prepareStyleClearStatements(rawDb)`) — a function whose whole
|
|
2349
|
+
* job is to return pre-prepared statement objects, so its name carries the same
|
|
2350
|
+
* "statement prepared here" signal the `.prepare()` member call does. */
|
|
2351
|
+
function subtreeContainsPrepare(node, adapter, sourceCode) {
|
|
2352
|
+
let found = false;
|
|
2353
|
+
walkSubtree(node, adapter, (n) => {
|
|
2354
|
+
if (found || adapter.getNodeType(n) !== 'call_expression')
|
|
2355
|
+
return;
|
|
2356
|
+
if (dbCallMethodName(n, adapter, sourceCode) === 'prepare') {
|
|
2357
|
+
found = true;
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
const callee = getCallExpressionCallee(n, adapter);
|
|
2361
|
+
if (callee && adapter.getNodeType(callee) === 'identifier') {
|
|
2362
|
+
const name = adapter.getNodeText(callee, sourceCode);
|
|
2363
|
+
if (name && /^prepare/i.test(name))
|
|
2364
|
+
found = true;
|
|
2365
|
+
}
|
|
2366
|
+
});
|
|
2367
|
+
return found;
|
|
2368
|
+
}
|
|
2369
|
+
/**
|
|
2370
|
+
* §13.1: True when `node` sits inside a `db.transaction(fn)` callback. Walks the
|
|
2371
|
+
* parent chain looking for a `call_expression` whose callee is a
|
|
2372
|
+
* `member_expression` with a `transaction` property. The loop is the input (not
|
|
2373
|
+
* the DB call) so a transaction that wraps only a single per-iteration call —
|
|
2374
|
+
* `for (…) { db.transaction(() => run()) }` — does not read as "batched": there
|
|
2375
|
+
* the loop itself is outside the transaction and every iteration commits
|
|
2376
|
+
* separately, so it must still fire.
|
|
2377
|
+
*/
|
|
2378
|
+
function isInsideDbTransaction(node, adapter, sourceCode) {
|
|
2379
|
+
let current = node;
|
|
2380
|
+
while (current) {
|
|
2381
|
+
const parent = adapter.getParent(current);
|
|
2382
|
+
if (!parent)
|
|
2383
|
+
break;
|
|
2384
|
+
if (adapter.getNodeType(parent) === 'call_expression') {
|
|
2385
|
+
const callee = adapter.getChildren(parent).find((c) => adapter.getNodeType(c) === 'member_expression');
|
|
2386
|
+
if (callee && memberPropertyName(callee, adapter, sourceCode) === 'transaction') {
|
|
2387
|
+
return true;
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
current = parent;
|
|
2391
|
+
}
|
|
2392
|
+
return false;
|
|
2393
|
+
}
|
|
2394
|
+
/**
|
|
2395
|
+
* §13.1 (transaction-wrapped-helper discriminator): names of the functions this
|
|
2396
|
+
* file invokes *inside* a `db.transaction(fn)` callback — either as a
|
|
2397
|
+
* `this.method(...)` call (`this.db.transaction(() => this.syncFileIndexRow(...))`)
|
|
2398
|
+
* or as a free function reference/call (`rawDb.transaction(insertAll)`, or
|
|
2399
|
+
* `rawDb.transaction(() => { … insertOne(…) })`). A private helper written to run
|
|
2400
|
+
* inside the caller's transaction has its per-iteration writes already batched
|
|
2401
|
+
* into the caller's single commit, so its internal loops are not a batchable N+1
|
|
2402
|
+
* *provided their statements are prepared outside the loop* (see the
|
|
2403
|
+
* no-`.prepare()`-in-loop guard at the call site). This mirrors the textual
|
|
2404
|
+
* {@link isInsideDbTransaction} check but bridges the call boundary: the loop
|
|
2405
|
+
* lives in the helper's body while the transaction wraps the *call*, which the
|
|
2406
|
+
* purely lexical check cannot see. A per-iteration `db.transaction(() => …)`
|
|
2407
|
+
* *inside* a loop is unaffected — the helper is only recognized when it is the
|
|
2408
|
+
* thing being invoked within a transaction callback.
|
|
2409
|
+
*/
|
|
2410
|
+
function collectTransactionWrappedFunctions(ast, adapter, sourceCode) {
|
|
2411
|
+
const wrapped = new Set();
|
|
2412
|
+
for (const callNode of adapter.findNodes(ast, {
|
|
2413
|
+
custom: (n) => adapter.getNodeType(n) === 'call_expression',
|
|
2414
|
+
})) {
|
|
2415
|
+
const callee = adapter.getChildren(callNode).find((c) => adapter.getNodeType(c) === 'member_expression');
|
|
2416
|
+
if (!callee || memberPropertyName(callee, adapter, sourceCode) !== 'transaction')
|
|
2417
|
+
continue;
|
|
2418
|
+
// (1) A bare function reference handed straight to `transaction` — the
|
|
2419
|
+
// reducer shape `rawDb.transaction(insertAll)()`. The callback argument is
|
|
2420
|
+
// the helper's name, not a call inside it.
|
|
2421
|
+
const args = adapter.getChildren(callNode).find((c) => adapter.getNodeType(c) === 'arguments');
|
|
2422
|
+
if (args) {
|
|
2423
|
+
for (const arg of adapter.getChildren(args)) {
|
|
2424
|
+
if (adapter.getNodeType(arg) !== 'identifier')
|
|
2425
|
+
continue;
|
|
2426
|
+
const name = adapter.getNodeText(arg, sourceCode);
|
|
2427
|
+
if (name)
|
|
2428
|
+
wrapped.add(name);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
// (2) Calls inside the callback: `this.method(...)` and free-function calls
|
|
2432
|
+
// (`insertOne(...)`).
|
|
2433
|
+
walkSubtree(callNode, adapter, (n) => {
|
|
2434
|
+
if (n === callNode)
|
|
2435
|
+
return;
|
|
2436
|
+
if (adapter.getNodeType(n) !== 'call_expression')
|
|
2437
|
+
return;
|
|
2438
|
+
const innerCallee = getCallExpressionCallee(n, adapter);
|
|
2439
|
+
if (!innerCallee)
|
|
2440
|
+
return;
|
|
2441
|
+
if (adapter.getNodeType(innerCallee) === 'identifier') {
|
|
2442
|
+
const name = adapter.getNodeText(innerCallee, sourceCode);
|
|
2443
|
+
if (name)
|
|
2444
|
+
wrapped.add(name);
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
if (adapter.getNodeType(innerCallee) !== 'member_expression')
|
|
2448
|
+
return;
|
|
2449
|
+
const obj = adapter.getChildren(innerCallee).find((c) => adapter.getNodeType(c) !== 'property_identifier');
|
|
2450
|
+
const objText = obj ? adapter.getNodeText(obj, sourceCode) : '';
|
|
2451
|
+
if (objText !== 'this')
|
|
2452
|
+
return;
|
|
2453
|
+
const prop = memberPropertyName(innerCallee, adapter, sourceCode);
|
|
2454
|
+
if (prop)
|
|
2455
|
+
wrapped.add(prop);
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
return wrapped;
|
|
2459
|
+
}
|
|
2460
|
+
/** Depth-first walk over an ASTNode subtree (children only, no parent links). */
|
|
2461
|
+
function walkSubtree(node, adapter, visitor) {
|
|
2462
|
+
visitor(node);
|
|
2463
|
+
for (const child of adapter.getChildren(node)) {
|
|
2464
|
+
walkSubtree(child, adapter, visitor);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
/**
|
|
2468
|
+
* Array methods that transform one collection into another collection (a batch):
|
|
2469
|
+
* the elements of a `.slice()`/`.splice()`/`.map()`/`.filter()`/`.concat()`/
|
|
2470
|
+
* `.flatMap()` result are a *sub*-collection, not a single row.
|
|
2471
|
+
*/
|
|
2472
|
+
const COLLECTION_TRANSFORM_METHODS = new Set(['slice', 'splice', 'map', 'filter', 'concat', 'flatMap']);
|
|
2473
|
+
/**
|
|
2474
|
+
* Free-function callee names that chunk a collection into batches — `chunk(ids,
|
|
2475
|
+
* n)`, `chunkArray(ids, n)`, `partition(rows, p)`. Recognized by name (not by a
|
|
2476
|
+
* slice call) because a chunking helper is a user-written function whose internal
|
|
2477
|
+
* shape the analyzer cannot see; the name is the only stable signal.
|
|
2478
|
+
*/
|
|
2479
|
+
const CHUNK_HELPER_RE = /^(chunk|chunks|chunkArray|chunkBy|partition|paginate|paged)$/i;
|
|
2480
|
+
/** Split a `variable_declarator` into its name text and initializer (value) node. */
|
|
2481
|
+
function splitDeclarator(node, adapter, sourceCode) {
|
|
2482
|
+
let name = null;
|
|
2483
|
+
let value = null;
|
|
2484
|
+
let pastEquals = false;
|
|
2485
|
+
for (const child of adapter.getChildren(node)) {
|
|
2486
|
+
const t = adapter.getNodeType(child);
|
|
2487
|
+
if (t === '=' || t === 'equals') {
|
|
2488
|
+
pastEquals = true;
|
|
2489
|
+
continue;
|
|
2490
|
+
}
|
|
2491
|
+
if (t === ':' || t === 'type_annotation')
|
|
2492
|
+
continue;
|
|
2493
|
+
if (!pastEquals && !name && (t === 'identifier' || t === 'object_pattern' || t === 'array_pattern')) {
|
|
2494
|
+
name = adapter.getNodeText(child, sourceCode);
|
|
2495
|
+
continue;
|
|
2496
|
+
}
|
|
2497
|
+
if ((pastEquals || name) && !value)
|
|
2498
|
+
value = child;
|
|
2499
|
+
}
|
|
2500
|
+
return { name, value };
|
|
2501
|
+
}
|
|
2502
|
+
/**
|
|
2503
|
+
* §13.2: resolve a bare identifier to the initializer of the `const`/`let` that
|
|
2504
|
+
* declares it inside the loop subtree (`const chunk = ids.slice(…)` → the slice
|
|
2505
|
+
* call). Scoped to the loop so a same-named variable elsewhere in the function is
|
|
2506
|
+
* not mistaken for the loop's chunk. Returns null when the identifier is the
|
|
2507
|
+
* loop's own element (declared in a `for…of` header, not a `variable_declarator`).
|
|
2508
|
+
*/
|
|
2509
|
+
function resolveLoopLocalBinding(identifierNode, loopNode, adapter, sourceCode) {
|
|
2510
|
+
const target = adapter.getNodeText(identifierNode, sourceCode);
|
|
2511
|
+
if (!target)
|
|
2512
|
+
return null;
|
|
2513
|
+
let result = null;
|
|
2514
|
+
walkSubtree(loopNode, adapter, (n) => {
|
|
2515
|
+
if (result)
|
|
2516
|
+
return;
|
|
2517
|
+
if (adapter.getNodeType(n) !== 'variable_declarator')
|
|
2518
|
+
return;
|
|
2519
|
+
const { name, value } = splitDeclarator(n, adapter, sourceCode);
|
|
2520
|
+
if (name === target)
|
|
2521
|
+
result = value;
|
|
2522
|
+
});
|
|
2523
|
+
return result;
|
|
2524
|
+
}
|
|
2525
|
+
/**
|
|
2526
|
+
* §13.2: true when `expr` denotes a collection-derived array — a batch of rows —
|
|
2527
|
+
* rather than a single row's scalar. Three shapes qualify:
|
|
2528
|
+
* (a) a call to an array-transform method (`ids.slice(…)`, `rows.map(…)`);
|
|
2529
|
+
* (b) a call to a chunking helper (`chunkArray(ids, n)`);
|
|
2530
|
+
* (c) a local `const chunk = <one of the above>` bound inside the loop.
|
|
2531
|
+
*/
|
|
2532
|
+
function isCollectionDerivedExpression(expr, loopNode, adapter, sourceCode) {
|
|
2533
|
+
const type = adapter.getNodeType(expr);
|
|
2534
|
+
if (type === 'call_expression') {
|
|
2535
|
+
const memberExpr = findMemberCallee(expr, adapter);
|
|
2536
|
+
if (memberExpr) {
|
|
2537
|
+
const method = memberPropertyName(memberExpr, adapter, sourceCode);
|
|
2538
|
+
if (method && COLLECTION_TRANSFORM_METHODS.has(method))
|
|
2539
|
+
return true;
|
|
2540
|
+
}
|
|
2541
|
+
const callee = getCallExpressionCallee(expr, adapter);
|
|
2542
|
+
if (callee && adapter.getNodeType(callee) === 'identifier') {
|
|
2543
|
+
const name = adapter.getNodeText(callee, sourceCode);
|
|
2544
|
+
if (name && CHUNK_HELPER_RE.test(name))
|
|
2545
|
+
return true;
|
|
2546
|
+
}
|
|
2547
|
+
return false;
|
|
2548
|
+
}
|
|
2549
|
+
if (type === 'identifier') {
|
|
2550
|
+
const value = resolveLoopLocalBinding(expr, loopNode, adapter, sourceCode);
|
|
2551
|
+
return value !== null && isCollectionDerivedExpression(value, loopNode, adapter, sourceCode);
|
|
2552
|
+
}
|
|
2553
|
+
return false;
|
|
2554
|
+
}
|
|
2555
|
+
/**
|
|
2556
|
+
* §13.3: true when a DB call sits in the iterable/header of a `for…of` loop
|
|
2557
|
+
* rather than its body. `for (const c of loadCatalog())` runs `loadCatalog` once
|
|
2558
|
+
* (it memoizes the query) and iterates the returned array in memory — the query
|
|
2559
|
+
* is not per-iteration. Only the header position qualifies; a DB call in the
|
|
2560
|
+
* `statement_block` body still executes once per pass and keeps firing.
|
|
2561
|
+
*/
|
|
2562
|
+
function isForOfIterableDbCall(node, loopNode, adapter) {
|
|
2563
|
+
if (adapter.getNodeType(loopNode) !== 'for_in_statement')
|
|
2564
|
+
return false;
|
|
2565
|
+
let body = null;
|
|
2566
|
+
for (const child of adapter.getChildren(loopNode)) {
|
|
2567
|
+
if (adapter.getNodeType(child) === 'statement_block') {
|
|
2568
|
+
body = child;
|
|
2569
|
+
break;
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
if (!body)
|
|
2573
|
+
return false;
|
|
2574
|
+
return !isDescendantOf(node, body, adapter);
|
|
2575
|
+
}
|
|
2576
|
+
/** True when `node` is `ancestor` or a descendant of it (parent-chain walk). */
|
|
2577
|
+
function isDescendantOf(node, ancestor, adapter) {
|
|
2578
|
+
let cur = node;
|
|
2579
|
+
while (cur) {
|
|
2580
|
+
if (cur === ancestor)
|
|
2581
|
+
return true;
|
|
2582
|
+
cur = adapter.getParent(cur);
|
|
2583
|
+
}
|
|
2584
|
+
return false;
|
|
2585
|
+
}
|
|
2586
|
+
/**
|
|
2587
|
+
* §13.2: the element and iterated-collection shape of a `for…of` loop. Needed to
|
|
2588
|
+
* distinguish `for (const chunk of chunkArray(ids)) { run(…chunk) }` — spreading
|
|
2589
|
+
* the loop element is a *batch* here because the collection itself is chunked —
|
|
2590
|
+
* from `for (const row of rows) { run(…row) }` — spreading the element is a
|
|
2591
|
+
* per-row write because the collection is a plain array.
|
|
2592
|
+
*/
|
|
2593
|
+
function forOfIteration(loopNode, adapter, sourceCode) {
|
|
2594
|
+
const elementNames = new Set();
|
|
2595
|
+
let iteratesChunkedCollection = false;
|
|
2596
|
+
if (adapter.getNodeType(loopNode) !== 'for_in_statement') {
|
|
2597
|
+
return { elementNames, iteratesChunkedCollection };
|
|
2598
|
+
}
|
|
2599
|
+
let element = null;
|
|
2600
|
+
let collection = null;
|
|
2601
|
+
for (const child of adapter.getChildren(loopNode)) {
|
|
2602
|
+
const t = adapter.getNodeType(child);
|
|
2603
|
+
if (t === 'statement_block')
|
|
2604
|
+
break; // the body — nothing after it matters
|
|
2605
|
+
if (!element && (t === 'identifier' || t === 'object_pattern' || t === 'array_pattern')) {
|
|
2606
|
+
element = child;
|
|
2607
|
+
}
|
|
2608
|
+
else if (element && !collection) {
|
|
2609
|
+
collection = child;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
if (element) {
|
|
2613
|
+
const name = adapter.getNodeText(element, sourceCode);
|
|
2614
|
+
if (name)
|
|
2615
|
+
elementNames.add(name);
|
|
2616
|
+
}
|
|
2617
|
+
if (collection) {
|
|
2618
|
+
iteratesChunkedCollection = isCollectionDerivedExpression(collection, loopNode, adapter, sourceCode);
|
|
2619
|
+
}
|
|
2620
|
+
return { elementNames, iteratesChunkedCollection };
|
|
2621
|
+
}
|
|
2622
|
+
/**
|
|
2623
|
+
* §13.2 (per-batch binding discriminator): true when the DB call executes I/O
|
|
2624
|
+
* that binds a *batch* — a spread of a collection-derived expression — rather
|
|
2625
|
+
* than per-row scalars, so it is not an N+1. The eager call (`stmt.run(…)`, or
|
|
2626
|
+
* the `.run(…)` chained off `db.prepare(…)`) is where parameters are bound; its
|
|
2627
|
+
* arguments decide the row-vs-batch question.
|
|
2628
|
+
*/
|
|
2629
|
+
function bindsBatchArgument(node, loopNode, adapter, sourceCode) {
|
|
2630
|
+
const eagerCall = findEagerExecutionCall(node, adapter, sourceCode);
|
|
2631
|
+
if (!eagerCall)
|
|
2632
|
+
return false;
|
|
2633
|
+
const argsNode = adapter.getChildren(eagerCall).find((c) => adapter.getNodeType(c) === 'arguments');
|
|
2634
|
+
if (!argsNode)
|
|
2635
|
+
return false;
|
|
2636
|
+
const { elementNames, iteratesChunkedCollection } = forOfIteration(loopNode, adapter, sourceCode);
|
|
2637
|
+
for (const arg of adapter.getChildren(argsNode)) {
|
|
2638
|
+
if (adapter.getNodeType(arg) === 'spread_element') {
|
|
2639
|
+
const operand = adapter.getChildren(arg).find((c) => adapter.getNodeType(c) !== '...');
|
|
2640
|
+
if (!operand)
|
|
2641
|
+
continue;
|
|
2642
|
+
// Spreading the loop's own element is per-row (`…row`) unless the element
|
|
2643
|
+
// is itself a chunk of a chunked collection (`for (const chunk of chunkArray(ids))`).
|
|
2644
|
+
if (adapter.getNodeType(operand) === 'identifier' &&
|
|
2645
|
+
elementNames.has(adapter.getNodeText(operand, sourceCode))) {
|
|
2646
|
+
if (iteratesChunkedCollection)
|
|
2647
|
+
return true;
|
|
2648
|
+
continue;
|
|
2649
|
+
}
|
|
2650
|
+
if (isCollectionDerivedExpression(operand, loopNode, adapter, sourceCode))
|
|
2651
|
+
return true;
|
|
2652
|
+
continue;
|
|
2653
|
+
}
|
|
2654
|
+
// A bare collection-derived argument (no spread) is also a batch:
|
|
2655
|
+
// `run(rows.map(…))`, or a chunk bound inside the loop (`const chunk =
|
|
2656
|
+
// rows.slice(…)` then `run(chunk)` — the identifier resolves to the slice).
|
|
2657
|
+
// A bare loop-element identifier (`for (const row of rows) run(row)`) is NOT
|
|
2658
|
+
// collection-derived: `resolveLoopLocalBinding` only sees `variable_declarator`
|
|
2659
|
+
// bindings, and a `for…of` element is declared in the header, so `row` stays
|
|
2660
|
+
// unresolved and the call keeps firing as per-row.
|
|
2661
|
+
if ((adapter.getNodeType(arg) === 'call_expression' ||
|
|
2662
|
+
adapter.getNodeType(arg) === 'identifier') &&
|
|
2663
|
+
isCollectionDerivedExpression(arg, loopNode, adapter, sourceCode)) {
|
|
2664
|
+
return true;
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
return false;
|
|
2668
|
+
}
|
|
2669
|
+
/**
|
|
2670
|
+
* §13.2: the eager I/O call that actually binds parameters for a DB call — the
|
|
2671
|
+
* call itself when it is eager (`stmt.run(…)`, `db.query(…)`), or the eager call
|
|
2672
|
+
* chained off a statement-construction call (`db.prepare(sql).run(…)`). Returns
|
|
2673
|
+
* null when the call never executes I/O (already filtered upstream).
|
|
2674
|
+
*/
|
|
2675
|
+
function findEagerExecutionCall(node, adapter, sourceCode) {
|
|
2676
|
+
const call = resolveDbCallNode(node, adapter);
|
|
2677
|
+
if (!call)
|
|
2678
|
+
return null;
|
|
2679
|
+
const direct = dbCallMethodName(call, adapter, sourceCode);
|
|
2680
|
+
if (direct && EAGER_DB_METHODS.has(direct)) {
|
|
2681
|
+
const memberExpr = findMemberCallee(call, adapter);
|
|
2682
|
+
if (!(memberExpr && isPromiseAllMember(memberExpr, adapter, sourceCode)))
|
|
2683
|
+
return call;
|
|
2684
|
+
}
|
|
2685
|
+
let cur = adapter.getParent(call);
|
|
2686
|
+
while (cur) {
|
|
2687
|
+
const type = adapter.getNodeType(cur);
|
|
2688
|
+
if (type === 'call_expression') {
|
|
2689
|
+
const memberExpr = findMemberCallee(cur, adapter);
|
|
2690
|
+
const name = memberExpr ? memberPropertyName(memberExpr, adapter, sourceCode) : null;
|
|
2691
|
+
if (name && EAGER_DB_METHODS.has(name) && !isPromiseAllMember(memberExpr, adapter, sourceCode)) {
|
|
2692
|
+
return cur;
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
if (type === 'expression_statement' ||
|
|
2696
|
+
type === 'variable_declarator' ||
|
|
2697
|
+
type === 'return_statement' ||
|
|
2698
|
+
type === 'lexical_declaration' ||
|
|
2699
|
+
type === 'for_statement' ||
|
|
2700
|
+
type === 'for_in_statement' ||
|
|
2701
|
+
type === 'while_statement' ||
|
|
2702
|
+
type === 'statement_block' ||
|
|
2703
|
+
type === 'block') {
|
|
2704
|
+
break;
|
|
2705
|
+
}
|
|
2706
|
+
cur = adapter.getParent(cur);
|
|
2707
|
+
}
|
|
2708
|
+
return null;
|
|
2709
|
+
}
|
|
2710
|
+
/**
|
|
2711
|
+
* Classify a call_expression as an LLM/agent invocation via three unambiguous
|
|
2712
|
+
* signals: a provider/SDK namespace in the callee text, an LLM action verb in the
|
|
2713
|
+
* callee text, or a model-client identifier passed as an argument / used as the
|
|
2714
|
+
* member-callee object.
|
|
2715
|
+
*/
|
|
2716
|
+
function isLlmCallNode(node, adapter, sourceCode) {
|
|
2717
|
+
const children = adapter.getChildren(node);
|
|
2718
|
+
const callee = children.find(c => adapter.getNodeType(c) !== 'arguments');
|
|
2719
|
+
if (callee) {
|
|
2720
|
+
const calleeText = adapter.getNodeText(callee, sourceCode);
|
|
2721
|
+
if (LLM_PROVIDER_RE.test(calleeText) || LLM_ACTION_RE.test(calleeText))
|
|
2722
|
+
return true;
|
|
2723
|
+
// Member callee whose object is an LLM client: `model.chat(…)`, `llm.invoke(…)`.
|
|
2724
|
+
if (adapter.getNodeType(callee) === 'member_expression') {
|
|
2725
|
+
const obj = adapter.getChildren(callee).find(c => adapter.getNodeType(c) === 'identifier');
|
|
2726
|
+
if (obj && LLM_CLIENT_ARG_NAMES.has(adapter.getNodeText(obj, sourceCode)))
|
|
2727
|
+
return true;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
// Argument signal: an LLM client identifier passed into the call, e.g.
|
|
2731
|
+
// `extractBuildsFromCorpus(db, model, …)`.
|
|
2732
|
+
const args = children.find(c => adapter.getNodeType(c) === 'arguments');
|
|
1531
2733
|
if (args) {
|
|
1532
2734
|
for (const arg of adapter.getChildren(args)) {
|
|
1533
2735
|
if (adapter.getNodeType(arg) === 'identifier' &&
|
|
@@ -1544,48 +2746,31 @@ function isLlmCallNode(node, adapter, sourceCode) {
|
|
|
1544
2746
|
}
|
|
1545
2747
|
return false;
|
|
1546
2748
|
}
|
|
1547
|
-
/** True when `node` is a function call whose callee is DB-provenanced. */
|
|
1548
|
-
function isDBProvenancedFunctionCall(node, adapter, sourceCode, provenanceContext) {
|
|
1549
|
-
return isFunctionCall(node, adapter) &&
|
|
1550
|
-
isDBProvenanced(node, { adapter, sourceCode, context: provenanceContext, methods: DB_CALL_METHODS });
|
|
1551
|
-
}
|
|
1552
2749
|
/**
|
|
1553
2750
|
* R4.1: Determine if a node is a database call expression.
|
|
1554
|
-
* Spec 21:
|
|
1555
|
-
*
|
|
1556
|
-
*
|
|
2751
|
+
* Spec 21: provenance-based detection (conjunctive guard — never name alone).
|
|
2752
|
+
*
|
|
2753
|
+
* Spec 70 R3/R4: handle-ness is decided once, by `identifyHandle` (via
|
|
2754
|
+
* `handleVerdictForCall`); only a proven `handle` — a parsed SQL argument (R3)
|
|
2755
|
+
* or a package in the manifest (R4) — is a DB node for the loop walk. `unproven`
|
|
2756
|
+
* is `cannot-fire` (visible, never a finding), so a `map.delete()` / `set.join()`
|
|
2757
|
+
* on an unresolved receiver does not read as a DB call inside a loop.
|
|
2758
|
+
*
|
|
2759
|
+
* The legacy `dbPatterns` substring fallback (a call whose *text* contained
|
|
2760
|
+
* `select`/`insert`/`delete`/…) is deleted, not fixed: `buildDataAccessScan`
|
|
2761
|
+
* always provides a provenance context (`mode` defaults to `'hybrid'`), so the
|
|
2762
|
+
* no-context / `names`-mode arm was unreachable in a default run. Without
|
|
2763
|
+
* provenance there is no honest handle verdict, so the node is not a DB call.
|
|
2764
|
+
*
|
|
2765
|
+
* @param node the candidate call node to classify
|
|
2766
|
+
* @param scan the data-access scan context (adapter, source, provenance)
|
|
2767
|
+
* @returns true when the node's handle verdict is `handle`
|
|
1557
2768
|
*/
|
|
1558
|
-
function isDbCallNode(node,
|
|
1559
|
-
|
|
2769
|
+
export function isDbCallNode(node, scan) {
|
|
2770
|
+
const { provenanceContext } = scan;
|
|
1560
2771
|
if (provenanceContext && provenanceContext.mode !== 'names') {
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
return true;
|
|
1564
|
-
}
|
|
1565
|
-
// Spec 17 R2: Template literal is a DB node only when it sits
|
|
1566
|
-
// inside a DB-provenanced call's arguments — no content scan.
|
|
1567
|
-
if (isTemplateLiteral(node, adapter)) {
|
|
1568
|
-
const parent = adapter.getParent(node);
|
|
1569
|
-
if (parent && adapter.getNodeType(parent) === 'arguments') {
|
|
1570
|
-
const callExpr = adapter.getParent(parent);
|
|
1571
|
-
if (callExpr && adapter.getNodeType(callExpr) === 'call_expression') {
|
|
1572
|
-
return isDBProvenancedFunctionCall(callExpr, adapter, sourceCode, provenanceContext);
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
return false;
|
|
1576
|
-
}
|
|
1577
|
-
return false;
|
|
1578
|
-
}
|
|
1579
|
-
// Legacy fallback: name-based matching for names mode / no context.
|
|
1580
|
-
// Template-literal content scanning is removed (Spec 17 R2) — without
|
|
1581
|
-
// provenance context, we can't determine if a template literal is SQL;
|
|
1582
|
-
// function-call-based SQL detection still works via dbPatterns match.
|
|
1583
|
-
const nodeText = adapter.getNodeText(node, sourceCode);
|
|
1584
|
-
if (isFunctionCall(node, adapter)) {
|
|
1585
|
-
const dbPatterns = ['select', 'insert', 'update', 'delete', 'from', 'where', 'execute', 'query', 'find', 'aggregate', 'count', 'distinct'];
|
|
1586
|
-
if (dbPatterns.some(pattern => nodeText.toLowerCase().includes(pattern))) {
|
|
1587
|
-
return true;
|
|
1588
|
-
}
|
|
2772
|
+
const verdict = handleVerdictForCall(node, scan);
|
|
2773
|
+
return verdict !== null && verdict.kind === 'handle';
|
|
1589
2774
|
}
|
|
1590
2775
|
return false;
|
|
1591
2776
|
}
|
|
@@ -1597,7 +2782,7 @@ function isDbCallNode(node, adapter, sourceCode, provenanceContext) {
|
|
|
1597
2782
|
* - for / while / do loops via adapter.isLoop()
|
|
1598
2783
|
* - .forEach / .map / .filter callbacks via AST pattern matching
|
|
1599
2784
|
*/
|
|
1600
|
-
function findEnclosingLoop(node, adapter) {
|
|
2785
|
+
function findEnclosingLoop(node, adapter, sourceCode) {
|
|
1601
2786
|
let current = node;
|
|
1602
2787
|
let depth = 0;
|
|
1603
2788
|
const foundLoops = [];
|
|
@@ -1610,7 +2795,7 @@ function findEnclosingLoop(node, adapter) {
|
|
|
1610
2795
|
foundLoops.push(parent);
|
|
1611
2796
|
}
|
|
1612
2797
|
// Check for iterator callbacks (.forEach, .map, .filter, etc.)
|
|
1613
|
-
if (isIteratorCallback(parent, adapter)) {
|
|
2798
|
+
if (isIteratorCallback(parent, current, adapter, sourceCode)) {
|
|
1614
2799
|
foundLoops.push(parent);
|
|
1615
2800
|
}
|
|
1616
2801
|
current = parent;
|
|
@@ -1628,47 +2813,46 @@ function findEnclosingLoop(node, adapter) {
|
|
|
1628
2813
|
* R4.1: Check if a node is a call_expression invoking an iterator method
|
|
1629
2814
|
* (.forEach, .map, .filter, .reduce, .some, .every) — these create
|
|
1630
2815
|
* implicit loops where a DB query inside the callback is an N+1 risk.
|
|
2816
|
+
*
|
|
2817
|
+
* `child` is the node one level below `node` on the walk-up path from the query.
|
|
2818
|
+
* An iterator call only encloses the query when the query sits *inside the
|
|
2819
|
+
* callback argument* (an arrow/function expression), not when the query is the
|
|
2820
|
+
* *subject* the method is called on. `this.db.prepare(sql).all().map(r => …)`
|
|
2821
|
+
* runs the query once and maps the rows in memory — a single query, not an N+1
|
|
2822
|
+
* — yet without this check the `.map` call would read as an enclosing loop
|
|
2823
|
+
* because the query is a descendant of the call expression. Only a query inside
|
|
2824
|
+
* the callback body executes once per iteration.
|
|
1631
2825
|
*/
|
|
1632
|
-
function isIteratorCallback(node, adapter) {
|
|
2826
|
+
function isIteratorCallback(node, child, adapter, sourceCode) {
|
|
1633
2827
|
// Must be a call_expression
|
|
1634
|
-
if (node
|
|
2828
|
+
if (adapter.getNodeType(node) !== 'call_expression')
|
|
1635
2829
|
return false;
|
|
1636
2830
|
// Callee must be a member_expression whose property matches iterator method names
|
|
1637
2831
|
const children = adapter.getChildren(node);
|
|
1638
|
-
const callee = children.find(c => c
|
|
2832
|
+
const callee = children.find(c => adapter.getNodeType(c) === 'member_expression');
|
|
1639
2833
|
if (!callee)
|
|
1640
2834
|
return false;
|
|
1641
2835
|
const calleeChildren = adapter.getChildren(callee);
|
|
1642
|
-
const propertyNode = calleeChildren.find(c => c
|
|
2836
|
+
const propertyNode = calleeChildren.find(c => adapter.getNodeType(c) === 'property_identifier' || adapter.getNodeType(c) === 'string');
|
|
1643
2837
|
if (!propertyNode)
|
|
1644
2838
|
return false;
|
|
1645
|
-
const methodName = adapter.getNodeType(propertyNode) === 'property_identifier'
|
|
1646
|
-
? propertyNode.text ?? adapter.getNodeText(propertyNode, '')
|
|
1647
|
-
: '';
|
|
1648
|
-
// Normalize: the method name might come from the node type or need text extraction
|
|
1649
2839
|
const iteratorMethods = ['forEach', 'map', 'filter', 'reduce', 'some', 'every', 'find', 'findIndex', 'flatMap'];
|
|
1650
|
-
//
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
//
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
//
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
if (child.name)
|
|
1667
|
-
return child.name;
|
|
1668
|
-
if (child.text)
|
|
1669
|
-
return child.text;
|
|
1670
|
-
}
|
|
1671
|
-
return '';
|
|
2840
|
+
// The property text must come from the source slice (web-tree-sitter nodes
|
|
2841
|
+
// carry no `.text`/`.name` payload; `getNodeText` with the real source is the
|
|
2842
|
+
// established extraction — cf. `memberPropertyName`). Passing an empty string
|
|
2843
|
+
// here returned '' for every node, so iterator callbacks were never matched.
|
|
2844
|
+
const propText = adapter.getNodeText(propertyNode, sourceCode);
|
|
2845
|
+
if (!iteratorMethods.includes(propText))
|
|
2846
|
+
return false;
|
|
2847
|
+
// The direct child on the path from the query must be the call's `arguments`
|
|
2848
|
+
// list (the callback), not the callee/subject the method is invoked on. The
|
|
2849
|
+
// child is the immediate descendant of the call_expression that the query sits
|
|
2850
|
+
// under: `arguments` when the query is inside the callback, the `member_expression`
|
|
2851
|
+
// callee when the query is the *subject* (`db.prepare(sql).all().map(r => …)`
|
|
2852
|
+
// runs once and maps rows in memory). Checking the child's node type directly —
|
|
2853
|
+
// rather than the callback function's — is what survives the `arguments` node
|
|
2854
|
+
// tree-sitter inserts between the call and its arrow/function argument.
|
|
2855
|
+
return adapter.getNodeType(child) === 'arguments';
|
|
1672
2856
|
}
|
|
1673
2857
|
/**
|
|
1674
2858
|
* The enclosing-function identity used for stable fingerprint symbols. The
|
|
@@ -1676,74 +2860,508 @@ function getPropertyName(node, adapter) {
|
|
|
1676
2860
|
* definition lives in codeAnalysis.ts (Spec 61 Amendment A).
|
|
1677
2861
|
*/
|
|
1678
2862
|
const enclosingIdentity = (node, adapter, filePath) => functionIdentityLabel(findEnclosingFunctionIdentity(node, adapter, filePath));
|
|
2863
|
+
/** Build the shared data-access scan context (config + provenance + imports). */
|
|
1679
2864
|
/**
|
|
1680
|
-
*
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
return
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
//
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
2865
|
+
* Build the TypeScript binding environment `identifyHandle` reads for declaration
|
|
2866
|
+
* resolution, keyed to the file's own imports/declarations plus the within-file
|
|
2867
|
+
* provenance set. Absent (undefined) for Go, which resolves cross-file and has no
|
|
2868
|
+
* `RootResolutionEnv`.
|
|
2869
|
+
*/
|
|
2870
|
+
function buildHandleEnv(ast, adapter, sourceCode, provenanceContext) {
|
|
2871
|
+
if (adapter.name === 'go')
|
|
2872
|
+
return undefined;
|
|
2873
|
+
return {
|
|
2874
|
+
provenance: provenanceContext.dbProvenanced,
|
|
2875
|
+
bindings: buildBindingEnv(ast, adapter, sourceCode),
|
|
2876
|
+
interfaceFields: extractInterfaceFields(ast, adapter, sourceCode),
|
|
2877
|
+
adapter,
|
|
2878
|
+
sourceCode,
|
|
2879
|
+
};
|
|
2880
|
+
}
|
|
2881
|
+
/** Build the Go binding environment `identifyHandle` reads for Go declaration
|
|
2882
|
+
* resolution. The provenance seed (cross-file `db *sql.DB` resolution) is what
|
|
2883
|
+
* proves `handle`; bindings + imports let `classifyGoRootIdentifier` dispose a
|
|
2884
|
+
* non-seeded receiver (`unproven` for an unrecognized package, `not-handle` for
|
|
2885
|
+
* a non-DB binding). Absent (undefined) for non-Go formats. */
|
|
2886
|
+
function buildGoEnv(ast, adapter, sourceCode, provenanceContext) {
|
|
2887
|
+
if (adapter.name !== 'go')
|
|
2888
|
+
return undefined;
|
|
2889
|
+
return {
|
|
2890
|
+
provenance: provenanceContext.dbProvenanced,
|
|
2891
|
+
bindings: buildGoBindingEnv(ast, adapter, sourceCode),
|
|
2892
|
+
imports: buildGoImportMap(ast, adapter),
|
|
2893
|
+
adapter,
|
|
2894
|
+
sourceCode,
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
function buildDataAccessScan(ast, adapter, sourceCode, config) {
|
|
2898
|
+
const finalConfig = { ...DEFAULT_DATA_ACCESS_CONFIG, ...config };
|
|
2899
|
+
const detectionMode = finalConfig.detection?.mode ?? 'hybrid';
|
|
2900
|
+
const seedProvenance = config?._receiverProvenance;
|
|
2901
|
+
const provenanceContext = buildProvenanceContext(ast, adapter, sourceCode, {
|
|
2902
|
+
mode: detectionMode,
|
|
2903
|
+
dbBindingNames: finalConfig.dbBindingNames,
|
|
2904
|
+
dbWrapperNames: finalConfig.dbWrapperNames,
|
|
2905
|
+
seedProvenance,
|
|
2906
|
+
// Spec 70 criterion 8 — thread the corpus's dialect so R3 proves a receiver
|
|
2907
|
+
// whose (now-deleted) type annotation was its only signal, and so wrapper
|
|
2908
|
+
// detection (detectDbWrappers) sees the proven receiver in helper bodies.
|
|
2909
|
+
sqlDialect: finalConfig.dialect ?? null,
|
|
2910
|
+
});
|
|
2911
|
+
return {
|
|
2912
|
+
adapter,
|
|
2913
|
+
sourceCode,
|
|
2914
|
+
config: finalConfig,
|
|
2915
|
+
ast,
|
|
2916
|
+
provenanceContext,
|
|
2917
|
+
handleEnv: buildHandleEnv(ast, adapter, sourceCode, provenanceContext),
|
|
2918
|
+
goEnv: buildGoEnv(ast, adapter, sourceCode, provenanceContext),
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
/**
|
|
2922
|
+
* Spec 68 §3.2 — the `data-access-calls` FileProcessor extraction, factored out
|
|
2923
|
+
* of `analyzeWithFacts` so the phase-model producer can obtain the per-file
|
|
2924
|
+
* resolved calls synchronously (the analyzer method is `async` but does no real
|
|
2925
|
+
* awaits — its body is entirely synchronous). Returns the `DatabaseCall[]` the
|
|
2926
|
+
* missing-org-filter / unfiltered-query / sql-injection-risk rules read, with no
|
|
2927
|
+
* violation production and no side effects. Config is the §10 tuning surface;
|
|
2928
|
+
* omitted here, the extraction runs on {@link DEFAULT_DATA_ACCESS_CONFIG}.
|
|
2929
|
+
* @param ast The parsed file AST.
|
|
2930
|
+
* @param adapter The language adapter for the file's syntax.
|
|
2931
|
+
* @param sourceCode The raw source text.
|
|
2932
|
+
* @param config The data-access analyzer config (defaults when omitted).
|
|
2933
|
+
* @returns The database calls extracted from the file.
|
|
2934
|
+
*/
|
|
2935
|
+
export function extractDataAccessCalls(ast, adapter, sourceCode, config) {
|
|
2936
|
+
return extractDatabaseCalls(ast, buildDataAccessScan(ast, adapter, sourceCode, config));
|
|
2937
|
+
}
|
|
2938
|
+
/**
|
|
2939
|
+
* Extract the loop-query candidates (loops whose body issues a DB call) as the
|
|
2940
|
+
* serializable `LoopQueryCandidate[]` the `loop-queries` producer projects. The
|
|
2941
|
+
* candidate set is exactly what `checkLoopQueries` reduces to findings, computed
|
|
2942
|
+
* on the same provenance context — so detection parity holds by construction,
|
|
2943
|
+
* not by re-implementation. Config is the §10 tuning surface; omitted here, the
|
|
2944
|
+
* extraction runs on {@link DEFAULT_DATA_ACCESS_CONFIG}.
|
|
2945
|
+
* @param ast The parsed file AST.
|
|
2946
|
+
* @param adapter The language adapter for the file's syntax.
|
|
2947
|
+
* @param sourceCode The raw source text.
|
|
2948
|
+
* @param config The data-access analyzer config (defaults when omitted).
|
|
2949
|
+
* @returns The loop-query candidates extracted from the file.
|
|
2950
|
+
*/
|
|
2951
|
+
export function extractLoopQueries(ast, adapter, sourceCode, config) {
|
|
2952
|
+
return collectLoopQueryCandidates(ast, buildDataAccessScan(ast, adapter, sourceCode, config));
|
|
2953
|
+
}
|
|
2954
|
+
// ── Spec 70 Item 4 (step 3) — raw candidate extractors ───────────────────────
|
|
2955
|
+
//
|
|
2956
|
+
// The two remaining receiver consumers (`data-access-calls`, `loop-queries`) are
|
|
2957
|
+
// converted to corpus producers: while the AST lives, these extractors project the
|
|
2958
|
+
// *provenance-free* half of `buildDatabaseCall` / `collectLoopQueryCandidates` into
|
|
2959
|
+
// serializable candidates, and the corpus producers re-fold the provenance-dependent
|
|
2960
|
+
// half (the `identifyHandle` verdict → admission + injection-risk gate, the site
|
|
2961
|
+
// dialect → the SQL parse, and the loop dedup) over the re-derived `dbProvenanced`.
|
|
2962
|
+
// They run on an EMPTY-provenance scan so `checkQuerySecurity`'s wrapper arm sees
|
|
2963
|
+
// only `config.dbWrapperNames` (the learned wrappers are corrected corpus-side).
|
|
2964
|
+
/** Build a scan with an empty provenance context — the raw producer's discovery and
|
|
2965
|
+
* static-security arms must not read the cross-file seed (which only the corpus
|
|
2966
|
+
* producer sees), so every provenance-dependent signal is captured as an identity
|
|
2967
|
+
* and re-folded later. `handleEnv`/`goEnv` still build their provenance-free
|
|
2968
|
+
* bindings/imports, which the structural discovery predicates read. */
|
|
2969
|
+
function buildCandidateScan(ast, adapter, sourceCode, config) {
|
|
2970
|
+
const finalConfig = { ...DEFAULT_DATA_ACCESS_CONFIG, ...config };
|
|
2971
|
+
const provenanceContext = {
|
|
2972
|
+
dbProvenanced: new Map(),
|
|
2973
|
+
validatorProvenanced: new Map(),
|
|
2974
|
+
mode: finalConfig.detection?.mode ?? 'hybrid',
|
|
2975
|
+
dbActivity: new Set(),
|
|
2976
|
+
};
|
|
2977
|
+
return {
|
|
2978
|
+
adapter,
|
|
2979
|
+
sourceCode,
|
|
2980
|
+
config: finalConfig,
|
|
2981
|
+
ast,
|
|
2982
|
+
provenanceContext,
|
|
2983
|
+
handleEnv: buildHandleEnv(ast, adapter, sourceCode, provenanceContext),
|
|
2984
|
+
goEnv: buildGoEnv(ast, adapter, sourceCode, provenanceContext),
|
|
2985
|
+
};
|
|
2986
|
+
}
|
|
2987
|
+
// ── Structural discovery (empty-provenance supersets) ────────────────────────
|
|
2988
|
+
/** True when `node` is a bare-identifier call whose callee name has any binding —
|
|
2989
|
+
* the raw-side superset of `handleVerdictForCall`'s identifier arm. A provenanced
|
|
2990
|
+
* wrapper, a type-annotated handle, or a propagated local are all *bound*, so a
|
|
2991
|
+
* name with no binding is provably not a handle candidate and is dropped here. */
|
|
2992
|
+
function isBoundIdentifierCall(node, scan) {
|
|
2993
|
+
const { adapter, sourceCode, handleEnv } = scan;
|
|
2994
|
+
if (!handleEnv)
|
|
2995
|
+
return false;
|
|
2996
|
+
if (adapter.getNodeType(node) !== 'call_expression')
|
|
2997
|
+
return false;
|
|
2998
|
+
const callee = getCallExpressionCallee(node, adapter);
|
|
2999
|
+
if (!callee || callee.type !== 'identifier')
|
|
3000
|
+
return false;
|
|
3001
|
+
const name = adapter.getNodeText(callee, sourceCode);
|
|
3002
|
+
return !!name && handleEnv.bindings.has(name);
|
|
3003
|
+
}
|
|
3004
|
+
/** True when `node` is a member/selector call whose receiver root is DB-shaped
|
|
3005
|
+
* (handle or unproven) or that carries a static SQL argument — the structural
|
|
3006
|
+
* superset of `handleVerdictForCall`'s member arm (and Go's selector arm, which
|
|
3007
|
+
* is the same shape). The package discriminant — not the method name — admits a
|
|
3008
|
+
* receiver (`join` is `Array.prototype.join` and also `SQL JOIN`); the empty
|
|
3009
|
+
* provenance of the candidate scan makes more roots `unproven`, so this stays a
|
|
3010
|
+
* strict superset of the corpus-side re-fold. */
|
|
3011
|
+
function isMemberShapeCall(node, scan) {
|
|
3012
|
+
const { adapter, sourceCode, handleEnv, goEnv } = scan;
|
|
3013
|
+
if (adapter.getNodeType(node) !== 'call_expression')
|
|
3014
|
+
return false;
|
|
3015
|
+
const callee = getCallExpressionCallee(node, adapter);
|
|
3016
|
+
if (!callee || (callee.type !== 'member_expression' && callee.type !== 'selector_expression'))
|
|
3017
|
+
return false;
|
|
3018
|
+
const root = resolveReceiverRoot(callee, adapter, sourceCode);
|
|
3019
|
+
if (root === null)
|
|
3020
|
+
return false;
|
|
3021
|
+
const sqlArg = adapter.name === 'go'
|
|
3022
|
+
? extractGoStaticSql(node, adapter, sourceCode, goEnv)
|
|
3023
|
+
: extractStaticSql(node, adapter, sourceCode);
|
|
3024
|
+
if (sqlArg === null) {
|
|
3025
|
+
return adapter.name === 'go'
|
|
3026
|
+
? !!goEnv && classifyGoRootIdentifier(root, goEnv) !== 'not-handle'
|
|
3027
|
+
: !!handleEnv && isDbShapedRoot(root, handleEnv, { thisField: receiverIsThisRooted(callee, adapter) });
|
|
3028
|
+
}
|
|
3029
|
+
return true;
|
|
3030
|
+
}
|
|
3031
|
+
/** True when `node` is a template literal whose enclosing call is a bound-identifier
|
|
3032
|
+
* or member-shape call — the raw-side superset of `handleVerdictForCall`'s template
|
|
3033
|
+
* branch (`callNode = enclosingCallOf(template)`). */
|
|
3034
|
+
function isTemplateOfDbCall(node, scan) {
|
|
3035
|
+
const { adapter, sourceCode, config } = scan;
|
|
3036
|
+
if (!isTemplateLiteral(node, adapter))
|
|
3037
|
+
return false;
|
|
3038
|
+
const callNode = enclosingCallOf(node, adapter);
|
|
3039
|
+
if (!callNode)
|
|
3040
|
+
return false;
|
|
3041
|
+
// A tagged template (`this.sql`…``, `sql`…``) carries its template as its own
|
|
3042
|
+
// body, not as a template *argument* to a DB call. The tag is already a
|
|
3043
|
+
// candidate via `isTaggedTemplateSqlCall`; re-admitting the body here would
|
|
3044
|
+
// double-discover the site and let the body (method `unknown`) shadow the tag
|
|
3045
|
+
// (method `sql`) in line dedup.
|
|
3046
|
+
const tagNames = config.sqlTagNames ?? SQL_TAG_NAMES;
|
|
3047
|
+
if (isTaggedTemplateSqlCall(callNode, adapter, sourceCode, tagNames))
|
|
3048
|
+
return false;
|
|
3049
|
+
return isBoundIdentifierCall(callNode, scan) || isMemberShapeCall(callNode, scan);
|
|
3050
|
+
}
|
|
3051
|
+
/** The broadened data-access discovery — a strict superset of `isDbCallCandidate`
|
|
3052
|
+
* over the same pre-order. The three shape arms (query-builder, tagged template,
|
|
3053
|
+
* variable assignment) are provenance-free; the identifier/member/template arms
|
|
3054
|
+
* cover every `handleVerdictForCall` shape with the empty seed. */
|
|
3055
|
+
function isBroadenedDataAccessCandidate(node, scan, tagNames) {
|
|
3056
|
+
const { adapter, sourceCode } = scan;
|
|
3057
|
+
if (isQueryBuilderShape(node, adapter, sourceCode))
|
|
3058
|
+
return true;
|
|
3059
|
+
if (isTaggedTemplateSqlCall(node, adapter, sourceCode, tagNames))
|
|
3060
|
+
return true;
|
|
3061
|
+
if (isVariableAssignment(node, adapter) && extractStaticSql(node, adapter, sourceCode) !== null)
|
|
3062
|
+
return true;
|
|
3063
|
+
if (isBoundIdentifierCall(node, scan))
|
|
3064
|
+
return true;
|
|
3065
|
+
if (isMemberShapeCall(node, scan))
|
|
3066
|
+
return true;
|
|
3067
|
+
if (isTemplateOfDbCall(node, scan))
|
|
3068
|
+
return true;
|
|
3069
|
+
return false;
|
|
3070
|
+
}
|
|
3071
|
+
/** The broadened loop-query discovery — a strict superset of `isDbCallNode` (which
|
|
3072
|
+
* is `handleVerdictForCall(node).kind === 'handle'`). The template-literal branch is
|
|
3073
|
+
* skipped later (as in `collectLoopQueryCandidates`), so only the two structural
|
|
3074
|
+
* call arms remain. */
|
|
3075
|
+
function isBroadenedLoopCandidate(node, scan) {
|
|
3076
|
+
return isBoundIdentifierCall(node, scan) || isMemberShapeCall(node, scan);
|
|
3077
|
+
}
|
|
3078
|
+
const NULL_HANDLE_IDENTITY = {
|
|
3079
|
+
calleeType: null, name: null, root: null, receiver: null, method: null,
|
|
3080
|
+
thisField: false, thisHeritage: null, sqlArg: null, siteReceiver: null, scope: 0,
|
|
3081
|
+
};
|
|
3082
|
+
/** Project `handleVerdictForCall`'s structural gates (without `identifyHandle`) into
|
|
3083
|
+
* the identity the corpus producer re-folds. Mirrors the TS identifier/member arms
|
|
3084
|
+
* and the Go selector arm exactly — same method/root gates, same template → enclosing
|
|
3085
|
+
* call resolution, same `extractStaticSql(callNode)` argument. */
|
|
3086
|
+
function handleCallSiteIdentity(node, scan) {
|
|
3087
|
+
const { adapter, sourceCode } = scan;
|
|
3088
|
+
if (adapter.name === 'go') {
|
|
3089
|
+
// Go selector arm (goHandleVerdictForCall) — node is always a call_expression.
|
|
3090
|
+
if (!isFunctionCall(node, adapter))
|
|
3091
|
+
return NULL_HANDLE_IDENTITY;
|
|
3092
|
+
const callee = getCallExpressionCallee(node, adapter);
|
|
3093
|
+
if (!callee || callee.type !== 'selector_expression')
|
|
3094
|
+
return NULL_HANDLE_IDENTITY;
|
|
3095
|
+
const method = extractMemberExpressionProperty(callee, adapter, sourceCode);
|
|
3096
|
+
if (!method)
|
|
3097
|
+
return NULL_HANDLE_IDENTITY;
|
|
3098
|
+
const root = resolveReceiverRoot(callee, adapter, sourceCode);
|
|
3099
|
+
if (root === null)
|
|
3100
|
+
return NULL_HANDLE_IDENTITY;
|
|
3101
|
+
const sqlArg = extractGoStaticSql(node, adapter, sourceCode, scan.goEnv);
|
|
3102
|
+
if (sqlArg === null && (!scan.goEnv || classifyGoRootIdentifier(root, scan.goEnv) === 'not-handle')) {
|
|
3103
|
+
return NULL_HANDLE_IDENTITY;
|
|
1744
3104
|
}
|
|
1745
|
-
|
|
1746
|
-
return {
|
|
3105
|
+
const receiver = getMemberExpressionReceiver(callee, adapter, sourceCode) ?? root;
|
|
3106
|
+
return {
|
|
3107
|
+
calleeType: 'member', name: null, root, receiver, method, thisField: false,
|
|
3108
|
+
thisHeritage: null,
|
|
3109
|
+
sqlArg,
|
|
3110
|
+
siteReceiver: getMemberExpressionReceiver(callee, adapter, sourceCode),
|
|
3111
|
+
scope: 0,
|
|
3112
|
+
};
|
|
3113
|
+
}
|
|
3114
|
+
const callNode = isTemplateLiteral(node, adapter) ? enclosingCallOf(node, adapter) : node;
|
|
3115
|
+
if (!callNode || !isFunctionCall(callNode, adapter))
|
|
3116
|
+
return NULL_HANDLE_IDENTITY;
|
|
3117
|
+
if (callNode.type === 'new_expression')
|
|
3118
|
+
return NULL_HANDLE_IDENTITY;
|
|
3119
|
+
const callee = getCallExpressionCallee(callNode, adapter);
|
|
3120
|
+
if (!callee)
|
|
3121
|
+
return NULL_HANDLE_IDENTITY;
|
|
3122
|
+
if (callee.type === 'identifier') {
|
|
3123
|
+
const name = adapter.getNodeText(callee, sourceCode);
|
|
3124
|
+
if (!name)
|
|
3125
|
+
return NULL_HANDLE_IDENTITY;
|
|
3126
|
+
return {
|
|
3127
|
+
calleeType: 'identifier', name, root: name, receiver: name, method: name,
|
|
3128
|
+
thisField: false,
|
|
3129
|
+
thisHeritage: null,
|
|
3130
|
+
sqlArg: extractStaticSql(callNode, adapter, sourceCode),
|
|
3131
|
+
siteReceiver: null,
|
|
3132
|
+
scope: findEnclosingFunctionNode(callNode, adapter)?.range[0] ?? 0,
|
|
3133
|
+
};
|
|
3134
|
+
}
|
|
3135
|
+
if (callee.type !== 'member_expression' && callee.type !== 'selector_expression') {
|
|
3136
|
+
return NULL_HANDLE_IDENTITY;
|
|
3137
|
+
}
|
|
3138
|
+
const method = extractMemberExpressionProperty(callee, adapter, sourceCode);
|
|
3139
|
+
if (!method)
|
|
3140
|
+
return NULL_HANDLE_IDENTITY;
|
|
3141
|
+
const root = resolveReceiverRoot(callee, adapter, sourceCode);
|
|
3142
|
+
if (root === null)
|
|
3143
|
+
return NULL_HANDLE_IDENTITY;
|
|
3144
|
+
const sqlArg = extractStaticSql(callNode, adapter, sourceCode);
|
|
3145
|
+
const thisRooted = receiverIsThisRooted(callee, adapter);
|
|
3146
|
+
if (sqlArg === null && (!scan.handleEnv || !isDbShapedRoot(root, scan.handleEnv, { thisField: thisRooted }))) {
|
|
3147
|
+
return NULL_HANDLE_IDENTITY;
|
|
3148
|
+
}
|
|
3149
|
+
const receiver = getMemberExpressionReceiver(callee, adapter, sourceCode) ?? root;
|
|
3150
|
+
// Spec 70 Q3 — a `this.<root>` receiver resolves to the enclosing class's
|
|
3151
|
+
// base-class field type (`extends WorkflowEntrypoint<Env>` → `this.env` is
|
|
3152
|
+
// `Env`), re-folded through the member path by the same heritage seam as the
|
|
3153
|
+
// build side. The raw `extends` text is carried, resolved at fold time; null
|
|
3154
|
+
// when not `this`-rooted or the class has no base class.
|
|
3155
|
+
const thisHeritage = thisRooted
|
|
3156
|
+
? findEnclosingClassHeritage(scan.ast, adapter, callNode, sourceCode)
|
|
3157
|
+
: null;
|
|
3158
|
+
return {
|
|
3159
|
+
calleeType: 'member', name: null, root, receiver, method,
|
|
3160
|
+
thisField: thisRooted,
|
|
3161
|
+
thisHeritage,
|
|
3162
|
+
sqlArg,
|
|
3163
|
+
siteReceiver: getMemberExpressionReceiver(callee, adapter, sourceCode),
|
|
3164
|
+
scope: findEnclosingFunctionNode(callNode, adapter)?.range[0] ?? 0,
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
3167
|
+
function siteDialectIdentity(node, scan) {
|
|
3168
|
+
const { adapter, sourceCode } = scan;
|
|
3169
|
+
const callee = getCallExpressionCallee(node, adapter);
|
|
3170
|
+
if (!callee)
|
|
3171
|
+
return { calleeType: null, name: null, receiver: null };
|
|
3172
|
+
if (callee.type === 'identifier') {
|
|
3173
|
+
return { calleeType: 'identifier', name: adapter.getNodeText(callee, sourceCode) || null, receiver: null };
|
|
3174
|
+
}
|
|
3175
|
+
if (callee.type === 'member_expression' || callee.type === 'selector_expression') {
|
|
3176
|
+
return { calleeType: 'member', name: null, receiver: getMemberExpressionReceiver(callee, adapter, sourceCode) };
|
|
3177
|
+
}
|
|
3178
|
+
return { calleeType: null, name: null, receiver: null };
|
|
3179
|
+
}
|
|
3180
|
+
/** The bare-identifier callee name of a template's enclosing call, only when the
|
|
3181
|
+
* call carries ≥2 real args — the `isWrapperFunctionWithBindParams` shape's identity
|
|
3182
|
+
* (arm 4). Captured raw-side so the corpus producer can correct the static-security
|
|
3183
|
+
* result once it learns the name as a DB wrapper (`reason: 'wrapper'`). */
|
|
3184
|
+
function wrapperCalleeNameForTemplate(node, adapter, sourceCode) {
|
|
3185
|
+
if (adapter.getNodeType(node) !== 'template_string')
|
|
3186
|
+
return null;
|
|
3187
|
+
const args = adapter.getParent(node);
|
|
3188
|
+
if (!args || adapter.getNodeType(args) !== 'arguments')
|
|
3189
|
+
return null;
|
|
3190
|
+
const call = adapter.getParent(args);
|
|
3191
|
+
if (!call || adapter.getNodeType(call) !== 'call_expression')
|
|
3192
|
+
return null;
|
|
3193
|
+
const callee = getCallExpressionCallee(call, adapter);
|
|
3194
|
+
if (!callee || adapter.getNodeType(callee) !== 'identifier')
|
|
3195
|
+
return null;
|
|
3196
|
+
const realArgs = adapter.getChildren(args).filter((c) => !['(', ')', ','].includes(adapter.getNodeType(c)));
|
|
3197
|
+
if (realArgs.length < 2)
|
|
3198
|
+
return null;
|
|
3199
|
+
return adapter.getNodeText(callee, sourceCode) || null;
|
|
3200
|
+
}
|
|
3201
|
+
// ── The `data-access-calls-candidates` extractor ─────────────────────────────
|
|
3202
|
+
/** Project one broadened candidate into the raw, provenance-free `DataAccessCallCandidate`
|
|
3203
|
+
* the corpus `data-access-calls` producer re-folds. The static-security arms run here
|
|
3204
|
+
* on the empty-provenance scan; only `arm4CalleeName` is corrected corpus-side. */
|
|
3205
|
+
function buildDataAccessCallCandidate(node, ast, scan) {
|
|
3206
|
+
const { adapter, sourceCode, config } = scan;
|
|
3207
|
+
const nodeText = stripComments(adapter.getNodeText(node, sourceCode));
|
|
3208
|
+
if (!nodeText || nodeText.trim().length < 10)
|
|
3209
|
+
return null;
|
|
3210
|
+
const sqlArg = adapter.name === 'go'
|
|
3211
|
+
? extractGoStaticSql(node, adapter, sourceCode, scan.goEnv)
|
|
3212
|
+
: extractStaticSql(node, adapter, sourceCode);
|
|
3213
|
+
const isOrmCall = isOrmPattern(nodeText);
|
|
3214
|
+
const tagNames = config.sqlTagNames ?? SQL_TAG_NAMES;
|
|
3215
|
+
const isTaggedSqlCall = isTaggedTemplateSqlCall(node, adapter, sourceCode, tagNames);
|
|
3216
|
+
const isQueryBuilder = isQueryBuilderShape(node, adapter, sourceCode);
|
|
3217
|
+
const isVariableAssignmentSql = isVariableAssignment(node, adapter) && sqlArg !== null;
|
|
3218
|
+
const isTemplate = isTemplateLiteral(node, adapter);
|
|
3219
|
+
const handle = handleCallSiteIdentity(node, scan);
|
|
3220
|
+
const site = siteDialectIdentity(node, scan);
|
|
3221
|
+
const security = checkQuerySecurity(node, nodeText, ast, scan);
|
|
3222
|
+
return {
|
|
3223
|
+
file: ast.filePath,
|
|
3224
|
+
line: node.location.start.line,
|
|
3225
|
+
column: node.location.start.column,
|
|
3226
|
+
format: adapter.name === 'go' ? 'go' : 'typescript',
|
|
3227
|
+
nodeText,
|
|
3228
|
+
method: extractMethodName(node, adapter, sourceCode),
|
|
3229
|
+
sqlArg,
|
|
3230
|
+
isOrmCall,
|
|
3231
|
+
isTaggedSqlCall,
|
|
3232
|
+
isQueryBuilderShape: isQueryBuilder,
|
|
3233
|
+
isVariableAssignmentSql,
|
|
3234
|
+
isTemplateLiteral: isTemplate,
|
|
3235
|
+
handleCalleeType: handle.calleeType,
|
|
3236
|
+
handleName: handle.name,
|
|
3237
|
+
handleRoot: handle.root,
|
|
3238
|
+
handleReceiver: handle.receiver,
|
|
3239
|
+
handleMethod: handle.method,
|
|
3240
|
+
handleThisField: handle.thisField,
|
|
3241
|
+
handleThisHeritage: handle.thisHeritage,
|
|
3242
|
+
handleSqlArg: handle.sqlArg,
|
|
3243
|
+
handleSiteReceiver: handle.siteReceiver,
|
|
3244
|
+
handleRootScope: handle.scope,
|
|
3245
|
+
skipCallForTemplateArg: shouldSkipCallForTemplateArg(node, adapter),
|
|
3246
|
+
siteCalleeType: site.calleeType,
|
|
3247
|
+
siteName: site.name,
|
|
3248
|
+
siteReceiver: site.receiver,
|
|
3249
|
+
hasOrganizationFilter: hasOrganizationFilter(nodeText, config),
|
|
3250
|
+
enclosingFunction: enclosingIdentity(node, adapter, ast.filePath),
|
|
3251
|
+
resolvedWhere: resolveWhereBinding(node, sourceCode, adapter),
|
|
3252
|
+
ormTables: isOrmCall ? extractOrmTables(node, adapter, sourceCode, config) : [],
|
|
3253
|
+
builderVerb: builderWriteVerb(node, adapter, sourceCode),
|
|
3254
|
+
ormHasFilter: hasOrmFilterShape(node, adapter, sourceCode),
|
|
3255
|
+
staticParameterized: security.parameterized,
|
|
3256
|
+
staticInjectionRisk: security.injectionRisk,
|
|
3257
|
+
staticEscaped: security.escaped,
|
|
3258
|
+
arm4CalleeName: wrapperCalleeNameForTemplate(node, adapter, sourceCode),
|
|
3259
|
+
};
|
|
3260
|
+
}
|
|
3261
|
+
/**
|
|
3262
|
+
* Extract the raw, provenance-free `data-access-calls-candidates` for one file —
|
|
3263
|
+
* the extract half of the corpus `data-access-calls` reduction (Spec 70 Item 4,
|
|
3264
|
+
* step 3). No dedup happens here: the broadened discovery can perturb line-dedup
|
|
3265
|
+
* (a bound bare call and a member call on the same line), so the corpus producer
|
|
3266
|
+
* dedups after re-folding admission. Config is the §10 tuning surface; omitted
|
|
3267
|
+
* here, extraction runs on {@link DEFAULT_DATA_ACCESS_CONFIG}.
|
|
3268
|
+
* @param ast - The file's parsed AST.
|
|
3269
|
+
* @param adapter - The language adapter (drives node discovery + text reads).
|
|
3270
|
+
* @param sourceCode - The file's source text.
|
|
3271
|
+
* @param config - Optional data-access config; defaults to the shared config.
|
|
3272
|
+
* @returns The raw, provenance-free `data-access-calls-candidates`.
|
|
3273
|
+
*/
|
|
3274
|
+
export function extractDataAccessCallCandidates(ast, adapter, sourceCode, config) {
|
|
3275
|
+
const scan = buildCandidateScan(ast, adapter, sourceCode, config);
|
|
3276
|
+
const tagNames = scan.config.sqlTagNames ?? SQL_TAG_NAMES;
|
|
3277
|
+
const nodes = adapter.findNodes(ast, {
|
|
3278
|
+
custom: (node) => isBroadenedDataAccessCandidate(node, scan, tagNames),
|
|
3279
|
+
});
|
|
3280
|
+
const out = [];
|
|
3281
|
+
for (const node of nodes) {
|
|
3282
|
+
const cand = buildDataAccessCallCandidate(node, ast, scan);
|
|
3283
|
+
if (cand)
|
|
3284
|
+
out.push(cand);
|
|
3285
|
+
}
|
|
3286
|
+
return out;
|
|
3287
|
+
}
|
|
3288
|
+
// ── The `loop-query-candidates` extractor ────────────────────────────────────
|
|
3289
|
+
/**
|
|
3290
|
+
* Extract the raw, provenance-free `loop-query-candidates` for one file — the
|
|
3291
|
+
* extract half of the corpus `loop-queries` reduction. Mirrors `collectLoopQueryCandidates`
|
|
3292
|
+
* with the broadened discovery and without the three provenance-dependent steps
|
|
3293
|
+
* (the strict-handle filter, the per-loop dedup, the stable symbol), which the
|
|
3294
|
+
* corpus producer re-folds once `dbProvenanced` is re-derived. The provenance-free
|
|
3295
|
+
* discriminators (statement-construction, sql-string-construction, for-of-iterable,
|
|
3296
|
+
* hoisted-reuse, batch-argument, LLM/queue suppression) already ran here.
|
|
3297
|
+
* @param ast - The file's parsed AST.
|
|
3298
|
+
* @param adapter - The language adapter (drives node discovery + text reads).
|
|
3299
|
+
* @param sourceCode - The file's source text.
|
|
3300
|
+
* @param config - Optional data-access config; defaults to the shared config.
|
|
3301
|
+
* @returns The raw, provenance-free `loop-query-candidates`.
|
|
3302
|
+
*/
|
|
3303
|
+
export function extractLoopQueryRawCandidates(ast, adapter, sourceCode, config) {
|
|
3304
|
+
const scan = buildCandidateScan(ast, adapter, sourceCode, config);
|
|
3305
|
+
const candidates = [];
|
|
3306
|
+
const dbNodes = adapter.findNodes(ast, {
|
|
3307
|
+
custom: (node) => isBroadenedLoopCandidate(node, scan),
|
|
3308
|
+
});
|
|
3309
|
+
const transactionWrappedFunctions = collectTransactionWrappedFunctions(ast, adapter, sourceCode);
|
|
3310
|
+
for (const node of dbNodes) {
|
|
3311
|
+
// A DB call and its template-literal SQL argument both satisfy the structural
|
|
3312
|
+
// discovery — collapse to the call node, exactly as `collectLoopQueryCandidates`
|
|
3313
|
+
// does (isDbCallNode only admits a literal whose enclosing call is provenanced,
|
|
3314
|
+
// so skipping the literal loses nothing).
|
|
3315
|
+
if (isTemplateLiteral(node, adapter))
|
|
3316
|
+
continue;
|
|
3317
|
+
const nodeText = adapter.getNodeText(node, sourceCode);
|
|
3318
|
+
if (!nodeText || nodeText.trim().length < 10)
|
|
3319
|
+
continue;
|
|
3320
|
+
if (isStatementConstructionOnly(node, adapter, sourceCode))
|
|
3321
|
+
continue;
|
|
3322
|
+
if (isSqlStringConstruction(node, adapter, sourceCode))
|
|
3323
|
+
continue;
|
|
3324
|
+
const loopInfo = findEnclosingLoop(node, adapter, sourceCode);
|
|
3325
|
+
if (!loopInfo)
|
|
3326
|
+
continue;
|
|
3327
|
+
if (isForOfIterableDbCall(node, loopInfo.loopNode, adapter))
|
|
3328
|
+
continue;
|
|
3329
|
+
const enclosingFnName = findEnclosingFunctionIdentity(loopInfo.loopNode, adapter, ast.filePath).name;
|
|
3330
|
+
const transactionEnclosesLoop = isInsideDbTransaction(loopInfo.loopNode, adapter, sourceCode) ||
|
|
3331
|
+
(enclosingFnName !== null && transactionWrappedFunctions.has(enclosingFnName));
|
|
3332
|
+
const preparesInLoop = loopBodyContainsPrepare(loopInfo.loopNode, adapter, sourceCode);
|
|
3333
|
+
const hoistedReuse = !preparesInLoop &&
|
|
3334
|
+
(transactionEnclosesLoop || isHoistedStatementReRun(node, loopInfo.loopNode, adapter, sourceCode));
|
|
3335
|
+
if (hoistedReuse)
|
|
3336
|
+
continue;
|
|
3337
|
+
if (bindsBatchArgument(node, loopInfo.loopNode, adapter, sourceCode))
|
|
3338
|
+
continue;
|
|
3339
|
+
if (loopBodyContainsLlmCall(loopInfo.loopNode, adapter, sourceCode))
|
|
3340
|
+
continue;
|
|
3341
|
+
if (loopBodyContainsMessageLifecycleCall(loopInfo.loopNode, adapter, sourceCode))
|
|
3342
|
+
continue;
|
|
3343
|
+
const handle = handleCallSiteIdentity(node, scan);
|
|
3344
|
+
const anchor = getCallExpressionCallee(node, adapter) ?? node;
|
|
3345
|
+
candidates.push({
|
|
3346
|
+
file: ast.filePath,
|
|
3347
|
+
line: anchor.location.start.line,
|
|
3348
|
+
column: anchor.location.start.column,
|
|
3349
|
+
enclosingFunction: enclosingIdentity(node, adapter, ast.filePath),
|
|
3350
|
+
loopStartOffset: loopInfo.loopNode.range[0],
|
|
3351
|
+
loopLine: loopInfo.loopNode.location.start.line,
|
|
3352
|
+
depth: loopInfo.depth,
|
|
3353
|
+
handleCalleeType: handle.calleeType,
|
|
3354
|
+
handleName: handle.name,
|
|
3355
|
+
handleRoot: handle.root,
|
|
3356
|
+
handleReceiver: handle.receiver,
|
|
3357
|
+
handleMethod: handle.method,
|
|
3358
|
+
handleThisField: handle.thisField,
|
|
3359
|
+
handleThisHeritage: handle.thisHeritage,
|
|
3360
|
+
handleSiteReceiver: handle.siteReceiver,
|
|
3361
|
+
sqlArg: handle.sqlArg,
|
|
3362
|
+
handleRootScope: handle.scope,
|
|
3363
|
+
});
|
|
1747
3364
|
}
|
|
3365
|
+
return candidates;
|
|
1748
3366
|
}
|
|
1749
3367
|
//# sourceMappingURL=UniversalDataAccessAnalyzer.js.map
|