code-auditor-mcp 3.4.12 → 3.4.13
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/CHANGELOG.md +46 -1
- package/dist/analyzers/__verify_self_probe.d.ts +2 -0
- package/dist/analyzers/__verify_self_probe.d.ts.map +1 -0
- package/dist/analyzers/__verify_self_probe.js +5 -0
- package/dist/analyzers/__verify_self_probe.js.map +1 -0
- package/dist/analyzers/analyzerUtils.d.ts +34 -1
- package/dist/analyzers/analyzerUtils.d.ts.map +1 -1
- package/dist/analyzers/analyzerUtils.js +61 -22
- package/dist/analyzers/analyzerUtils.js.map +1 -1
- package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts +18 -23
- package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/cross-language/APIContractAnalyzer.js +198 -186
- package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +78 -55
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js +244 -243
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.d.ts +27 -37
- package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.js +314 -275
- package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +17 -46
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +620 -523
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
- package/dist/analyzers/documentationAnalyzer.d.ts +7 -2
- package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/documentationAnalyzer.js +259 -198
- package/dist/analyzers/documentationAnalyzer.js.map +1 -1
- package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/invariantsAnalyzer.js +57 -50
- package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
- package/dist/analyzers/orm/adapterRegistry.d.ts +10 -0
- package/dist/analyzers/orm/adapterRegistry.d.ts.map +1 -1
- package/dist/analyzers/orm/adapterRegistry.js +10 -0
- package/dist/analyzers/orm/adapterRegistry.js.map +1 -1
- package/dist/analyzers/orm/drizzleAdapter.d.ts +22 -0
- package/dist/analyzers/orm/drizzleAdapter.d.ts.map +1 -1
- package/dist/analyzers/orm/drizzleAdapter.js +42 -44
- package/dist/analyzers/orm/drizzleAdapter.js.map +1 -1
- package/dist/analyzers/orm/prismaAdapter.d.ts +30 -0
- package/dist/analyzers/orm/prismaAdapter.d.ts.map +1 -1
- package/dist/analyzers/orm/prismaAdapter.js +30 -0
- package/dist/analyzers/orm/prismaAdapter.js.map +1 -1
- package/dist/analyzers/provenance.d.ts +62 -2
- package/dist/analyzers/provenance.d.ts.map +1 -1
- package/dist/analyzers/provenance.js +443 -349
- package/dist/analyzers/provenance.js.map +1 -1
- package/dist/analyzers/reactAnalyzer.d.ts +11 -0
- package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/reactAnalyzer.js +188 -125
- package/dist/analyzers/reactAnalyzer.js.map +1 -1
- package/dist/analyzers/ruleRegistry.d.ts +110 -1
- package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
- package/dist/analyzers/ruleRegistry.js +1714 -124
- package/dist/analyzers/ruleRegistry.js.map +1 -1
- package/dist/analyzers/ruleTiming.d.ts +54 -0
- package/dist/analyzers/ruleTiming.d.ts.map +1 -0
- package/dist/analyzers/ruleTiming.js +71 -0
- package/dist/analyzers/ruleTiming.js.map +1 -0
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts +13 -0
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +362 -283
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +26 -68
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js +430 -391
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +17 -202
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +1073 -1121
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +3 -49
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +414 -340
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +48 -5
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +262 -134
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +23 -214
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +139 -1521
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +78 -79
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js +613 -480
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/schema/codeAnalysis.d.ts +307 -0
- package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/codeAnalysis.js +965 -0
- package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -0
- package/dist/analyzers/universal/schema/config.d.ts +31 -0
- package/dist/analyzers/universal/schema/config.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/config.js +57 -0
- package/dist/analyzers/universal/schema/config.js.map +1 -0
- package/dist/analyzers/universal/schema/discovery.d.ts +167 -0
- package/dist/analyzers/universal/schema/discovery.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/discovery.js +616 -0
- package/dist/analyzers/universal/schema/discovery.js.map +1 -0
- package/dist/analyzers/universal/schema/jsonSchema.d.ts +27 -0
- package/dist/analyzers/universal/schema/jsonSchema.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/jsonSchema.js +368 -0
- package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -0
- package/dist/analyzers/universal/schema/migrations.d.ts +90 -0
- package/dist/analyzers/universal/schema/migrations.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/migrations.js +238 -0
- package/dist/analyzers/universal/schema/migrations.js.map +1 -0
- package/dist/analyzers/universal/schema/types.d.ts +131 -0
- package/dist/analyzers/universal/schema/types.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/types.js +10 -0
- package/dist/analyzers/universal/schema/types.js.map +1 -0
- package/dist/analyzers/universal/schema/violations.d.ts +44 -0
- package/dist/analyzers/universal/schema/violations.d.ts.map +1 -0
- package/dist/analyzers/universal/schema/violations.js +54 -0
- package/dist/analyzers/universal/schema/violations.js.map +1 -0
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +89 -13
- package/dist/auditRunner.js.map +1 -1
- package/dist/cli.js +315 -40
- package/dist/cli.js.map +1 -1
- package/dist/config/configLoader.d.ts.map +1 -1
- package/dist/config/configLoader.js +9 -7
- package/dist/config/configLoader.js.map +1 -1
- package/dist/config/defaults.d.ts +12 -3
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +15 -5
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/effectiveConfig.d.ts +75 -0
- package/dist/config/effectiveConfig.d.ts.map +1 -0
- package/dist/config/effectiveConfig.js +213 -0
- package/dist/config/effectiveConfig.js.map +1 -0
- package/dist/config/pathProfiles.d.ts +7 -4
- package/dist/config/pathProfiles.d.ts.map +1 -1
- package/dist/config/pathProfiles.js +8 -10
- package/dist/config/pathProfiles.js.map +1 -1
- package/dist/config/thresholdRationales.d.ts +45 -0
- package/dist/config/thresholdRationales.d.ts.map +1 -0
- package/dist/config/thresholdRationales.js +67 -0
- package/dist/config/thresholdRationales.js.map +1 -0
- package/dist/conventions/conventionMiner.d.ts.map +1 -1
- package/dist/conventions/conventionMiner.js +6 -2
- package/dist/conventions/conventionMiner.js.map +1 -1
- package/dist/enforcement/diffGate.d.ts +55 -0
- package/dist/enforcement/diffGate.d.ts.map +1 -0
- package/dist/enforcement/diffGate.js +136 -0
- package/dist/enforcement/diffGate.js.map +1 -0
- package/dist/enforcement/gate.d.ts +36 -0
- package/dist/enforcement/gate.d.ts.map +1 -0
- package/dist/enforcement/gate.js +72 -0
- package/dist/enforcement/gate.js.map +1 -0
- package/dist/enforcement/suppressions.d.ts +41 -0
- package/dist/enforcement/suppressions.d.ts.map +1 -0
- package/dist/enforcement/suppressions.js +170 -0
- package/dist/enforcement/suppressions.js.map +1 -0
- package/dist/fingerprint.d.ts.map +1 -1
- package/dist/fingerprint.js +4 -1
- package/dist/fingerprint.js.map +1 -1
- package/dist/invariants/ruleEngine.d.ts +3 -4
- package/dist/invariants/ruleEngine.d.ts.map +1 -1
- package/dist/invariants/ruleEngine.js +2 -2
- package/dist/invariants/ruleEngine.js.map +1 -1
- package/dist/languages/LanguageOrchestrator.d.ts +14 -0
- package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
- package/dist/languages/LanguageOrchestrator.js +93 -55
- package/dist/languages/LanguageOrchestrator.js.map +1 -1
- package/dist/languages/LanguageRegistry.d.ts +8 -0
- package/dist/languages/LanguageRegistry.d.ts.map +1 -1
- package/dist/languages/LanguageRegistry.js +8 -0
- package/dist/languages/LanguageRegistry.js.map +1 -1
- package/dist/languages/RuntimeManager.d.ts +82 -60
- package/dist/languages/RuntimeManager.d.ts.map +1 -1
- package/dist/languages/RuntimeManager.js +404 -349
- package/dist/languages/RuntimeManager.js.map +1 -1
- package/dist/languages/UniversalAnalyzer.d.ts +51 -6
- package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
- package/dist/languages/UniversalAnalyzer.js +127 -88
- package/dist/languages/UniversalAnalyzer.js.map +1 -1
- package/dist/languages/adapterBridge.d.ts +36 -0
- package/dist/languages/adapterBridge.d.ts.map +1 -1
- package/dist/languages/adapterBridge.js +89 -35
- package/dist/languages/adapterBridge.js.map +1 -1
- package/dist/languages/go/GoAdapter.d.ts +66 -23
- package/dist/languages/go/GoAdapter.d.ts.map +1 -1
- package/dist/languages/go/GoAdapter.js +553 -474
- package/dist/languages/go/GoAdapter.js.map +1 -1
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts +33 -4
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.d.ts.map +1 -1
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.js +101 -66
- package/dist/languages/tree-sitter/TreeSitterCssAdapter.js.map +1 -1
- package/dist/languages/tree-sitter/converter.d.ts +18 -0
- package/dist/languages/tree-sitter/converter.d.ts.map +1 -1
- package/dist/languages/tree-sitter/converter.js +18 -0
- package/dist/languages/tree-sitter/converter.js.map +1 -1
- package/dist/languages/tree-sitter/parser.d.ts +39 -2
- package/dist/languages/tree-sitter/parser.d.ts.map +1 -1
- package/dist/languages/tree-sitter/parser.js +93 -12
- package/dist/languages/tree-sitter/parser.js.map +1 -1
- package/dist/languages/types.d.ts +67 -2
- package/dist/languages/types.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +253 -72
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +1419 -923
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
- package/dist/mcp-tools-shared.d.ts.map +1 -1
- package/dist/mcp-tools-shared.js.map +1 -1
- package/dist/pipeline.d.ts +13 -5
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +381 -200
- package/dist/pipeline.js.map +1 -1
- package/dist/pipelineAdapters.d.ts +3 -8
- package/dist/pipelineAdapters.d.ts.map +1 -1
- package/dist/pipelineAdapters.js +149 -94
- package/dist/pipelineAdapters.js.map +1 -1
- package/dist/presets/presets.d.ts +54 -0
- package/dist/presets/presets.d.ts.map +1 -0
- package/dist/presets/presets.js +181 -0
- package/dist/presets/presets.js.map +1 -0
- package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
- package/dist/reporting/sarifReportGenerator.js +0 -2
- package/dist/reporting/sarifReportGenerator.js.map +1 -1
- package/dist/ruleAliases.d.ts +67 -0
- package/dist/ruleAliases.d.ts.map +1 -0
- package/dist/ruleAliases.js +79 -0
- package/dist/ruleAliases.js.map +1 -0
- package/dist/styles/cssAstExtractor.d.ts.map +1 -1
- package/dist/styles/cssAstExtractor.js +78 -18
- package/dist/styles/cssAstExtractor.js.map +1 -1
- package/dist/styles/styleIndexer.js +10 -1
- package/dist/styles/styleIndexer.js.map +1 -1
- package/dist/styles/tailwindConfigLoader.d.ts +8 -6
- package/dist/styles/tailwindConfigLoader.d.ts.map +1 -1
- package/dist/styles/tailwindConfigLoader.js +61 -15
- package/dist/styles/tailwindConfigLoader.js.map +1 -1
- package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
- package/dist/styles/tailwindUtilityExpander.js +13 -1
- package/dist/styles/tailwindUtilityExpander.js.map +1 -1
- package/dist/types.d.ts +181 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -1
- package/package.json +5 -3
- package/plugin/hooks/hooks.json +1 -1
- package/plugin/scripts/hook-audit.sh +25 -7
- package/plugin/skills/code-auditor/SKILL.md +5 -5
|
@@ -65,7 +65,26 @@ export const DB_TYPES = new Set([
|
|
|
65
65
|
'SqliteDatabase',
|
|
66
66
|
'BetterSQLite3Database',
|
|
67
67
|
]);
|
|
68
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* DB call methods — the fixed API surface (language-invariant).
|
|
70
|
+
*
|
|
71
|
+
* Spec 33 Item 11 FP category 5: the bare-identifier hybrid fallback in
|
|
72
|
+
* `isDBProvenanced` treated any `get(...)` / `each(...)` / `values(...)` call
|
|
73
|
+
* as a DB query, flagging lodash-style object accessors (e.g.
|
|
74
|
+
* `@directus/utils`'s `get(item, ...)`) as sql-injection. Those three names
|
|
75
|
+
* are also common non-DB methods (lodash `get`, jQuery/iterator
|
|
76
|
+
* `each`, Map/WebSocket `.values()`), so they are removed from the fallback —
|
|
77
|
+
* mirroring the `get`/`each` trim in CHANGELOG 3.4.9 (DB_CALL_METHOD_NAMES).
|
|
78
|
+
*
|
|
79
|
+
* `query` is deliberately RETAINED: it is a genuine query-execution method on
|
|
80
|
+
* mysql2, pg, node-postgres, D1 and Planetscale (`.query(...)`), and the
|
|
81
|
+
* spec-19 data-access fixtures exercise it as a canonical DB entry point.
|
|
82
|
+
* Removing it would turn real SQL-injection positives into false negatives.
|
|
83
|
+
*
|
|
84
|
+
* `raw` is deliberately retained: it is a genuine raw-execution method on
|
|
85
|
+
* D1 prepared statements, Knex, and Kysely, and the Item-6 taint-tracking
|
|
86
|
+
* fixtures exercise it as the canonical raw-SQL entry point.
|
|
87
|
+
*/
|
|
69
88
|
export const DB_CALL_METHODS = new Set([
|
|
70
89
|
'exec',
|
|
71
90
|
'prepare',
|
|
@@ -74,10 +93,7 @@ export const DB_CALL_METHODS = new Set([
|
|
|
74
93
|
'all',
|
|
75
94
|
'first',
|
|
76
95
|
'query',
|
|
77
|
-
'get',
|
|
78
|
-
'each',
|
|
79
96
|
'raw',
|
|
80
|
-
'values',
|
|
81
97
|
]);
|
|
82
98
|
/** ORM method patterns — fixed API surface for ORM recognition (Spec 21 R1) */
|
|
83
99
|
export const ORM_METHODS = new Set([
|
|
@@ -128,6 +144,9 @@ function matchesValidatorPackage(specifier) {
|
|
|
128
144
|
*
|
|
129
145
|
* An import like `import Database from 'better-sqlite3'` produces
|
|
130
146
|
* `Database` as DB-provenanced with reason "package".
|
|
147
|
+
* @param adapter
|
|
148
|
+
* @param ast
|
|
149
|
+
* @returns
|
|
131
150
|
*/
|
|
132
151
|
export function extractDBProvenancedImports(ast, adapter) {
|
|
133
152
|
const seedMap = new Map();
|
|
@@ -159,6 +178,9 @@ export function extractDBProvenancedImports(ast, adapter) {
|
|
|
159
178
|
/**
|
|
160
179
|
* Extract all validator-provenanced identifiers from a file's imports.
|
|
161
180
|
* Same pattern as extractDBProvenancedImports but for validator packages.
|
|
181
|
+
* @param adapter
|
|
182
|
+
* @param ast
|
|
183
|
+
* @returns
|
|
162
184
|
*/
|
|
163
185
|
export function extractValidatorProvenancedImports(ast, adapter) {
|
|
164
186
|
const seedMap = new Map();
|
|
@@ -189,6 +211,133 @@ export function extractValidatorProvenancedImports(ast, adapter) {
|
|
|
189
211
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
190
212
|
// Provenance propagation (R1 core)
|
|
191
213
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
214
|
+
/**
|
|
215
|
+
* Propagate provenance through a variable declaration (rules 1-3, 8).
|
|
216
|
+
* Returns true if any new identifier was added.
|
|
217
|
+
*/
|
|
218
|
+
function propagateVariableDeclaration(node, ctx) {
|
|
219
|
+
const { nameNode, valueNode, typeAnnotationNode } = splitVariableDeclarator(node, ctx.adapter);
|
|
220
|
+
if (!nameNode)
|
|
221
|
+
return false;
|
|
222
|
+
let mutated = false;
|
|
223
|
+
// Rule 8: type annotation — let x: D1Database
|
|
224
|
+
if (typeAnnotationNode) {
|
|
225
|
+
mutated = propagateFromTypeAnnotation(nameNode, typeAnnotationNode, ctx);
|
|
226
|
+
}
|
|
227
|
+
if (valueNode) {
|
|
228
|
+
mutated = propagateFromValue(nameNode, valueNode, ctx) || mutated;
|
|
229
|
+
}
|
|
230
|
+
return mutated;
|
|
231
|
+
}
|
|
232
|
+
/** Rule 8: type annotation — `let x: D1Database`. */
|
|
233
|
+
function propagateFromTypeAnnotation(nameNode, typeAnnotationNode, ctx) {
|
|
234
|
+
if (nameNode.type !== 'identifier')
|
|
235
|
+
return false;
|
|
236
|
+
const typeText = ctx.adapter.getNodeText(typeAnnotationNode, ctx.sourceCode).trim();
|
|
237
|
+
if (!DB_TYPES.has(typeText))
|
|
238
|
+
return false;
|
|
239
|
+
const name = ctx.adapter.getNodeText(nameNode, ctx.sourceCode);
|
|
240
|
+
if (ctx.provenanceMap.has(name))
|
|
241
|
+
return false;
|
|
242
|
+
ctx.provenanceMap.set(name, {
|
|
243
|
+
identifier: name,
|
|
244
|
+
reason: 'type',
|
|
245
|
+
source: `type annotation ${typeText}`,
|
|
246
|
+
chain: [],
|
|
247
|
+
});
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
/** Propagate provenance from a declarator's value expression into its names. */
|
|
251
|
+
function propagateFromValue(nameNode, valueNode, ctx) {
|
|
252
|
+
const propagated = tryPropagateFromExpression(valueNode, ctx.adapter, ctx.sourceCode, ctx.provenanceMap);
|
|
253
|
+
if (!propagated)
|
|
254
|
+
return false;
|
|
255
|
+
let mutated = false;
|
|
256
|
+
for (const varName of extractPatternNames(nameNode, ctx.adapter, ctx.sourceCode)) {
|
|
257
|
+
if (!ctx.provenanceMap.has(varName)) {
|
|
258
|
+
ctx.provenanceMap.set(varName, {
|
|
259
|
+
identifier: varName,
|
|
260
|
+
reason: 'propagation',
|
|
261
|
+
source: propagated.source,
|
|
262
|
+
chain: [...propagated.chain, propagated.identifier],
|
|
263
|
+
});
|
|
264
|
+
mutated = true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return mutated;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Propagate provenance through a default parameter (rule 6).
|
|
271
|
+
* Returns true if a new identifier was added.
|
|
272
|
+
*/
|
|
273
|
+
function propagateDefaultParameter(node, adapter, sourceCode, provenanceMap) {
|
|
274
|
+
const children = adapter.getChildren(node);
|
|
275
|
+
// assignment_pattern has [left, right]
|
|
276
|
+
if (children.length < 2)
|
|
277
|
+
return false;
|
|
278
|
+
const leftNode = children[0];
|
|
279
|
+
const rightNode = children[1];
|
|
280
|
+
if (leftNode.type !== 'identifier')
|
|
281
|
+
return false;
|
|
282
|
+
const paramName = adapter.getNodeText(leftNode, sourceCode);
|
|
283
|
+
const propagated = tryPropagateFromExpression(rightNode, adapter, sourceCode, provenanceMap);
|
|
284
|
+
if (propagated && !provenanceMap.has(paramName)) {
|
|
285
|
+
provenanceMap.set(paramName, {
|
|
286
|
+
identifier: paramName,
|
|
287
|
+
reason: 'propagation',
|
|
288
|
+
source: `default parameter = ${propagated.source}`,
|
|
289
|
+
chain: [...propagated.chain, propagated.identifier],
|
|
290
|
+
});
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Propagate provenance through a class field initialization (rule 7).
|
|
297
|
+
* Returns true if a new identifier was added.
|
|
298
|
+
*/
|
|
299
|
+
function propagateClassField(node, adapter, sourceCode, provenanceMap) {
|
|
300
|
+
const children = adapter.getChildren(node);
|
|
301
|
+
// Typically [name, value] or [decorators..., name, value]
|
|
302
|
+
const nameChild = children.find((c) => c.type === 'property_identifier');
|
|
303
|
+
const valueChild = children.find((c) => c.type !== 'property_identifier' &&
|
|
304
|
+
c.type !== 'decorator' &&
|
|
305
|
+
c.type !== 'private' &&
|
|
306
|
+
c.type !== 'public' &&
|
|
307
|
+
c.type !== 'protected' &&
|
|
308
|
+
c.type !== 'static' &&
|
|
309
|
+
c.type !== 'readonly' &&
|
|
310
|
+
c.type !== 'abstract');
|
|
311
|
+
if (nameChild && valueChild) {
|
|
312
|
+
const fieldName = adapter.getNodeText(nameChild, sourceCode);
|
|
313
|
+
const propagated = tryPropagateFromExpression(valueChild, adapter, sourceCode, provenanceMap);
|
|
314
|
+
if (propagated && !provenanceMap.has(fieldName)) {
|
|
315
|
+
provenanceMap.set(fieldName, {
|
|
316
|
+
identifier: fieldName,
|
|
317
|
+
reason: 'propagation',
|
|
318
|
+
source: `class field initialized from ${propagated.source}`,
|
|
319
|
+
chain: [...propagated.chain, propagated.identifier],
|
|
320
|
+
});
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
function applyPropagationRule(node, parent, ctx) {
|
|
327
|
+
const { adapter, sourceCode, provenanceMap } = ctx;
|
|
328
|
+
if (node.type === 'variable_declarator') {
|
|
329
|
+
return propagateVariableDeclaration(node, ctx);
|
|
330
|
+
}
|
|
331
|
+
if (node.type === 'assignment_pattern' &&
|
|
332
|
+
parent?.type === 'formal_parameters') {
|
|
333
|
+
return propagateDefaultParameter(node, adapter, sourceCode, provenanceMap);
|
|
334
|
+
}
|
|
335
|
+
if (node.type === 'public_field_definition' ||
|
|
336
|
+
node.type === 'field_definition') {
|
|
337
|
+
return propagateClassField(node, adapter, sourceCode, provenanceMap);
|
|
338
|
+
}
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
192
341
|
/**
|
|
193
342
|
* Propagate provenance through assignments, destructuring, parameters,
|
|
194
343
|
* class fields, and type annotations within a single file.
|
|
@@ -202,10 +351,16 @@ export function extractValidatorProvenancedImports(ast, adapter) {
|
|
|
202
351
|
* 6. default parameter with DB value
|
|
203
352
|
* 7. class field initialized with DB value
|
|
204
353
|
* 8. type annotation with known DB type
|
|
354
|
+
* @param adapter
|
|
355
|
+
* @param ast
|
|
356
|
+
* @param seedMap
|
|
357
|
+
* @param sourceCode
|
|
358
|
+
* @returns
|
|
205
359
|
*/
|
|
206
360
|
export function propagateProvenance(ast, adapter, sourceCode, seedMap) {
|
|
207
361
|
// Work on a copy so we can add newly-provenanced identifiers during the walk
|
|
208
362
|
const provenanceMap = new Map(seedMap);
|
|
363
|
+
const ctx = { adapter, sourceCode, provenanceMap };
|
|
209
364
|
// Keep iterating until no new identifiers are discovered (handles chains)
|
|
210
365
|
let changed = true;
|
|
211
366
|
let iterations = 0;
|
|
@@ -214,100 +369,8 @@ export function propagateProvenance(ast, adapter, sourceCode, seedMap) {
|
|
|
214
369
|
changed = false;
|
|
215
370
|
iterations++;
|
|
216
371
|
walkAST(ast.root, (node, parent) => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const { nameNode, valueNode, typeAnnotationNode } = splitVariableDeclarator(node, adapter);
|
|
220
|
-
if (!nameNode)
|
|
221
|
-
return;
|
|
222
|
-
// Rule 8: type annotation — let x: D1Database
|
|
223
|
-
if (typeAnnotationNode && nameNode.type === 'identifier') {
|
|
224
|
-
const typeText = adapter.getNodeText(typeAnnotationNode, sourceCode).trim();
|
|
225
|
-
if (DB_TYPES.has(typeText)) {
|
|
226
|
-
const name = adapter.getNodeText(nameNode, sourceCode);
|
|
227
|
-
if (!provenanceMap.has(name)) {
|
|
228
|
-
provenanceMap.set(name, {
|
|
229
|
-
identifier: name,
|
|
230
|
-
reason: 'type',
|
|
231
|
-
source: `type annotation ${typeText}`,
|
|
232
|
-
chain: [],
|
|
233
|
-
});
|
|
234
|
-
changed = true;
|
|
235
|
-
}
|
|
236
|
-
// Type-provenanced names count as DB-provenanced for further propagation
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
if (valueNode) {
|
|
240
|
-
const propagated = tryPropagateFromExpression(valueNode, adapter, sourceCode, provenanceMap);
|
|
241
|
-
if (propagated) {
|
|
242
|
-
// Extract the variable name(s) from the name node
|
|
243
|
-
const varNames = extractPatternNames(nameNode, adapter, sourceCode);
|
|
244
|
-
for (const varName of varNames) {
|
|
245
|
-
if (!provenanceMap.has(varName)) {
|
|
246
|
-
provenanceMap.set(varName, {
|
|
247
|
-
identifier: varName,
|
|
248
|
-
reason: 'propagation',
|
|
249
|
-
source: propagated.source,
|
|
250
|
-
chain: [...propagated.chain, propagated.identifier],
|
|
251
|
-
});
|
|
252
|
-
changed = true;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
// ── Rule 6: default parameters ──
|
|
260
|
-
if (node.type === 'assignment_pattern' &&
|
|
261
|
-
parent?.type === 'formal_parameters') {
|
|
262
|
-
const children = adapter.getChildren(node);
|
|
263
|
-
// assignment_pattern has [left, right]
|
|
264
|
-
if (children.length >= 2) {
|
|
265
|
-
const leftNode = children[0];
|
|
266
|
-
const rightNode = children[1];
|
|
267
|
-
if (leftNode.type === 'identifier') {
|
|
268
|
-
const paramName = adapter.getNodeText(leftNode, sourceCode);
|
|
269
|
-
const propagated = tryPropagateFromExpression(rightNode, adapter, sourceCode, provenanceMap);
|
|
270
|
-
if (propagated && !provenanceMap.has(paramName)) {
|
|
271
|
-
provenanceMap.set(paramName, {
|
|
272
|
-
identifier: paramName,
|
|
273
|
-
reason: 'propagation',
|
|
274
|
-
source: `default parameter = ${propagated.source}`,
|
|
275
|
-
chain: [...propagated.chain, propagated.identifier],
|
|
276
|
-
});
|
|
277
|
-
changed = true;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
// ── Rule 7: class field initialization ──
|
|
284
|
-
if (node.type === 'public_field_definition' ||
|
|
285
|
-
node.type === 'field_definition') {
|
|
286
|
-
const children = adapter.getChildren(node);
|
|
287
|
-
// Typically [name, value] or [decorators..., name, value]
|
|
288
|
-
const nameChild = children.find((c) => c.type === 'property_identifier');
|
|
289
|
-
const valueChild = children.find((c) => c.type !== 'property_identifier' &&
|
|
290
|
-
c.type !== 'decorator' &&
|
|
291
|
-
c.type !== 'private' &&
|
|
292
|
-
c.type !== 'public' &&
|
|
293
|
-
c.type !== 'protected' &&
|
|
294
|
-
c.type !== 'static' &&
|
|
295
|
-
c.type !== 'readonly' &&
|
|
296
|
-
c.type !== 'abstract');
|
|
297
|
-
if (nameChild && valueChild) {
|
|
298
|
-
const fieldName = adapter.getNodeText(nameChild, sourceCode);
|
|
299
|
-
const propagated = tryPropagateFromExpression(valueChild, adapter, sourceCode, provenanceMap);
|
|
300
|
-
if (propagated && !provenanceMap.has(fieldName)) {
|
|
301
|
-
provenanceMap.set(fieldName, {
|
|
302
|
-
identifier: fieldName,
|
|
303
|
-
reason: 'propagation',
|
|
304
|
-
source: `class field initialized from ${propagated.source}`,
|
|
305
|
-
chain: [...propagated.chain, propagated.identifier],
|
|
306
|
-
});
|
|
307
|
-
changed = true;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return;
|
|
372
|
+
if (applyPropagationRule(node, parent, ctx)) {
|
|
373
|
+
changed = true;
|
|
311
374
|
}
|
|
312
375
|
});
|
|
313
376
|
}
|
|
@@ -337,24 +400,9 @@ function tryPropagateFromExpression(node, adapter, sourceCode, provenanceMap) {
|
|
|
337
400
|
// ── Rule 2: drizzle(env.DB) — call where callee is DB-provenanced ──
|
|
338
401
|
// ── Rule 3: db.prepare(sql) — member expression call on DB receiver ──
|
|
339
402
|
if (node.type === 'call_expression') {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
// Case: simple identifier call — drizzle(...)
|
|
344
|
-
if (calleeNode.type === 'identifier') {
|
|
345
|
-
const name = adapter.getNodeText(calleeNode, sourceCode);
|
|
346
|
-
if (name && provenanceMap.has(name)) {
|
|
347
|
-
return provenanceMap.get(name);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
// Case: member expression — db.prepare(...)
|
|
351
|
-
if (calleeNode.type === 'member_expression') {
|
|
352
|
-
const receiver = getMemberExpressionReceiver(calleeNode, adapter, sourceCode);
|
|
353
|
-
if (receiver && provenanceMap.has(receiver)) {
|
|
354
|
-
return provenanceMap.get(receiver);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
403
|
+
const viaCall = tryCallProvenance(node, adapter, sourceCode, provenanceMap);
|
|
404
|
+
if (viaCall)
|
|
405
|
+
return viaCall;
|
|
358
406
|
}
|
|
359
407
|
// ── Simple identifier reference (for destructuring sources) ──
|
|
360
408
|
if (node.type === 'identifier') {
|
|
@@ -372,6 +420,31 @@ function tryPropagateFromExpression(node, adapter, sourceCode, provenanceMap) {
|
|
|
372
420
|
}
|
|
373
421
|
return null;
|
|
374
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Rule 2/3: check a call_expression whose callee is either a DB-provenanced
|
|
425
|
+
* identifier (e.g. `drizzle(...)`) or a member expression on a DB receiver
|
|
426
|
+
* (e.g. `db.prepare(sql)`).
|
|
427
|
+
*/
|
|
428
|
+
function tryCallProvenance(node, adapter, sourceCode, provenanceMap) {
|
|
429
|
+
const calleeNode = getCallExpressionCallee(node, adapter);
|
|
430
|
+
if (!calleeNode)
|
|
431
|
+
return null;
|
|
432
|
+
// Case: simple identifier call — drizzle(...)
|
|
433
|
+
if (calleeNode.type === 'identifier') {
|
|
434
|
+
const name = adapter.getNodeText(calleeNode, sourceCode);
|
|
435
|
+
if (name && provenanceMap.has(name)) {
|
|
436
|
+
return provenanceMap.get(name);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Case: member expression — db.prepare(...)
|
|
440
|
+
if (calleeNode.type === 'member_expression') {
|
|
441
|
+
const receiver = getMemberExpressionReceiver(calleeNode, adapter, sourceCode);
|
|
442
|
+
if (receiver && provenanceMap.has(receiver)) {
|
|
443
|
+
return provenanceMap.get(receiver);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
375
448
|
/**
|
|
376
449
|
* Split a variable_declarator into its name, value, and type annotation
|
|
377
450
|
* child nodes.
|
|
@@ -425,59 +498,57 @@ function splitVariableDeclarator(node, adapter) {
|
|
|
425
498
|
*/
|
|
426
499
|
function extractPatternNames(node, adapter, sourceCode) {
|
|
427
500
|
const names = [];
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
else if (child.type === 'pair_pattern') {
|
|
448
|
-
// pair_pattern children: [property, value]
|
|
449
|
-
// Extract from the value side (which might be an identifier or nested pattern)
|
|
450
|
-
const pairChildren = adapter.getChildren(child);
|
|
451
|
-
if (pairChildren.length >= 2) {
|
|
452
|
-
collect(pairChildren[1]);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
else if (child.type === 'rest_pattern') {
|
|
456
|
-
collectChildren(child, adapter, sourceCode, names);
|
|
457
|
-
}
|
|
458
|
-
else {
|
|
459
|
-
collect(child);
|
|
460
|
-
}
|
|
501
|
+
collectPatternNames(node, adapter, sourceCode, names);
|
|
502
|
+
return names;
|
|
503
|
+
}
|
|
504
|
+
/** Recursively collect identifier names from a destructuring pattern node. */
|
|
505
|
+
function collectPatternNames(node, adapter, sourceCode, names) {
|
|
506
|
+
if (node.type === 'identifier') {
|
|
507
|
+
const name = adapter.getNodeText(node, sourceCode);
|
|
508
|
+
if (name)
|
|
509
|
+
names.push(name);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (node.type === 'object_pattern') {
|
|
513
|
+
for (const child of adapter.getChildren(node)) {
|
|
514
|
+
if (child.type === '{' || child.type === '}' || child.type === ',')
|
|
515
|
+
continue;
|
|
516
|
+
if (child.type === 'shorthand_property_identifier') {
|
|
517
|
+
const name = adapter.getNodeText(child, sourceCode);
|
|
518
|
+
if (name)
|
|
519
|
+
names.push(name);
|
|
461
520
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if (
|
|
466
|
-
|
|
521
|
+
else if (child.type === 'pair_pattern') {
|
|
522
|
+
// pair_pattern children: [property, value] — collect from value side
|
|
523
|
+
const pairChildren = adapter.getChildren(child);
|
|
524
|
+
if (pairChildren.length >= 2) {
|
|
525
|
+
collectPatternNames(pairChildren[1], adapter, sourceCode, names);
|
|
467
526
|
}
|
|
468
|
-
collect(child);
|
|
469
527
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
collect(children[0]);
|
|
528
|
+
else if (child.type === 'rest_pattern') {
|
|
529
|
+
collectChildren(child, adapter, sourceCode, names);
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
collectPatternNames(child, adapter, sourceCode, names);
|
|
476
533
|
}
|
|
477
534
|
}
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (node.type === 'array_pattern') {
|
|
538
|
+
for (const child of adapter.getChildren(node)) {
|
|
539
|
+
if (child.type === '[' || child.type === ']' || child.type === ',')
|
|
540
|
+
continue;
|
|
541
|
+
collectPatternNames(child, adapter, sourceCode, names);
|
|
542
|
+
}
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
// assignment_pattern — default value in destructuring, collect from left side
|
|
546
|
+
if (node.type === 'assignment_pattern') {
|
|
547
|
+
const children = adapter.getChildren(node);
|
|
548
|
+
if (children.length >= 1) {
|
|
549
|
+
collectPatternNames(children[0], adapter, sourceCode, names);
|
|
550
|
+
}
|
|
478
551
|
}
|
|
479
|
-
collect(node);
|
|
480
|
-
return names;
|
|
481
552
|
}
|
|
482
553
|
function collectChildren(node, adapter, sourceCode, names) {
|
|
483
554
|
for (const child of adapter.getChildren(node)) {
|
|
@@ -594,6 +665,11 @@ function walkAST(root, visitor) {
|
|
|
594
665
|
*
|
|
595
666
|
* This is the main entry point — call once per file before analysis.
|
|
596
667
|
* Combines import extraction, propagation, and mode-based fallback.
|
|
668
|
+
* @param adapter
|
|
669
|
+
* @param ast
|
|
670
|
+
* @param options
|
|
671
|
+
* @param sourceCode
|
|
672
|
+
* @returns
|
|
597
673
|
*/
|
|
598
674
|
export function buildProvenanceContext(ast, adapter, sourceCode, options) {
|
|
599
675
|
const mode = options.mode;
|
|
@@ -610,7 +686,11 @@ export function buildProvenanceContext(ast, adapter, sourceCode, options) {
|
|
|
610
686
|
// 3. Fallback: in hybrid mode, add identifiers that match name lists
|
|
611
687
|
// but weren't caught by provenance (R3)
|
|
612
688
|
if (mode === 'hybrid') {
|
|
613
|
-
dbProvenanced = addNameListFallbacks(dbProvenanced, sourceCode,
|
|
689
|
+
dbProvenanced = addNameListFallbacks(dbProvenanced, sourceCode, {
|
|
690
|
+
dbReceiverNames: options.dbReceiverNames ?? [],
|
|
691
|
+
dbBindingNames: options.dbBindingNames ?? [],
|
|
692
|
+
dbWrapperNames: options.dbWrapperNames ?? [],
|
|
693
|
+
});
|
|
614
694
|
}
|
|
615
695
|
// 4. In names mode, use ONLY name lists
|
|
616
696
|
if (mode === 'names') {
|
|
@@ -622,74 +702,63 @@ export function buildProvenanceContext(ast, adapter, sourceCode, options) {
|
|
|
622
702
|
mode,
|
|
623
703
|
};
|
|
624
704
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
* but weren't caught by the provenance chain (R3 hybrid mode).
|
|
628
|
-
*
|
|
629
|
-
* These entries carry `reason: 'fallback'` — visible in config detection
|
|
630
|
-
* so users can audit and tighten their chains.
|
|
631
|
-
*/
|
|
632
|
-
function addNameListFallbacks(provenanceMap, sourceCode, dbReceiverNames, dbBindingNames, dbWrapperNames) {
|
|
705
|
+
function addNameListFallbacks(provenanceMap, sourceCode, nameLists) {
|
|
706
|
+
const { dbReceiverNames, dbBindingNames, dbWrapperNames } = nameLists;
|
|
633
707
|
const result = new Map(provenanceMap);
|
|
634
|
-
|
|
635
|
-
|
|
708
|
+
addIdentifierFallbacks(result, sourceCode, dbReceiverNames, 'dbReceiverNames');
|
|
709
|
+
addBindingFallbacks(result, sourceCode, dbBindingNames);
|
|
710
|
+
addIdentifierFallbacks(result, sourceCode, dbWrapperNames, 'dbWrapperNames');
|
|
711
|
+
return result;
|
|
712
|
+
}
|
|
713
|
+
/** Add fallback entries for identifiers that appear in source but lack provenance. */
|
|
714
|
+
function addIdentifierFallbacks(result, sourceCode, names, label) {
|
|
715
|
+
for (const name of names) {
|
|
636
716
|
if (result.has(name))
|
|
637
717
|
continue; // already provenanced — provenance wins
|
|
638
|
-
// Check if this name appears as an identifier in the source
|
|
639
718
|
if (identifierAppearsInSource(sourceCode, name)) {
|
|
640
719
|
result.set(name, {
|
|
641
720
|
identifier: name,
|
|
642
721
|
reason: 'fallback',
|
|
643
|
-
source: `name list match:
|
|
722
|
+
source: `name list match: ${label} contains "${name}"`,
|
|
644
723
|
chain: [],
|
|
645
724
|
});
|
|
646
725
|
}
|
|
647
726
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
const shortName = dotIdx >= 0 ? binding.substring(dotIdx + 1) : binding;
|
|
654
|
-
// Also add the full binding path
|
|
655
|
-
if (!result.has(binding)) {
|
|
656
|
-
result.set(binding, {
|
|
657
|
-
identifier: binding,
|
|
658
|
-
reason: 'fallback',
|
|
659
|
-
source: `name list match: dbBindingNames contains "${binding}"`,
|
|
660
|
-
chain: [],
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
if (shortName !== binding && !result.has(shortName)) {
|
|
664
|
-
result.set(shortName, {
|
|
665
|
-
identifier: shortName,
|
|
666
|
-
reason: 'fallback',
|
|
667
|
-
source: `from binding ${binding}`,
|
|
668
|
-
chain: [],
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
// Scan for DB wrapper function names (e.g. d1Query, d1Exec)
|
|
674
|
-
for (const name of dbWrapperNames) {
|
|
675
|
-
if (result.has(name))
|
|
727
|
+
}
|
|
728
|
+
/** Add fallback entries for binding names like `env.DB` (plus their short forms). */
|
|
729
|
+
function addBindingFallbacks(result, sourceCode, bindings) {
|
|
730
|
+
for (const binding of bindings) {
|
|
731
|
+
if (!sourceCode.includes(binding))
|
|
676
732
|
continue;
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
733
|
+
const dotIdx = binding.lastIndexOf('.');
|
|
734
|
+
const shortName = dotIdx >= 0 ? binding.substring(dotIdx + 1) : binding;
|
|
735
|
+
if (!result.has(binding)) {
|
|
736
|
+
result.set(binding, {
|
|
737
|
+
identifier: binding,
|
|
738
|
+
reason: 'fallback',
|
|
739
|
+
source: `name list match: dbBindingNames contains "${binding}"`,
|
|
740
|
+
chain: [],
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
if (shortName !== binding && !result.has(shortName)) {
|
|
744
|
+
result.set(shortName, {
|
|
745
|
+
identifier: shortName,
|
|
680
746
|
reason: 'fallback',
|
|
681
|
-
source: `
|
|
747
|
+
source: `from binding ${binding}`,
|
|
682
748
|
chain: [],
|
|
683
749
|
});
|
|
684
750
|
}
|
|
685
751
|
}
|
|
686
|
-
return result;
|
|
687
752
|
}
|
|
688
753
|
/**
|
|
689
754
|
* Build a provenance map using ONLY name lists (for names mode).
|
|
690
755
|
*/
|
|
691
756
|
function buildNamesOnlyProvenance(sourceCode, dbReceiverNames, dbBindingNames, dbWrapperNames) {
|
|
692
|
-
return addNameListFallbacks(new Map(), sourceCode,
|
|
757
|
+
return addNameListFallbacks(new Map(), sourceCode, {
|
|
758
|
+
dbReceiverNames,
|
|
759
|
+
dbBindingNames,
|
|
760
|
+
dbWrapperNames,
|
|
761
|
+
});
|
|
693
762
|
}
|
|
694
763
|
/** Check if an identifier name appears as a standalone identifier in source. */
|
|
695
764
|
function identifierAppearsInSource(sourceCode, name) {
|
|
@@ -700,9 +769,6 @@ function identifierAppearsInSource(sourceCode, name) {
|
|
|
700
769
|
function escapeRegex(s) {
|
|
701
770
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
702
771
|
}
|
|
703
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
704
|
-
// Core detection — is this call DB-provenanced?
|
|
705
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
706
772
|
/**
|
|
707
773
|
* Determine if a call-expression node's callee is DB-provenanced.
|
|
708
774
|
*
|
|
@@ -714,11 +780,15 @@ function escapeRegex(s) {
|
|
|
714
780
|
* 2. Member expression call → is the receiver DB-provenanced AND is the
|
|
715
781
|
* method in the DB call method set?
|
|
716
782
|
* 3. ORM patterns → receiver is DB-provenanced and method matches ORM API
|
|
783
|
+
*
|
|
784
|
+
* @param node the call-expression node whose callee is under test
|
|
785
|
+
* @param query bundled adapter, source, provenance context, and DB methods
|
|
786
|
+
* @returns true if the call's callee resolves to a DB-provenanced target
|
|
717
787
|
*/
|
|
718
|
-
export function isDBProvenanced(node,
|
|
788
|
+
export function isDBProvenanced(node, query) {
|
|
719
789
|
if (node.type !== 'call_expression')
|
|
720
790
|
return false;
|
|
721
|
-
const methods =
|
|
791
|
+
const { adapter, sourceCode, context, methods } = query;
|
|
722
792
|
const calleeNode = getCallExpressionCallee(node, adapter);
|
|
723
793
|
if (!calleeNode)
|
|
724
794
|
return false;
|
|
@@ -740,7 +810,7 @@ export function isDBProvenanced(node, adapter, sourceCode, context, dbCallMethod
|
|
|
740
810
|
}
|
|
741
811
|
// Case 2: Member expression — e.g. db.prepare(...)
|
|
742
812
|
if (calleeNode.type === 'member_expression' || calleeNode.type === 'selector_expression') {
|
|
743
|
-
return isMemberExpressionDBProvenanced(calleeNode,
|
|
813
|
+
return isMemberExpressionDBProvenanced(calleeNode, query);
|
|
744
814
|
}
|
|
745
815
|
return false;
|
|
746
816
|
}
|
|
@@ -751,38 +821,14 @@ export function isDBProvenanced(node, adapter, sourceCode, context, dbCallMethod
|
|
|
751
821
|
* checks if it's in the provenance context, and verifies
|
|
752
822
|
* the method matches the DB call/ORM API.
|
|
753
823
|
*/
|
|
754
|
-
function isMemberExpressionDBProvenanced(node,
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
let current = node;
|
|
758
|
-
while (current.type === 'member_expression' || current.type === 'selector_expression') {
|
|
759
|
-
const children = adapter.getChildren(current);
|
|
760
|
-
const firstChild = children[0];
|
|
761
|
-
if (!firstChild)
|
|
762
|
-
break;
|
|
763
|
-
if (firstChild.type === 'identifier') {
|
|
764
|
-
rootReceiver = adapter.getNodeText(firstChild, sourceCode);
|
|
765
|
-
break;
|
|
766
|
-
}
|
|
767
|
-
if (firstChild.type === 'member_expression' || firstChild.type === 'selector_expression') {
|
|
768
|
-
// Compound receiver: env.DB.prepare → root is "env.DB", not "env".
|
|
769
|
-
// Returning the full text of the inner member expression ensures
|
|
770
|
-
// fallback entries match (e.g. dbBindingNames: ['env.DB']).
|
|
771
|
-
rootReceiver = adapter.getNodeText(firstChild, sourceCode);
|
|
772
|
-
break;
|
|
773
|
-
}
|
|
774
|
-
// this.db.prepare → root is a chain on `this`, check `this.xxx`
|
|
775
|
-
if (firstChild.type === 'this' || firstChild.type === 'super') {
|
|
776
|
-
rootReceiver = 'this';
|
|
777
|
-
break;
|
|
778
|
-
}
|
|
779
|
-
break;
|
|
780
|
-
}
|
|
824
|
+
function isMemberExpressionDBProvenanced(node, query) {
|
|
825
|
+
const { adapter, sourceCode, context } = query;
|
|
826
|
+
const rootReceiver = findRootReceiver(node, adapter, sourceCode);
|
|
781
827
|
if (!rootReceiver)
|
|
782
828
|
return false;
|
|
783
829
|
// In names mode, check the method name directly
|
|
784
830
|
if (context.mode === 'names') {
|
|
785
|
-
return isDBMethodCall(node,
|
|
831
|
+
return isDBMethodCall(node, query);
|
|
786
832
|
}
|
|
787
833
|
// Check if the root receiver is DB-provenanced.
|
|
788
834
|
// Compound receivers (e.g. "db.users" or "env.DB") need to match both
|
|
@@ -801,16 +847,45 @@ function isMemberExpressionDBProvenanced(node, adapter, sourceCode, context, met
|
|
|
801
847
|
if (rootReceiver !== 'this')
|
|
802
848
|
return false;
|
|
803
849
|
// For `this.xxx`, check if the method chain itself suggests DB usage
|
|
804
|
-
return isDBMethodOnThis(node,
|
|
850
|
+
return isDBMethodOnThis(node, query);
|
|
805
851
|
}
|
|
806
852
|
// Check that the method is in the DB call/ORM API
|
|
807
|
-
return isDBMethodCall(node,
|
|
853
|
+
return isDBMethodCall(node, query);
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Walk a member/selector expression chain to its root receiver text.
|
|
857
|
+
* Returns `'this'` for `this.xxx` chains and `null` when no root is found.
|
|
858
|
+
*/
|
|
859
|
+
function findRootReceiver(node, adapter, sourceCode) {
|
|
860
|
+
let current = node;
|
|
861
|
+
while (current.type === 'member_expression' || current.type === 'selector_expression') {
|
|
862
|
+
const children = adapter.getChildren(current);
|
|
863
|
+
const firstChild = children[0];
|
|
864
|
+
if (!firstChild)
|
|
865
|
+
break;
|
|
866
|
+
if (firstChild.type === 'identifier') {
|
|
867
|
+
return adapter.getNodeText(firstChild, sourceCode);
|
|
868
|
+
}
|
|
869
|
+
if (firstChild.type === 'member_expression' || firstChild.type === 'selector_expression') {
|
|
870
|
+
// Compound receiver: env.DB.prepare → root is "env.DB", not "env".
|
|
871
|
+
// Returning the full text of the inner member expression ensures
|
|
872
|
+
// fallback entries match (e.g. dbBindingNames: ['env.DB']).
|
|
873
|
+
return adapter.getNodeText(firstChild, sourceCode);
|
|
874
|
+
}
|
|
875
|
+
// this.db.prepare → root is a chain on `this`, check `this.xxx`
|
|
876
|
+
if (firstChild.type === 'this' || firstChild.type === 'super') {
|
|
877
|
+
return 'this';
|
|
878
|
+
}
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
return null;
|
|
808
882
|
}
|
|
809
883
|
/**
|
|
810
884
|
* Check if a call through a member expression uses a DB method
|
|
811
885
|
* (exec, prepare, all, etc.) or an ORM method (find, insert, etc.).
|
|
812
886
|
*/
|
|
813
|
-
function isDBMethodCall(node,
|
|
887
|
+
function isDBMethodCall(node, query) {
|
|
888
|
+
const { adapter, sourceCode, methods } = query;
|
|
814
889
|
// Walk the member expression chain and check each property
|
|
815
890
|
let current = node;
|
|
816
891
|
while (current.type === 'member_expression' ||
|
|
@@ -820,7 +895,7 @@ function isDBMethodCall(node, _adapter, _sourceCode, _context, methods) {
|
|
|
820
895
|
for (const child of children) {
|
|
821
896
|
if (child.type === 'property_identifier' ||
|
|
822
897
|
child.type === 'field_identifier') {
|
|
823
|
-
const propName =
|
|
898
|
+
const propName = adapter.getNodeText(child, sourceCode);
|
|
824
899
|
const lower = propName.toLowerCase();
|
|
825
900
|
if (methods.has(lower) || ORM_METHODS.has(lower)) {
|
|
826
901
|
return true;
|
|
@@ -843,7 +918,8 @@ function isDBMethodCall(node, _adapter, _sourceCode, _context, methods) {
|
|
|
843
918
|
* For `this.xxx.method()` calls — check if the method chain suggests
|
|
844
919
|
* DB access. Used when the receiver is `this` (not directly DB-provenanced).
|
|
845
920
|
*/
|
|
846
|
-
function isDBMethodOnThis(node,
|
|
921
|
+
function isDBMethodOnThis(node, query) {
|
|
922
|
+
const { adapter, sourceCode, methods } = query;
|
|
847
923
|
// Walk the chain: this.db.prepare → check if any property matches DB methods
|
|
848
924
|
let current = node;
|
|
849
925
|
while (current.type === 'member_expression' ||
|
|
@@ -874,6 +950,9 @@ function isDBMethodOnThis(node, adapter, sourceCode, _context, methods) {
|
|
|
874
950
|
* Check if an identifier is validator-provenanced (R4 infrastructure).
|
|
875
951
|
*
|
|
876
952
|
* This will be consumed by Spec 15's validator-bypass detection.
|
|
953
|
+
* @param context
|
|
954
|
+
* @param identifier
|
|
955
|
+
* @returns
|
|
877
956
|
*/
|
|
878
957
|
export function isValidatorProvenanced(identifier, context) {
|
|
879
958
|
return context.validatorProvenanced.has(identifier);
|
|
@@ -890,108 +969,124 @@ export function isValidatorProvenanced(identifier, context) {
|
|
|
890
969
|
* (b) can be traced back to a provenanced source through assignments.
|
|
891
970
|
*
|
|
892
971
|
* Deferred: full implementation in R2 step.
|
|
972
|
+
* @param adapter
|
|
973
|
+
* @param fileAst
|
|
974
|
+
* @param provenancedSet
|
|
975
|
+
* @param sourceCode
|
|
976
|
+
* @returns
|
|
893
977
|
*/
|
|
894
978
|
export function inferReceivers(provenancedSet, fileAst, adapter, sourceCode) {
|
|
895
979
|
const inferred = { identifiers: [], evidence: [] };
|
|
896
980
|
const seen = new Set();
|
|
897
981
|
// Step 1: Build an assignment graph — name → source text of initializer
|
|
898
982
|
const assignmentGraph = buildAssignmentGraph(fileAst, adapter, sourceCode);
|
|
983
|
+
const ctx = {
|
|
984
|
+
adapter, sourceCode, provenancedSet, assignmentGraph, seen, inferred,
|
|
985
|
+
};
|
|
899
986
|
// Step 2: Walk all call expressions whose callee is a member expression
|
|
900
|
-
walkAST(fileAst.root, (node) =>
|
|
901
|
-
if (node.type !== 'call_expression')
|
|
902
|
-
return;
|
|
903
|
-
const callee = getCallExpressionCallee(node, adapter);
|
|
904
|
-
if (!callee || (callee.type !== 'member_expression' && callee.type !== 'selector_expression'))
|
|
905
|
-
return;
|
|
906
|
-
// Extract method name (the property being called)
|
|
907
|
-
const methodName = extractMemberExpressionProperty(callee, adapter, sourceCode);
|
|
908
|
-
if (!methodName || !DB_CALL_METHODS.has(methodName.toLowerCase()))
|
|
909
|
-
return;
|
|
910
|
-
// Get the receiver identifier
|
|
911
|
-
const receiver = getMemberExpressionReceiver(callee, adapter, sourceCode);
|
|
912
|
-
if (!receiver)
|
|
913
|
-
return;
|
|
914
|
-
// Already in the provenanced set → skip (already first-class, not inferred)
|
|
915
|
-
if (provenancedSet.has(receiver))
|
|
916
|
-
return;
|
|
917
|
-
// Already added to inferred
|
|
918
|
-
if (seen.has(receiver))
|
|
919
|
-
return;
|
|
920
|
-
// Step 3: Trace receiver through assignment chain to see if it reaches
|
|
921
|
-
// a provenanced source
|
|
922
|
-
const traced = traceAssignmentChain(receiver, assignmentGraph, provenancedSet);
|
|
923
|
-
if (traced.found) {
|
|
924
|
-
seen.add(receiver);
|
|
925
|
-
inferred.identifiers.push(receiver);
|
|
926
|
-
inferred.evidence.push({
|
|
927
|
-
identifier: receiver,
|
|
928
|
-
reason: `calls .${methodName}() traced to ${traced.chains[0]} — ${traced.reason}`,
|
|
929
|
-
});
|
|
930
|
-
}
|
|
931
|
-
});
|
|
987
|
+
walkAST(fileAst.root, (node) => inferReceiverFromCall(node, ctx));
|
|
932
988
|
return inferred;
|
|
933
989
|
}
|
|
990
|
+
/** Infer a receiver from a single call_expression node, if it qualifies. */
|
|
991
|
+
function inferReceiverFromCall(node, ctx) {
|
|
992
|
+
const { adapter, sourceCode, provenancedSet, assignmentGraph, seen, inferred } = ctx;
|
|
993
|
+
if (node.type !== 'call_expression')
|
|
994
|
+
return;
|
|
995
|
+
const callee = getCallExpressionCallee(node, adapter);
|
|
996
|
+
if (!callee || (callee.type !== 'member_expression' && callee.type !== 'selector_expression'))
|
|
997
|
+
return;
|
|
998
|
+
const methodName = extractMemberExpressionProperty(callee, adapter, sourceCode);
|
|
999
|
+
if (!methodName || !DB_CALL_METHODS.has(methodName.toLowerCase()))
|
|
1000
|
+
return;
|
|
1001
|
+
const receiver = getMemberExpressionReceiver(callee, adapter, sourceCode);
|
|
1002
|
+
if (!receiver)
|
|
1003
|
+
return;
|
|
1004
|
+
// Already provenanced → first-class, not inferred; already added → skip
|
|
1005
|
+
if (provenancedSet.has(receiver) || seen.has(receiver))
|
|
1006
|
+
return;
|
|
1007
|
+
// Step 3: Trace receiver through assignment chain to a provenanced source
|
|
1008
|
+
const traced = traceAssignmentChain(receiver, assignmentGraph, provenancedSet);
|
|
1009
|
+
if (traced.found) {
|
|
1010
|
+
seen.add(receiver);
|
|
1011
|
+
inferred.identifiers.push(receiver);
|
|
1012
|
+
inferred.evidence.push({
|
|
1013
|
+
identifier: receiver,
|
|
1014
|
+
reason: `calls .${methodName}() traced to ${traced.chains[0]} — ${traced.reason}`,
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
934
1018
|
/**
|
|
935
1019
|
* Build a reverse assignment graph from the AST.
|
|
936
1020
|
* Maps variable name → the text of its initializer expression.
|
|
937
1021
|
* Handles: const/let/var declarations, parameter defaults, class fields.
|
|
938
1022
|
*/
|
|
939
1023
|
function buildAssignmentGraph(ast, adapter, sourceCode) {
|
|
940
|
-
const
|
|
1024
|
+
const ctx = {
|
|
1025
|
+
graph: new Map(),
|
|
1026
|
+
adapter,
|
|
1027
|
+
sourceCode,
|
|
1028
|
+
};
|
|
941
1029
|
walkAST(ast.root, (node, parent) => {
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
if (nameNode && valueNode) {
|
|
946
|
-
const names = extractPatternNames(nameNode, adapter, sourceCode);
|
|
947
|
-
const valueText = adapter.getNodeText(valueNode, sourceCode);
|
|
948
|
-
for (const name of names) {
|
|
949
|
-
if (!graph.has(name))
|
|
950
|
-
graph.set(name, valueText);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
return;
|
|
954
|
-
}
|
|
955
|
-
// Parameter defaults: function foo(x = <expr>)
|
|
956
|
-
if (node.type === 'assignment_pattern' &&
|
|
957
|
-
parent?.type === 'formal_parameters') {
|
|
958
|
-
const children = adapter.getChildren(node);
|
|
959
|
-
if (children.length >= 2) {
|
|
960
|
-
const leftNode = children[0];
|
|
961
|
-
const rightNode = children[1];
|
|
962
|
-
if (leftNode.type === 'identifier') {
|
|
963
|
-
const paramName = adapter.getNodeText(leftNode, sourceCode);
|
|
964
|
-
const valueText = adapter.getNodeText(rightNode, sourceCode);
|
|
965
|
-
if (!graph.has(paramName))
|
|
966
|
-
graph.set(paramName, valueText);
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
return;
|
|
970
|
-
}
|
|
971
|
-
// Class field: fieldName = <expr>
|
|
972
|
-
if (node.type === 'public_field_definition' ||
|
|
973
|
-
node.type === 'field_definition') {
|
|
974
|
-
const children = adapter.getChildren(node);
|
|
975
|
-
const nameChild = children.find((c) => c.type === 'property_identifier');
|
|
976
|
-
const valueChild = children.find((c) => c.type !== 'property_identifier' &&
|
|
977
|
-
c.type !== 'decorator' &&
|
|
978
|
-
c.type !== 'private' &&
|
|
979
|
-
c.type !== 'public' &&
|
|
980
|
-
c.type !== 'protected' &&
|
|
981
|
-
c.type !== 'static' &&
|
|
982
|
-
c.type !== 'readonly' &&
|
|
983
|
-
c.type !== 'abstract' &&
|
|
984
|
-
c.type !== '=');
|
|
985
|
-
if (nameChild && valueChild) {
|
|
986
|
-
const fieldName = adapter.getNodeText(nameChild, sourceCode);
|
|
987
|
-
const valueText = adapter.getNodeText(valueChild, sourceCode);
|
|
988
|
-
if (!graph.has(fieldName))
|
|
989
|
-
graph.set(fieldName, valueText);
|
|
990
|
-
}
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
1030
|
+
recordVariableDeclarator(node, ctx);
|
|
1031
|
+
recordParameterDefault(node, parent, ctx);
|
|
1032
|
+
recordClassField(node, ctx);
|
|
993
1033
|
});
|
|
994
|
-
return graph;
|
|
1034
|
+
return ctx.graph;
|
|
1035
|
+
}
|
|
1036
|
+
/** Record `const/let/var x = <expr>` into the assignment graph. */
|
|
1037
|
+
function recordVariableDeclarator(node, ctx) {
|
|
1038
|
+
if (node.type !== 'variable_declarator')
|
|
1039
|
+
return;
|
|
1040
|
+
const { nameNode, valueNode } = splitVariableDeclarator(node, ctx.adapter);
|
|
1041
|
+
if (!nameNode || !valueNode)
|
|
1042
|
+
return;
|
|
1043
|
+
const names = extractPatternNames(nameNode, ctx.adapter, ctx.sourceCode);
|
|
1044
|
+
const valueText = ctx.adapter.getNodeText(valueNode, ctx.sourceCode);
|
|
1045
|
+
for (const name of names) {
|
|
1046
|
+
if (!ctx.graph.has(name))
|
|
1047
|
+
ctx.graph.set(name, valueText);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
/** Record parameter defaults (`function foo(x = <expr>)`) into the graph. */
|
|
1051
|
+
function recordParameterDefault(node, parent, ctx) {
|
|
1052
|
+
if (node.type !== 'assignment_pattern' || parent?.type !== 'formal_parameters') {
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
const children = ctx.adapter.getChildren(node);
|
|
1056
|
+
if (children.length < 2)
|
|
1057
|
+
return;
|
|
1058
|
+
const leftNode = children[0];
|
|
1059
|
+
const rightNode = children[1];
|
|
1060
|
+
if (leftNode.type !== 'identifier')
|
|
1061
|
+
return;
|
|
1062
|
+
const paramName = ctx.adapter.getNodeText(leftNode, ctx.sourceCode);
|
|
1063
|
+
const valueText = ctx.adapter.getNodeText(rightNode, ctx.sourceCode);
|
|
1064
|
+
if (!ctx.graph.has(paramName))
|
|
1065
|
+
ctx.graph.set(paramName, valueText);
|
|
1066
|
+
}
|
|
1067
|
+
/** Record class-field initializers (`fieldName = <expr>`) into the graph. */
|
|
1068
|
+
function recordClassField(node, ctx) {
|
|
1069
|
+
if (node.type !== 'public_field_definition' &&
|
|
1070
|
+
node.type !== 'field_definition') {
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
const children = ctx.adapter.getChildren(node);
|
|
1074
|
+
const nameChild = children.find((c) => c.type === 'property_identifier');
|
|
1075
|
+
const valueChild = children.find((c) => c.type !== 'property_identifier' &&
|
|
1076
|
+
c.type !== 'decorator' &&
|
|
1077
|
+
c.type !== 'private' &&
|
|
1078
|
+
c.type !== 'public' &&
|
|
1079
|
+
c.type !== 'protected' &&
|
|
1080
|
+
c.type !== 'static' &&
|
|
1081
|
+
c.type !== 'readonly' &&
|
|
1082
|
+
c.type !== 'abstract' &&
|
|
1083
|
+
c.type !== '=');
|
|
1084
|
+
if (!nameChild || !valueChild)
|
|
1085
|
+
return;
|
|
1086
|
+
const fieldName = ctx.adapter.getNodeText(nameChild, ctx.sourceCode);
|
|
1087
|
+
const valueText = ctx.adapter.getNodeText(valueChild, ctx.sourceCode);
|
|
1088
|
+
if (!ctx.graph.has(fieldName))
|
|
1089
|
+
ctx.graph.set(fieldName, valueText);
|
|
995
1090
|
}
|
|
996
1091
|
/**
|
|
997
1092
|
* Trace a receiver identifier through the assignment graph to see if it
|
|
@@ -1012,10 +1107,8 @@ function traceAssignmentChain(receiver, assignmentGraph, provenancedSet) {
|
|
|
1012
1107
|
reason: provenancedSet.get(current).source,
|
|
1013
1108
|
};
|
|
1014
1109
|
}
|
|
1015
|
-
// Also check if any identifier in the initializer text matches a provenanced source
|
|
1016
1110
|
const initText = assignmentGraph.get(current);
|
|
1017
1111
|
if (initText) {
|
|
1018
|
-
// Extract identifiers from the initializer text and check against provenanced set
|
|
1019
1112
|
const idents = extractTopLevelIdentifiers(initText);
|
|
1020
1113
|
for (const ident of idents) {
|
|
1021
1114
|
if (provenancedSet.has(ident)) {
|
|
@@ -1028,27 +1121,28 @@ function traceAssignmentChain(receiver, assignmentGraph, provenancedSet) {
|
|
|
1028
1121
|
}
|
|
1029
1122
|
}
|
|
1030
1123
|
}
|
|
1031
|
-
// Move to the next link in the chain
|
|
1032
1124
|
if (!initText || visited.has(initText))
|
|
1033
1125
|
break;
|
|
1034
1126
|
visited.add(initText);
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
let foundNext = false;
|
|
1038
|
-
for (const ident of idents) {
|
|
1039
|
-
if (assignmentGraph.has(ident) && !visited.has(ident)) {
|
|
1040
|
-
chain.push(ident);
|
|
1041
|
-
current = ident;
|
|
1042
|
-
foundNext = true;
|
|
1043
|
-
break;
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
if (!foundNext)
|
|
1127
|
+
const next = findNextTraceIdentifier(initText, assignmentGraph, visited);
|
|
1128
|
+
if (!next)
|
|
1047
1129
|
break;
|
|
1130
|
+
chain.push(next);
|
|
1131
|
+
current = next;
|
|
1048
1132
|
depth++;
|
|
1049
1133
|
}
|
|
1050
1134
|
return { found: false, chains: [], reason: '' };
|
|
1051
1135
|
}
|
|
1136
|
+
/** Find the next assignment-graph identifier to trace, if any. */
|
|
1137
|
+
function findNextTraceIdentifier(initText, assignmentGraph, visited) {
|
|
1138
|
+
const idents = extractTopLevelIdentifiers(initText);
|
|
1139
|
+
for (const ident of idents) {
|
|
1140
|
+
if (assignmentGraph.has(ident) && !visited.has(ident)) {
|
|
1141
|
+
return ident;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1052
1146
|
/**
|
|
1053
1147
|
* Extract top-level identifier names from an expression text.
|
|
1054
1148
|
* E.g., "db.prepare(sql)" → ["db"], "getConnection()" → ["getConnection"]
|