code-auditor-mcp 3.4.9 → 3.4.11
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 +10 -3
- package/CHANGELOG.md +81 -0
- package/dist/analyzers/analyzerUtils.d.ts.map +1 -1
- package/dist/analyzers/analyzerUtils.js +2 -1
- package/dist/analyzers/analyzerUtils.js.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +0 -2
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +74 -99
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
- package/dist/analyzers/documentationAnalyzer.d.ts +1 -5
- package/dist/analyzers/documentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/documentationAnalyzer.js +13 -69
- package/dist/analyzers/documentationAnalyzer.js.map +1 -1
- package/dist/analyzers/invariantsAnalyzer.d.ts +1 -6
- package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/invariantsAnalyzer.js +16 -41
- package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
- package/dist/analyzers/reactAnalyzer.d.ts +20 -3
- package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/reactAnalyzer.js +4 -165
- package/dist/analyzers/reactAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +43 -69
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +62 -0
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +357 -3
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +81 -69
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +319 -325
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js +32 -37
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
- package/dist/auditRunner.d.ts +2 -7
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +306 -434
- package/dist/auditRunner.js.map +1 -1
- package/dist/cli.js +27 -5
- package/dist/cli.js.map +1 -1
- package/dist/codeIndexDB.d.ts +22 -0
- package/dist/codeIndexDB.d.ts.map +1 -1
- package/dist/codeIndexDB.js +92 -44
- package/dist/codeIndexDB.js.map +1 -1
- package/dist/config/defaults.js +3 -3
- package/dist/config/defaults.js.map +1 -1
- package/dist/conventions/conventionMiner.d.ts +5 -4
- package/dist/conventions/conventionMiner.d.ts.map +1 -1
- package/dist/conventions/conventionMiner.js +65 -25
- package/dist/conventions/conventionMiner.js.map +1 -1
- package/dist/dataPaths.js +1 -1
- package/dist/dataPaths.js.map +1 -1
- package/dist/invariants/index.d.ts +1 -1
- package/dist/invariants/index.d.ts.map +1 -1
- package/dist/invariants/index.js +1 -1
- package/dist/invariants/index.js.map +1 -1
- package/dist/invariants/ruleEngine.d.ts +38 -3
- package/dist/invariants/ruleEngine.d.ts.map +1 -1
- package/dist/invariants/ruleEngine.js +42 -174
- package/dist/invariants/ruleEngine.js.map +1 -1
- package/dist/languages/UniversalAnalyzer.d.ts +2 -2
- package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
- package/dist/languages/UniversalAnalyzer.js +3 -1
- package/dist/languages/UniversalAnalyzer.js.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +3 -0
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +60 -16
- 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 +10 -6
- package/dist/mcp-tools-shared.js.map +1 -1
- package/dist/mcp.js +0 -0
- package/dist/mcpAuditJobs.d.ts.map +1 -1
- package/dist/mcpAuditJobs.js +4 -1
- package/dist/mcpAuditJobs.js.map +1 -1
- package/dist/pipeline.d.ts +74 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +620 -0
- package/dist/pipeline.js.bak +465 -0
- package/dist/pipeline.js.bak2 +465 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/pipelineAdapters.d.ts +116 -0
- package/dist/pipelineAdapters.d.ts.map +1 -0
- package/dist/pipelineAdapters.js +1146 -0
- package/dist/pipelineAdapters.js.map +1 -0
- package/dist/pipelineTypes.d.ts +44 -0
- package/dist/pipelineTypes.d.ts.map +1 -0
- package/dist/pipelineTypes.js +118 -0
- package/dist/pipelineTypes.js.map +1 -0
- package/dist/reporting/csvReportGenerator.d.ts.map +1 -1
- package/dist/reporting/csvReportGenerator.js +2 -1
- package/dist/reporting/csvReportGenerator.js.map +1 -1
- package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
- package/dist/reporting/jsonReportGenerator.js +4 -2
- package/dist/reporting/jsonReportGenerator.js.map +1 -1
- package/dist/reporting/sarifReportGenerator.d.ts.map +1 -1
- package/dist/reporting/sarifReportGenerator.js +2 -1
- package/dist/reporting/sarifReportGenerator.js.map +1 -1
- package/dist/styles/cssAstExtractor.d.ts +46 -0
- package/dist/styles/cssAstExtractor.d.ts.map +1 -0
- package/dist/styles/cssAstExtractor.js +439 -0
- package/dist/styles/cssAstExtractor.js.map +1 -0
- package/dist/styles/styleExtractor.d.ts +2 -2
- package/dist/styles/styleExtractor.d.ts.map +1 -1
- package/dist/styles/styleExtractor.js +69 -16
- package/dist/styles/styleExtractor.js.map +1 -1
- package/dist/styles/styleIndexer.d.ts.map +1 -1
- package/dist/styles/styleIndexer.js +4 -0
- package/dist/styles/styleIndexer.js.map +1 -1
- package/dist/styles/tailwindProbe.d.ts +2 -0
- package/dist/styles/tailwindProbe.d.ts.map +1 -1
- package/dist/styles/tailwindProbe.js +5 -5
- package/dist/styles/tailwindProbe.js.map +1 -1
- package/dist/styles/tailwindUtilityExpander.d.ts +3 -0
- package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
- package/dist/styles/tailwindUtilityExpander.js +7 -0
- package/dist/styles/tailwindUtilityExpander.js.map +1 -1
- package/dist/types.d.ts +216 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/fileDiscovery.d.ts +4 -0
- package/dist/utils/fileDiscovery.d.ts.map +1 -1
- package/dist/utils/fileDiscovery.js +7 -1
- package/dist/utils/fileDiscovery.js.map +1 -1
- package/dist/utils/reactDetection.d.ts.map +1 -1
- package/dist/utils/reactDetection.js +32 -0
- package/dist/utils/reactDetection.js.map +1 -1
- package/package.json +2 -4
- package/plugin/.claude-plugin/plugin.json +1 -1
|
@@ -8,10 +8,30 @@
|
|
|
8
8
|
* R2.5: Legacy scan-all-strings path DELETED.
|
|
9
9
|
* R7: schema/unknown-table severity is "suggestion".
|
|
10
10
|
*/
|
|
11
|
+
import fs from 'fs/promises';
|
|
12
|
+
import path from 'path';
|
|
11
13
|
import { UniversalAnalyzer } from '../../languages/UniversalAnalyzer.js';
|
|
12
14
|
import picomatch from 'picomatch';
|
|
13
15
|
import { buildProvenanceContext, isDBProvenanced, DB_CALL_METHODS, } from '../provenance.js';
|
|
14
16
|
import { OrmAdapterRegistry } from '../orm/index.js';
|
|
17
|
+
import { makeVisitorStatus } from '../../pipeline.js';
|
|
18
|
+
/**
|
|
19
|
+
* Single-source constants for SQL context detection.
|
|
20
|
+
*
|
|
21
|
+
* These are the ground-truth defaults. DEFAULT_SCHEMA_CONFIG references them,
|
|
22
|
+
* and every inline fallback dereferences them directly — so ?? narrowing works
|
|
23
|
+
* (TypeScript infers `string[]`, not `string[] | undefined` from the optional
|
|
24
|
+
* SchemaAnalyzerConfig fields).
|
|
25
|
+
*
|
|
26
|
+
* Trimmed to D1/Workers DB patterns only (4 receivers, 6 methods).
|
|
27
|
+
* Broader entries like 'connection'/'client'/'query'/'get'/'each' matched
|
|
28
|
+
* non-DB code (WebSocket, Map, jQuery, vector stores), causing phantom
|
|
29
|
+
* cross-domain lifecycle violations. See CHANGELOG 3.4.9 accuracy fix.
|
|
30
|
+
*/
|
|
31
|
+
export const DB_RECEIVER_NAMES = ['db', 'database', 'sql', 'stmt'];
|
|
32
|
+
export const DB_CALL_METHOD_NAMES = ['exec', 'prepare', 'batch', 'run', 'all', 'first'];
|
|
33
|
+
export const DB_BINDING_NAMES = ['env.DB'];
|
|
34
|
+
export const SQL_TAG_NAMES = ['sql', 'db'];
|
|
15
35
|
export const DEFAULT_SCHEMA_CONFIG = {
|
|
16
36
|
enableTableUsageTracking: true,
|
|
17
37
|
checkMissingReferences: true,
|
|
@@ -29,53 +49,79 @@ export const DEFAULT_SCHEMA_CONFIG = {
|
|
|
29
49
|
strictMode: false,
|
|
30
50
|
allowAdditionalProperties: true,
|
|
31
51
|
// Spec-17 R2 defaults
|
|
32
|
-
sqlTagNames: [
|
|
33
|
-
dbReceiverNames: [
|
|
34
|
-
dbCallMethods: [
|
|
35
|
-
dbBindingNames: [
|
|
52
|
+
sqlTagNames: [...SQL_TAG_NAMES],
|
|
53
|
+
dbReceiverNames: [...DB_RECEIVER_NAMES],
|
|
54
|
+
dbCallMethods: [...DB_CALL_METHOD_NAMES],
|
|
55
|
+
dbBindingNames: [...DB_BINDING_NAMES],
|
|
36
56
|
fileGateGlobs: ['**/*.sql', '**/migrations/**'],
|
|
37
57
|
schemaFiles: [],
|
|
38
58
|
};
|
|
39
|
-
import { promises as fs } from 'fs';
|
|
40
|
-
import * as path from 'path';
|
|
41
|
-
import { CodeIndexDB } from '../../codeIndexDB.js';
|
|
42
59
|
export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
43
60
|
name = 'schema';
|
|
44
61
|
description = 'Analyzes code against database schemas and validates JSON schemas';
|
|
45
62
|
category = 'database';
|
|
46
63
|
// Track references across files
|
|
47
64
|
tableReferences = new Map();
|
|
48
|
-
//
|
|
65
|
+
// Spec 25 B4 — Queue schema records for the pipeline to write after stage 2.
|
|
66
|
+
// Was: direct CodeIndexDB.getInstance() call in recordTableUsage.
|
|
67
|
+
_pendingSchemaRecords = { clearFiles: [], usages: [] };
|
|
49
68
|
projectRoot;
|
|
69
|
+
/**
|
|
70
|
+
* Strip SQL identifier delimiters: backticks or double-quotes.
|
|
71
|
+
*/
|
|
72
|
+
stripIdentifier(name) {
|
|
73
|
+
if ((name.startsWith('`') && name.endsWith('`')) ||
|
|
74
|
+
(name.startsWith('"') && name.endsWith('"'))) {
|
|
75
|
+
return name.slice(1, -1);
|
|
76
|
+
}
|
|
77
|
+
return name;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Parse a migration SQL source and apply stateful CREATE/DROP/RENAME
|
|
81
|
+
* operations to the given table set in migration order.
|
|
82
|
+
*/
|
|
83
|
+
processMigrationSource(source, tables) {
|
|
84
|
+
// Sequential state machine: apply CREATE/DROP/RENAME in statement order
|
|
85
|
+
// within each migration file. Fixes the rename-replay bug where CREATE
|
|
86
|
+
// after RENAME in the same file was silently deleted by the old three-pass
|
|
87
|
+
// approach (all CREATE then all DROP then all RENAME).
|
|
88
|
+
const ddlRe = /(CREATE)\s+(?:VIRTUAL\s+)?TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(`[^`]+`|"[^"]+"|\w+)|(DROP)\s+TABLE\s+(?:IF\s+EXISTS\s+)?(`[^`]+`|"[^"]+"|\w+)|(ALTER)\s+TABLE\s+(`[^`]+`|"[^"]+"|\w+)\s+RENAME\s+TO\s+(`[^`]+`|"[^"]+"|\w+)/gi;
|
|
89
|
+
let match;
|
|
90
|
+
while ((match = ddlRe.exec(source)) !== null) {
|
|
91
|
+
const op = match[1] || match[3] || match[5];
|
|
92
|
+
if (op === 'CREATE') {
|
|
93
|
+
tables.add(this.stripIdentifier(match[2]));
|
|
94
|
+
}
|
|
95
|
+
else if (op === 'DROP') {
|
|
96
|
+
tables.delete(this.stripIdentifier(match[4]));
|
|
97
|
+
}
|
|
98
|
+
else if (op === 'ALTER') {
|
|
99
|
+
tables.delete(this.stripIdentifier(match[6]));
|
|
100
|
+
tables.add(this.stripIdentifier(match[7]));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Standalone analyze() override for backward compatibility with direct analyzer
|
|
106
|
+
* calls (e.g., tests and non-pipeline audit paths). All production analysis now
|
|
107
|
+
* flows through the pipeline visitors, but this method is preserved so tests
|
|
108
|
+
* that call analyzer.analyze([file], config) continue to work.
|
|
109
|
+
*/
|
|
50
110
|
async analyze(files, config) {
|
|
51
111
|
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
52
112
|
const codeFiles = files.filter(f => !f.endsWith('.json'));
|
|
53
|
-
//
|
|
54
|
-
// when no schemas are configured.
|
|
55
|
-
//
|
|
56
|
-
// Spec 24 Item 2 — wrangler.toml is the external authority for D1 projects:
|
|
57
|
-
// parse [[d1_databases]] migration_dir, walk .sql files statefully with
|
|
58
|
-
// CREATE/DROP/RENAME to derive the current table catalog.
|
|
59
|
-
//
|
|
60
|
-
// Spec 24 Item 3 — schemaFiles config lets users point at a full snapshot
|
|
61
|
-
// (e.g., snapshots/schema.sql) for easy mode.
|
|
62
|
-
//
|
|
63
|
-
// Spec 24 Item 4 — Auto-discover from ORM schema definitions (Drizzle
|
|
64
|
-
// pgTable/mysqlTable/sqliteTable calls and Prisma schema.prisma model blocks)
|
|
65
|
-
// via import provenance.
|
|
66
|
-
//
|
|
67
|
-
// Discovery priority: wrangler.toml > schemaFiles > migration glob walk > ORM.
|
|
113
|
+
// Auto-discover known tables when no schemas are configured.
|
|
68
114
|
const schemas = config.schemas;
|
|
69
115
|
const projectRoot = config.projectRoot || process.cwd();
|
|
70
116
|
this.projectRoot = projectRoot;
|
|
71
117
|
if (!schemas || schemas.length === 0) {
|
|
72
|
-
const fromWrangler = await this.
|
|
118
|
+
const fromWrangler = await this._discoverTablesFromWrangler(projectRoot);
|
|
73
119
|
const schemaFiles = config.schemaFiles;
|
|
74
120
|
const fromSchemaFiles = schemaFiles && schemaFiles.length > 0
|
|
75
|
-
? await this.
|
|
121
|
+
? await this._discoverTablesFromSchemaFiles(schemaFiles, projectRoot)
|
|
76
122
|
: new Set();
|
|
77
|
-
const fromMigrations = await this.
|
|
78
|
-
const fromOrm = await this.
|
|
123
|
+
const fromMigrations = await this._discoverTablesFromMigrations(projectRoot, config);
|
|
124
|
+
const fromOrm = await this._discoverTablesFromOrmSchemas(codeFiles);
|
|
79
125
|
const discovered = new Set([
|
|
80
126
|
...fromWrangler,
|
|
81
127
|
...fromSchemaFiles,
|
|
@@ -94,57 +140,127 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
94
140
|
}
|
|
95
141
|
const codeResult = codeFiles.length > 0 ? await super.analyze(codeFiles, config) : {
|
|
96
142
|
violations: [],
|
|
143
|
+
executionTime: 0,
|
|
144
|
+
status: makeVisitorStatus(0),
|
|
145
|
+
analyzerName: this.name,
|
|
146
|
+
errors: [],
|
|
147
|
+
filesProcessed: 0,
|
|
148
|
+
};
|
|
149
|
+
// Adapt JSON handling to the pipeline-style analyzeJsonSchemas(Map) signature.
|
|
150
|
+
let jsonResult = {
|
|
151
|
+
violations: [],
|
|
152
|
+
executionTime: 0,
|
|
153
|
+
status: makeVisitorStatus(0),
|
|
154
|
+
analyzerName: this.name,
|
|
97
155
|
errors: [],
|
|
98
156
|
filesProcessed: 0,
|
|
99
|
-
executionTime: 0
|
|
100
157
|
};
|
|
101
|
-
|
|
158
|
+
if (jsonFiles.length > 0) {
|
|
159
|
+
const jsonContents = new Map();
|
|
160
|
+
for (const file of jsonFiles) {
|
|
161
|
+
try {
|
|
162
|
+
const raw = await fs.readFile(file, 'utf8');
|
|
163
|
+
let parsed = null;
|
|
164
|
+
try {
|
|
165
|
+
parsed = JSON.parse(raw);
|
|
166
|
+
}
|
|
167
|
+
catch { /* not valid JSON */ }
|
|
168
|
+
jsonContents.set(file, { parsed, raw });
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// Skip unreadable files
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
jsonResult = this.analyzeJsonSchemas(jsonContents, config);
|
|
175
|
+
}
|
|
102
176
|
return {
|
|
103
177
|
violations: [...codeResult.violations, ...jsonResult.violations],
|
|
178
|
+
executionTime: (codeResult.executionTime || 0) + (jsonResult.executionTime || 0),
|
|
179
|
+
status: makeVisitorStatus((codeResult.filesProcessed ?? 0) + (jsonResult.filesProcessed ?? 0)),
|
|
180
|
+
analyzerName: this.name,
|
|
104
181
|
errors: [...(codeResult.errors || []), ...(jsonResult.errors || [])],
|
|
105
|
-
filesProcessed: codeResult.filesProcessed + jsonResult.filesProcessed,
|
|
106
|
-
executionTime: (codeResult.executionTime || 0) + (jsonResult.executionTime || 0)
|
|
182
|
+
filesProcessed: (codeResult.filesProcessed ?? 0) + (jsonResult.filesProcessed ?? 0),
|
|
107
183
|
};
|
|
108
184
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
const source = await fs.readFile(file, 'utf8');
|
|
135
|
-
this.processMigrationSource(source, tables);
|
|
185
|
+
// analyzeAST is required by the protected abstract in UniversalAnalyzer.
|
|
186
|
+
// In production, all schema analysis flows through the pipeline visitors.
|
|
187
|
+
// This method is preserved for backward compatibility with direct test calls.
|
|
188
|
+
async analyzeAST(ast, adapter, config, sourceCode) {
|
|
189
|
+
const violations = [];
|
|
190
|
+
const finalConfig = { ...DEFAULT_SCHEMA_CONFIG, ...config };
|
|
191
|
+
// Spec 21: Build provenance context for this file (R1 — provenance-primary detection)
|
|
192
|
+
const detectionMode = config.detection?.mode ?? 'hybrid';
|
|
193
|
+
const provenanceContext = buildProvenanceContext(ast, adapter, sourceCode, {
|
|
194
|
+
mode: detectionMode,
|
|
195
|
+
dbReceiverNames: finalConfig.dbReceiverNames ?? DEFAULT_SCHEMA_CONFIG.dbReceiverNames,
|
|
196
|
+
dbBindingNames: finalConfig.dbBindingNames ?? DEFAULT_SCHEMA_CONFIG.dbBindingNames,
|
|
197
|
+
dbCallMethods: finalConfig.dbCallMethods ?? DEFAULT_SCHEMA_CONFIG.dbCallMethods,
|
|
198
|
+
});
|
|
199
|
+
// R2.2 — File gate: only analyze files with DB context (Spec 21: provenance-based)
|
|
200
|
+
if (!this.passesFileGate(ast.filePath, sourceCode, finalConfig, provenanceContext)) {
|
|
201
|
+
return violations;
|
|
202
|
+
}
|
|
203
|
+
// Get available schemas
|
|
204
|
+
const schemas = finalConfig.schemas || [];
|
|
205
|
+
const allTables = new Set();
|
|
206
|
+
for (const schema of schemas) {
|
|
207
|
+
for (const table of schema.tables) {
|
|
208
|
+
allTables.add(table.name);
|
|
136
209
|
}
|
|
137
|
-
|
|
138
|
-
|
|
210
|
+
}
|
|
211
|
+
if (finalConfig.requiredSchemas && finalConfig.requiredSchemas.length > 0 && schemas.length === 0) {
|
|
212
|
+
violations.push(this.createViolation(ast.filePath, { line: 1, column: 1 }, 'No database schemas loaded for analysis', 'warning', 'missing-schemas', undefined, 'top-level:missing-schemas'));
|
|
213
|
+
return violations;
|
|
214
|
+
}
|
|
215
|
+
// R2.1 — AST-based table reference extraction (replaces legacy regex scan-all-strings)
|
|
216
|
+
// Spec 21: Uses provenance context for DB-call pattern detection
|
|
217
|
+
const tableRefs = this.findTableReferences(ast, adapter, sourceCode, finalConfig, provenanceContext, allTables);
|
|
218
|
+
// Spec 15 R1 — Record schema usage for cross-domain lifecycle analysis.
|
|
219
|
+
// Idempotent per-file: clear stale entries before inserting fresh references.
|
|
220
|
+
if (finalConfig.enableTableUsageTracking) {
|
|
221
|
+
this.recordTableUsage(ast, adapter, ast.filePath, tableRefs);
|
|
222
|
+
}
|
|
223
|
+
// Check for missing table references — R2.4: Levenshtein suggestions
|
|
224
|
+
if (finalConfig.checkMissingReferences) {
|
|
225
|
+
// Spec 24 Item 4 Part B — 10:1 fail-open ratio.
|
|
226
|
+
const unknownRefs = tableRefs.filter(ref => !allTables.has(ref.table) && !this.isSystemTable(ref.table));
|
|
227
|
+
const knownCount = allTables.size;
|
|
228
|
+
const unknownCount = unknownRefs.length;
|
|
229
|
+
if (knownCount === 0 || unknownCount / Math.max(knownCount, 1) > 10) {
|
|
230
|
+
const displayRatio = knownCount === 0 ? '∞' : (unknownCount / Math.max(knownCount, 1)).toFixed(1);
|
|
231
|
+
// Silently skip — in direct test mode we don't emit console warnings
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
for (const ref of unknownRefs) {
|
|
235
|
+
const suggestions = this.getNearestTableSuggestions(ref.table, allTables, 2);
|
|
236
|
+
const msg = suggestions.length > 0
|
|
237
|
+
? `Reference to unknown table '${ref.table}' (${ref.type}). Did you mean: ${suggestions.join(', ')}?`
|
|
238
|
+
: `Reference to unknown table '${ref.table}' (${ref.type})`;
|
|
239
|
+
violations.push(this.createViolation(ast.filePath, ref.location, msg, 'suggestion', 'unknown-table', undefined, ref.table));
|
|
240
|
+
}
|
|
139
241
|
}
|
|
140
242
|
}
|
|
141
|
-
|
|
243
|
+
// Check naming conventions
|
|
244
|
+
if (finalConfig.checkNamingConventions) {
|
|
245
|
+
violations.push(...this.checkNamingConventions(tableRefs, ast.filePath));
|
|
246
|
+
}
|
|
247
|
+
// Check query patterns
|
|
248
|
+
if (finalConfig.validateQueryPatterns) {
|
|
249
|
+
violations.push(...this.checkQueryPatterns(ast, adapter, sourceCode, finalConfig));
|
|
250
|
+
}
|
|
251
|
+
// Check for SQL injection patterns
|
|
252
|
+
violations.push(...this.checkSQLInjection(ast, adapter, sourceCode));
|
|
253
|
+
return violations;
|
|
142
254
|
}
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// Auto-discovery helpers — used by the standalone analyze() override and
|
|
257
|
+
// the pre-pipeline discovery phase in auditRunner.
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
143
259
|
/**
|
|
144
260
|
* Walk project root recursively, returning files matching any of the given
|
|
145
261
|
* picomatch globs. Skips node_modules and dot-directories.
|
|
146
262
|
*/
|
|
147
|
-
async
|
|
263
|
+
async _walkFiles(root, globs) {
|
|
148
264
|
const results = [];
|
|
149
265
|
async function walk(dir) {
|
|
150
266
|
let names;
|
|
@@ -170,7 +286,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
170
286
|
await walk(fullPath);
|
|
171
287
|
}
|
|
172
288
|
else if (stat.isFile()) {
|
|
173
|
-
// Check picomatch against the path relative to root
|
|
174
289
|
const relative = path.relative(root, fullPath);
|
|
175
290
|
const matched = globs.some(g => picomatch.isMatch(relative, g));
|
|
176
291
|
if (matched) {
|
|
@@ -182,115 +297,32 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
182
297
|
await walk(root);
|
|
183
298
|
return results;
|
|
184
299
|
}
|
|
185
|
-
|
|
186
|
-
* Strip SQL identifier delimiters: backticks or double-quotes.
|
|
187
|
-
*/
|
|
188
|
-
stripIdentifier(name) {
|
|
189
|
-
if ((name.startsWith('`') && name.endsWith('`')) ||
|
|
190
|
-
(name.startsWith('"') && name.endsWith('"'))) {
|
|
191
|
-
return name.slice(1, -1);
|
|
192
|
-
}
|
|
193
|
-
return name;
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Parse a migration SQL source and apply stateful CREATE/DROP/RENAME
|
|
197
|
-
* operations to the given table set in migration order.
|
|
198
|
-
*/
|
|
199
|
-
processMigrationSource(source, tables) {
|
|
200
|
-
// Sequential state machine: apply CREATE/DROP/RENAME in statement order
|
|
201
|
-
// within each migration file. Fixes the rename-replay bug where CREATE
|
|
202
|
-
// after RENAME in the same file was silently deleted by the old three-pass
|
|
203
|
-
// approach (all CREATE then all DROP then all RENAME).
|
|
204
|
-
const ddlRe = /(CREATE)\s+(?:VIRTUAL\s+)?TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(`[^`]+`|"[^"]+"|\w+)|(DROP)\s+TABLE\s+(?:IF\s+EXISTS\s+)?(`[^`]+`|"[^"]+"|\w+)|(ALTER)\s+TABLE\s+(`[^`]+`|"[^"]+"|\w+)\s+RENAME\s+TO\s+(`[^`]+`|"[^"]+"|\w+)/gi;
|
|
205
|
-
let match;
|
|
206
|
-
while ((match = ddlRe.exec(source)) !== null) {
|
|
207
|
-
const op = match[1] || match[3] || match[5];
|
|
208
|
-
if (op === 'CREATE') {
|
|
209
|
-
tables.add(this.stripIdentifier(match[2]));
|
|
210
|
-
}
|
|
211
|
-
else if (op === 'DROP') {
|
|
212
|
-
tables.delete(this.stripIdentifier(match[4]));
|
|
213
|
-
}
|
|
214
|
-
else if (op === 'ALTER') {
|
|
215
|
-
tables.delete(this.stripIdentifier(match[6]));
|
|
216
|
-
tables.add(this.stripIdentifier(match[7]));
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Spec 24 Item 4 — Auto-discover known tables from ORM schema definitions.
|
|
222
|
-
*
|
|
223
|
-
* Uses import provenance to identify schema files (files importing Drizzle
|
|
224
|
-
* table constructors from drizzle-orm) and Prisma's canonical schema.prisma
|
|
225
|
-
* filename. Extracts table/model names and feeds them into allTables so the
|
|
226
|
-
* unknown-table detector works without explicit user config.
|
|
227
|
-
*
|
|
228
|
-
* Drizzle: scans .ts/.tsx/.js/.jsx files that import from drizzle-orm for
|
|
229
|
-
* pgTable/mysqlTable/sqliteTable('tableName', ...) calls.
|
|
230
|
-
* Prisma: scans schema.prisma files for model Name { ... } blocks.
|
|
231
|
-
*/
|
|
232
|
-
async discoverTablesFromOrmSchemas(files) {
|
|
300
|
+
async _discoverTablesFromMigrations(projectRoot, config) {
|
|
233
301
|
const tables = new Set();
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (/from\s+['"]drizzle-orm/.test(source)) {
|
|
242
|
-
const builderRegex = /(?:pgTable|mysqlTable|sqliteTable)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
243
|
-
let match;
|
|
244
|
-
while ((match = builderRegex.exec(source)) !== null) {
|
|
245
|
-
tables.add(match[1]);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
catch {
|
|
250
|
-
// Skip unreadable files — discovery is best-effort
|
|
251
|
-
}
|
|
302
|
+
const gateGlobs = config.fileGateGlobs ?? ['**/*.sql', '**/migrations/**'];
|
|
303
|
+
const walkedFiles = await this._walkFiles(projectRoot, gateGlobs);
|
|
304
|
+
walkedFiles.sort();
|
|
305
|
+
for (const file of walkedFiles) {
|
|
306
|
+
try {
|
|
307
|
+
const source = await fs.readFile(file, 'utf8');
|
|
308
|
+
this.processMigrationSource(source, tables);
|
|
252
309
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
const source = await fs.readFile(file, 'utf8');
|
|
257
|
-
const modelRegex = /model\s+(\w+)\s*\{/g;
|
|
258
|
-
let match;
|
|
259
|
-
while ((match = modelRegex.exec(source)) !== null) {
|
|
260
|
-
tables.add(match[1]);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
catch {
|
|
264
|
-
// Skip unreadable files — discovery is best-effort
|
|
265
|
-
}
|
|
310
|
+
catch {
|
|
311
|
+
// Skip unreadable files
|
|
266
312
|
}
|
|
267
313
|
}
|
|
268
314
|
return tables;
|
|
269
315
|
}
|
|
270
|
-
|
|
271
|
-
* Spec 24 Item 2 — Discover tables from wrangler.toml for D1/Cloudflare Workers projects.
|
|
272
|
-
*
|
|
273
|
-
* wrangler.toml is the external authority declaring where D1 migrations live.
|
|
274
|
-
* Parses it for [[d1_databases]] blocks with migrations_dir, then walks the
|
|
275
|
-
* migration directory reading .sql files in alphanumeric order (migration
|
|
276
|
-
* order). Processes statefully in migration order: CREATE adds, DROP removes,
|
|
277
|
-
* RENAME moves inline (delete old + add new) so recreated tables survive.
|
|
278
|
-
*
|
|
279
|
-
* Returns only the current table set — not historical/transient names.
|
|
280
|
-
*/
|
|
281
|
-
async discoverTablesFromWrangler(projectRoot) {
|
|
316
|
+
async _discoverTablesFromWrangler(projectRoot) {
|
|
282
317
|
const tables = new Set();
|
|
283
|
-
// 1. Look for wrangler.toml in project root
|
|
284
318
|
const wranglerPath = path.join(projectRoot, 'wrangler.toml');
|
|
285
319
|
let wranglerContent;
|
|
286
320
|
try {
|
|
287
321
|
wranglerContent = await fs.readFile(wranglerPath, 'utf8');
|
|
288
322
|
}
|
|
289
323
|
catch {
|
|
290
|
-
return tables; // No wrangler.toml
|
|
324
|
+
return tables; // No wrangler.toml
|
|
291
325
|
}
|
|
292
|
-
// 2. Parse [[d1_databases]] blocks for migrations_dir
|
|
293
|
-
// Simple TOML section parser — no dependency needed for this narrow use case
|
|
294
326
|
const migrationDirs = [];
|
|
295
327
|
let inD1Block = false;
|
|
296
328
|
for (const line of wranglerContent.split('\n')) {
|
|
@@ -300,7 +332,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
300
332
|
continue;
|
|
301
333
|
}
|
|
302
334
|
if (inD1Block && trimmed.startsWith('[')) {
|
|
303
|
-
// Next TOML section — exit d1_databases block
|
|
304
335
|
inD1Block = false;
|
|
305
336
|
continue;
|
|
306
337
|
}
|
|
@@ -311,7 +342,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
311
342
|
}
|
|
312
343
|
}
|
|
313
344
|
}
|
|
314
|
-
// 3. Walk each migration directory, read .sql files in alphanumeric order
|
|
315
345
|
for (const migDir of migrationDirs) {
|
|
316
346
|
const absDir = path.resolve(projectRoot, migDir);
|
|
317
347
|
let entries;
|
|
@@ -320,10 +350,10 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
320
350
|
entries = dirents
|
|
321
351
|
.filter(e => e.isFile() && e.name.endsWith('.sql'))
|
|
322
352
|
.map(e => e.name)
|
|
323
|
-
.sort();
|
|
353
|
+
.sort();
|
|
324
354
|
}
|
|
325
355
|
catch {
|
|
326
|
-
continue;
|
|
356
|
+
continue;
|
|
327
357
|
}
|
|
328
358
|
for (const entry of entries) {
|
|
329
359
|
const filePath = path.join(absDir, entry);
|
|
@@ -338,23 +368,12 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
338
368
|
}
|
|
339
369
|
return tables;
|
|
340
370
|
}
|
|
341
|
-
|
|
342
|
-
* Spec 24 Item 3 — Discover tables from explicit schema file paths.
|
|
343
|
-
*
|
|
344
|
-
* Reads each file path (resolved against projectRoot) and extracts
|
|
345
|
-
* CREATE TABLE [IF NOT EXISTS] name statements. Supports the common
|
|
346
|
-
* D1 pattern where users snapshot their full schema to a single .sql file
|
|
347
|
-
* (e.g., snapshots/schema.sql).
|
|
348
|
-
*/
|
|
349
|
-
async discoverTablesFromSchemaFiles(schemaFiles, projectRoot) {
|
|
371
|
+
async _discoverTablesFromSchemaFiles(schemaFiles, projectRoot) {
|
|
350
372
|
const tables = new Set();
|
|
351
373
|
for (const file of schemaFiles) {
|
|
352
374
|
const absPath = path.resolve(projectRoot, file);
|
|
353
375
|
try {
|
|
354
376
|
const source = await fs.readFile(absPath, 'utf8');
|
|
355
|
-
// Only extract CREATE TABLE — these are snapshots, not migration streams,
|
|
356
|
-
// so no stateful DROP/RENAME processing needed.
|
|
357
|
-
// v3.4.8: Added VIRTUAL TABLE support (FTS tables) and quoted/backtick identifiers
|
|
358
377
|
const createRe = /CREATE\s+(?:VIRTUAL\s+)?TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(`[^`]+`|"[^"]+"|\w+)/gi;
|
|
359
378
|
let match;
|
|
360
379
|
while ((match = createRe.exec(source)) !== null) {
|
|
@@ -362,94 +381,45 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
362
381
|
}
|
|
363
382
|
}
|
|
364
383
|
catch {
|
|
365
|
-
// Skip unreadable
|
|
384
|
+
// Skip unreadable files
|
|
366
385
|
}
|
|
367
386
|
}
|
|
368
387
|
return tables;
|
|
369
388
|
}
|
|
370
|
-
async
|
|
371
|
-
const
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
// Get available schemas
|
|
390
|
-
const schemas = finalConfig.schemas || [];
|
|
391
|
-
const allTables = new Set();
|
|
392
|
-
for (const schema of schemas) {
|
|
393
|
-
for (const table of schema.tables) {
|
|
394
|
-
allTables.add(table.name);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
if (finalConfig.requiredSchemas && finalConfig.requiredSchemas.length > 0 && schemas.length === 0) {
|
|
398
|
-
violations.push(this.createViolation(ast.filePath, { line: 1, column: 1 }, 'No database schemas loaded for analysis', 'warning', 'missing-schemas', undefined, 'top-level:missing-schemas'));
|
|
399
|
-
return violations;
|
|
400
|
-
}
|
|
401
|
-
// R2.1 — AST-based table reference extraction (replaces legacy regex scan-all-strings)
|
|
402
|
-
// Spec 21: Uses provenance context for DB-call pattern detection
|
|
403
|
-
const tableRefs = this.findTableReferences(ast, adapter, sourceCode, finalConfig, provenanceContext, allTables);
|
|
404
|
-
// Spec 15 R1 — Record schema usage for cross-domain lifecycle analysis.
|
|
405
|
-
// Idempotent per-file: clear stale entries before inserting fresh references.
|
|
406
|
-
if (finalConfig.enableTableUsageTracking) {
|
|
407
|
-
this.recordTableUsage(ast, adapter, ast.filePath, tableRefs);
|
|
408
|
-
}
|
|
409
|
-
// Check for missing table references — R2.4: Levenshtein suggestions
|
|
410
|
-
if (finalConfig.checkMissingReferences) {
|
|
411
|
-
// Spec 24 Item 4 Part B — 10:1 fail-open ratio.
|
|
412
|
-
// When unknown table references vastly outnumber known tables, the
|
|
413
|
-
// schema catalog is likely incomplete (e.g. external/managed tables).
|
|
414
|
-
// Disable the rule with a warning instead of flooding the output with
|
|
415
|
-
// false positives.
|
|
416
|
-
const unknownRefs = tableRefs.filter(ref => !allTables.has(ref.table) && !this.isSystemTable(ref.table));
|
|
417
|
-
const knownCount = allTables.size;
|
|
418
|
-
const unknownCount = unknownRefs.length;
|
|
419
|
-
// Spec 24 Item 4 Part B — 10:1 fail-open ratio.
|
|
420
|
-
// When zero known tables: a detector that knows zero tables may not
|
|
421
|
-
// call anything unknown — every reference is "unknown" by construction.
|
|
422
|
-
// When known tables exist: disable if unknown:known ratio exceeds 10:1.
|
|
423
|
-
if (knownCount === 0 || unknownCount / Math.max(knownCount, 1) > 10) {
|
|
424
|
-
const displayRatio = knownCount === 0 ? '∞' : (unknownCount / Math.max(knownCount, 1)).toFixed(1);
|
|
425
|
-
console.error(`[code-auditor] unknown-table rule disabled: ` +
|
|
426
|
-
`${unknownCount} unknown refs vs ${knownCount} known tables ` +
|
|
427
|
-
`(ratio ${displayRatio}:1 exceeds 10:1). ` +
|
|
428
|
-
`Add schemas to .codeauditor.json or ORM schema files.`);
|
|
429
|
-
// Skip unknown-table findings — fall through to column refs below
|
|
389
|
+
async _discoverTablesFromOrmSchemas(files) {
|
|
390
|
+
const tables = new Set();
|
|
391
|
+
for (const file of files) {
|
|
392
|
+
const lowerFile = file.toLowerCase();
|
|
393
|
+
if (/\.(ts|tsx|js|jsx)$/i.test(file)) {
|
|
394
|
+
try {
|
|
395
|
+
const source = await fs.readFile(file, 'utf8');
|
|
396
|
+
if (/from\s+['"]drizzle-orm/.test(source)) {
|
|
397
|
+
const builderRegex = /(?:pgTable|mysqlTable|sqliteTable)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
398
|
+
let match;
|
|
399
|
+
while ((match = builderRegex.exec(source)) !== null) {
|
|
400
|
+
tables.add(match[1]);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
// Skip unreadable files
|
|
406
|
+
}
|
|
430
407
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
408
|
+
if (file.endsWith('schema.prisma') || file.endsWith('\\schema.prisma')) {
|
|
409
|
+
try {
|
|
410
|
+
const source = await fs.readFile(file, 'utf8');
|
|
411
|
+
const modelRegex = /model\s+(\w+)\s*\{/g;
|
|
412
|
+
let match;
|
|
413
|
+
while ((match = modelRegex.exec(source)) !== null) {
|
|
414
|
+
tables.add(match[1]);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
// Skip unreadable files
|
|
439
419
|
}
|
|
440
420
|
}
|
|
441
421
|
}
|
|
442
|
-
|
|
443
|
-
if (finalConfig.checkNamingConventions) {
|
|
444
|
-
violations.push(...this.checkNamingConventions(tableRefs, ast.filePath));
|
|
445
|
-
}
|
|
446
|
-
// Check query patterns
|
|
447
|
-
if (finalConfig.validateQueryPatterns) {
|
|
448
|
-
violations.push(...this.checkQueryPatterns(ast, adapter, sourceCode, finalConfig));
|
|
449
|
-
}
|
|
450
|
-
// Check for SQL injection patterns — R7: no critical by default
|
|
451
|
-
violations.push(...this.checkSQLInjection(ast, adapter, sourceCode));
|
|
452
|
-
return violations;
|
|
422
|
+
return tables;
|
|
453
423
|
}
|
|
454
424
|
// ---------------------------------------------------------------------------
|
|
455
425
|
// R2.2 — File gate
|
|
@@ -492,14 +462,14 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
492
462
|
return true;
|
|
493
463
|
}
|
|
494
464
|
// Check for env-binding patterns (e.g., env.DB in Cloudflare Workers)
|
|
495
|
-
const bindingNames = config.dbBindingNames ?? [
|
|
465
|
+
const bindingNames = config.dbBindingNames ?? [...DB_BINDING_NAMES];
|
|
496
466
|
for (const binding of bindingNames) {
|
|
497
467
|
if (sourceCode.includes(binding))
|
|
498
468
|
return true;
|
|
499
469
|
}
|
|
500
470
|
// Check for DB call patterns (receiver.method)
|
|
501
|
-
const receivers = config.dbReceiverNames ?? [
|
|
502
|
-
const methods = config.dbCallMethods ?? [
|
|
471
|
+
const receivers = config.dbReceiverNames ?? [...DB_RECEIVER_NAMES];
|
|
472
|
+
const methods = config.dbCallMethods ?? [...DB_CALL_METHOD_NAMES];
|
|
503
473
|
for (const receiver of receivers) {
|
|
504
474
|
for (const method of methods) {
|
|
505
475
|
const pattern = new RegExp(`\\b${escapeRegex(receiver)}\\.${escapeRegex(method)}\\s*\\(`);
|
|
@@ -509,7 +479,7 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
509
479
|
}
|
|
510
480
|
}
|
|
511
481
|
// Check for SQL tagged template literals (syntax feature, not naming convention)
|
|
512
|
-
const sqlTags = config.sqlTagNames ?? [
|
|
482
|
+
const sqlTags = config.sqlTagNames ?? [...SQL_TAG_NAMES];
|
|
513
483
|
for (const tag of sqlTags) {
|
|
514
484
|
const pattern = new RegExp(`\\b${escapeRegex(tag)}\`\\s*SELECT|\\b${escapeRegex(tag)}\`\\s*INSERT|\\b${escapeRegex(tag)}\`\\s*UPDATE|\\b${escapeRegex(tag)}\`\\s*DELETE|\\b${escapeRegex(tag)}\`\\s*CREATE`, 'i');
|
|
515
485
|
if (pattern.test(sourceCode))
|
|
@@ -527,7 +497,7 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
527
497
|
*/
|
|
528
498
|
findTableReferences(ast, adapter, sourceCode, config, provenanceContext, allTables) {
|
|
529
499
|
const references = [];
|
|
530
|
-
const sqlTags = config.sqlTagNames ?? [
|
|
500
|
+
const sqlTags = config.sqlTagNames ?? [...SQL_TAG_NAMES];
|
|
531
501
|
// (1) Tagged template SQL — e.g. sql`SELECT * FROM heroes`
|
|
532
502
|
// This is a syntax feature, not a naming convention — keep the sqlTagNames gate.
|
|
533
503
|
const taggedTemplates = adapter.findNodes(ast, {
|
|
@@ -564,8 +534,8 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
564
534
|
const callee = this.getCallee(node, adapter, sourceCode);
|
|
565
535
|
if (!callee)
|
|
566
536
|
return false;
|
|
567
|
-
const dbMethods = config.dbCallMethods ?? [
|
|
568
|
-
const dbReceivers = config.dbReceiverNames ?? [
|
|
537
|
+
const dbMethods = config.dbCallMethods ?? [...DB_CALL_METHOD_NAMES];
|
|
538
|
+
const dbReceivers = config.dbReceiverNames ?? [...DB_RECEIVER_NAMES];
|
|
569
539
|
return this.isDbMemberCall(node, callee, dbMethods, dbReceivers, adapter, sourceCode);
|
|
570
540
|
},
|
|
571
541
|
});
|
|
@@ -619,8 +589,7 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
619
589
|
*/
|
|
620
590
|
recordTableUsage(ast, adapter, filePath, references) {
|
|
621
591
|
try {
|
|
622
|
-
|
|
623
|
-
db.clearSchemaUsageForFile(filePath);
|
|
592
|
+
this._pendingSchemaRecords.clearFiles.push(filePath);
|
|
624
593
|
for (const ref of references) {
|
|
625
594
|
// Find enclosing function from the AST position
|
|
626
595
|
const node = this.findClosestNodeAt(ast.root, ref.location, adapter);
|
|
@@ -629,7 +598,7 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
629
598
|
: ast.filePath.endsWith('.sql') || ast.filePath.includes('/migrations/')
|
|
630
599
|
? 'schema-file'
|
|
631
600
|
: 'top-level';
|
|
632
|
-
|
|
601
|
+
this._pendingSchemaRecords.usages.push({
|
|
633
602
|
tableName: ref.table,
|
|
634
603
|
filePath,
|
|
635
604
|
functionName,
|
|
@@ -637,14 +606,19 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
637
606
|
line: ref.location.line,
|
|
638
607
|
column: ref.location.column,
|
|
639
608
|
rawQuery: ref.context,
|
|
640
|
-
};
|
|
641
|
-
db.recordSchemaUsage(usage);
|
|
609
|
+
});
|
|
642
610
|
}
|
|
643
611
|
}
|
|
644
612
|
catch {
|
|
645
613
|
// Schema recording is best-effort — failures don't block analysis.
|
|
646
614
|
}
|
|
647
615
|
}
|
|
616
|
+
/** Spec 25 B4 — Drain pending schema records for the pipeline to write. */
|
|
617
|
+
getPendingSchemaRecords() {
|
|
618
|
+
const records = this._pendingSchemaRecords;
|
|
619
|
+
this._pendingSchemaRecords = { clearFiles: [], usages: [] };
|
|
620
|
+
return records;
|
|
621
|
+
}
|
|
648
622
|
/**
|
|
649
623
|
* Parse SQL table names from a SQL text string.
|
|
650
624
|
* R2.3: Template expressions (${...}) resolve portions to wildcards.
|
|
@@ -1164,23 +1138,68 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
1164
1138
|
// ---------------------------------------------------------------------------
|
|
1165
1139
|
// JSON Schema validation (unchanged from original)
|
|
1166
1140
|
// ---------------------------------------------------------------------------
|
|
1167
|
-
|
|
1141
|
+
/**
|
|
1142
|
+
* Analyze JSON schemas and validate data files against them.
|
|
1143
|
+
*
|
|
1144
|
+
* Pipeline-refactored: accepts a Map of pre-loaded JSON contents instead of
|
|
1145
|
+
* reading from the filesystem, so this can run inside a Stage 3 reducer.
|
|
1146
|
+
*
|
|
1147
|
+
* @param jsonContents Map from filePath → { parsed: pre-parsed object or null, raw: string }
|
|
1148
|
+
* @param config Schema analyzer configuration
|
|
1149
|
+
*/
|
|
1150
|
+
analyzeJsonSchemas(jsonContents, config) {
|
|
1168
1151
|
const violations = [];
|
|
1169
1152
|
const errors = [];
|
|
1170
1153
|
let filesProcessed = 0;
|
|
1171
1154
|
const startTime = Date.now();
|
|
1172
1155
|
const finalConfig = { ...DEFAULT_SCHEMA_CONFIG, ...config };
|
|
1173
1156
|
if (!finalConfig.validateJsonSchemas) {
|
|
1174
|
-
return { violations, errors, filesProcessed, executionTime: 0 };
|
|
1157
|
+
return { violations, errors, status: makeVisitorStatus(filesProcessed), executionTime: 0, analyzerName: this.name };
|
|
1175
1158
|
}
|
|
1159
|
+
const files = Array.from(jsonContents.keys());
|
|
1176
1160
|
const schemaFiles = this.identifySchemaFiles(files, finalConfig);
|
|
1177
1161
|
const dataFiles = this.identifyDataFiles(files, finalConfig);
|
|
1178
1162
|
const unknownJsonFiles = files.filter(f => !schemaFiles.includes(f) && !dataFiles.includes(f));
|
|
1163
|
+
// Helper: parse from Map content (with fallback)
|
|
1164
|
+
const getParsed = (filePath) => {
|
|
1165
|
+
const entry = jsonContents.get(filePath);
|
|
1166
|
+
if (!entry)
|
|
1167
|
+
return null;
|
|
1168
|
+
if (entry.parsed !== null)
|
|
1169
|
+
return entry.parsed;
|
|
1170
|
+
try {
|
|
1171
|
+
return JSON.parse(entry.raw);
|
|
1172
|
+
}
|
|
1173
|
+
catch {
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1179
1177
|
const schemas = new Map();
|
|
1180
1178
|
for (const file of schemaFiles) {
|
|
1181
1179
|
try {
|
|
1182
|
-
const
|
|
1183
|
-
const
|
|
1180
|
+
const entry = jsonContents.get(file);
|
|
1181
|
+
const content = entry?.raw;
|
|
1182
|
+
if (!content) {
|
|
1183
|
+
violations.push({
|
|
1184
|
+
file,
|
|
1185
|
+
line: 1,
|
|
1186
|
+
column: 1,
|
|
1187
|
+
severity: 'warning',
|
|
1188
|
+
message: `Content not available for schema file`,
|
|
1189
|
+
rule: 'file-error',
|
|
1190
|
+
analyzer: 'schema'
|
|
1191
|
+
});
|
|
1192
|
+
filesProcessed++;
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
const schema = entry.parsed ?? (() => { try {
|
|
1196
|
+
return JSON.parse(content);
|
|
1197
|
+
}
|
|
1198
|
+
catch {
|
|
1199
|
+
return null;
|
|
1200
|
+
} })();
|
|
1201
|
+
if (schema === null)
|
|
1202
|
+
throw new SyntaxError('JSON parse failed');
|
|
1184
1203
|
schemas.set(file, schema);
|
|
1185
1204
|
const fileViolations = this.validateJsonSchema(schema, file, finalConfig);
|
|
1186
1205
|
violations.push(...fileViolations);
|
|
@@ -1209,13 +1228,28 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
1209
1228
|
}
|
|
1210
1229
|
if (finalConfig.schemaDataPairs) {
|
|
1211
1230
|
for (const pair of finalConfig.schemaDataPairs) {
|
|
1212
|
-
const schema = schemas.get(pair.schema)
|
|
1231
|
+
const schema = schemas.get(pair.schema) ?? getParsed(pair.schema);
|
|
1213
1232
|
if (schema) {
|
|
1214
1233
|
const dataFiles = Array.isArray(pair.data) ? pair.data : [pair.data];
|
|
1215
1234
|
for (const dataFile of dataFiles) {
|
|
1216
1235
|
if (files.includes(dataFile)) {
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1236
|
+
const parsed = getParsed(dataFile);
|
|
1237
|
+
if (parsed !== null) {
|
|
1238
|
+
const dataViolations = [];
|
|
1239
|
+
this.validateAgainstSchema(parsed, schema, dataFile, dataViolations, finalConfig);
|
|
1240
|
+
violations.push(...dataViolations);
|
|
1241
|
+
}
|
|
1242
|
+
else {
|
|
1243
|
+
violations.push({
|
|
1244
|
+
file: dataFile,
|
|
1245
|
+
line: 1,
|
|
1246
|
+
column: 1,
|
|
1247
|
+
severity: 'warning',
|
|
1248
|
+
message: 'Invalid JSON in data file',
|
|
1249
|
+
rule: 'invalid-json',
|
|
1250
|
+
analyzer: 'schema'
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1219
1253
|
filesProcessed++;
|
|
1220
1254
|
}
|
|
1221
1255
|
}
|
|
@@ -1226,21 +1260,22 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
1226
1260
|
for (const dataFile of [...dataFiles, ...unknownJsonFiles]) {
|
|
1227
1261
|
const matchedSchema = this.findMatchingSchema(dataFile, schemas, finalConfig);
|
|
1228
1262
|
if (matchedSchema) {
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1263
|
+
const parsed = getParsed(dataFile);
|
|
1264
|
+
if (parsed !== null) {
|
|
1265
|
+
const dataViolations = [];
|
|
1266
|
+
this.validateAgainstSchema(parsed, matchedSchema, dataFile, dataViolations, finalConfig);
|
|
1267
|
+
violations.push(...dataViolations);
|
|
1268
|
+
}
|
|
1231
1269
|
}
|
|
1232
1270
|
else if (unknownJsonFiles.includes(dataFile)) {
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
JSON.parse(content);
|
|
1236
|
-
}
|
|
1237
|
-
catch (error) {
|
|
1271
|
+
const parsed = getParsed(dataFile);
|
|
1272
|
+
if (parsed === null) {
|
|
1238
1273
|
violations.push({
|
|
1239
1274
|
file: dataFile,
|
|
1240
1275
|
line: 1,
|
|
1241
1276
|
column: 1,
|
|
1242
1277
|
severity: 'warning',
|
|
1243
|
-
message: `Invalid JSON:
|
|
1278
|
+
message: `Invalid JSON: Parse error`,
|
|
1244
1279
|
rule: 'invalid-json',
|
|
1245
1280
|
analyzer: 'schema'
|
|
1246
1281
|
});
|
|
@@ -1252,8 +1287,9 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
1252
1287
|
return {
|
|
1253
1288
|
violations,
|
|
1254
1289
|
errors,
|
|
1255
|
-
filesProcessed,
|
|
1256
|
-
executionTime: Date.now() - startTime
|
|
1290
|
+
status: makeVisitorStatus(filesProcessed),
|
|
1291
|
+
executionTime: Date.now() - startTime,
|
|
1292
|
+
analyzerName: this.name,
|
|
1257
1293
|
};
|
|
1258
1294
|
}
|
|
1259
1295
|
validateJsonSchema(schema, filePath, config) {
|
|
@@ -1357,15 +1393,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
1357
1393
|
});
|
|
1358
1394
|
});
|
|
1359
1395
|
}
|
|
1360
|
-
async loadSchema(schemaPath) {
|
|
1361
|
-
try {
|
|
1362
|
-
const content = await fs.readFile(schemaPath, 'utf8');
|
|
1363
|
-
return JSON.parse(content);
|
|
1364
|
-
}
|
|
1365
|
-
catch {
|
|
1366
|
-
return null;
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
1396
|
findMatchingSchema(dataFile, schemas, config) {
|
|
1370
1397
|
const dataFileName = dataFile.split('/').pop() || '';
|
|
1371
1398
|
const dataBaseName = dataFileName.replace(/\.(data|example|test)\.json$/, '');
|
|
@@ -1381,39 +1408,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
1381
1408
|
}
|
|
1382
1409
|
return null;
|
|
1383
1410
|
}
|
|
1384
|
-
async validateDataAgainstSchema(dataFile, schema, config) {
|
|
1385
|
-
const violations = [];
|
|
1386
|
-
try {
|
|
1387
|
-
const content = await fs.readFile(dataFile, 'utf8');
|
|
1388
|
-
const data = JSON.parse(content);
|
|
1389
|
-
this.validateAgainstSchema(data, schema, dataFile, violations, config);
|
|
1390
|
-
}
|
|
1391
|
-
catch (error) {
|
|
1392
|
-
if (error instanceof SyntaxError) {
|
|
1393
|
-
violations.push({
|
|
1394
|
-
file: dataFile,
|
|
1395
|
-
line: 1,
|
|
1396
|
-
column: 1,
|
|
1397
|
-
severity: 'warning',
|
|
1398
|
-
message: `Invalid JSON: ${error.message}`,
|
|
1399
|
-
rule: 'invalid-json',
|
|
1400
|
-
analyzer: 'schema'
|
|
1401
|
-
});
|
|
1402
|
-
}
|
|
1403
|
-
else {
|
|
1404
|
-
violations.push({
|
|
1405
|
-
file: dataFile,
|
|
1406
|
-
line: 1,
|
|
1407
|
-
column: 1,
|
|
1408
|
-
severity: 'warning',
|
|
1409
|
-
message: `Error reading file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1410
|
-
rule: 'file-error',
|
|
1411
|
-
analyzer: 'schema'
|
|
1412
|
-
});
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
return violations;
|
|
1416
|
-
}
|
|
1417
1411
|
validateAgainstSchema(data, schema, filePath, violations, config, path = '') {
|
|
1418
1412
|
if (schema.type) {
|
|
1419
1413
|
const actualType = Array.isArray(data) ? 'array' :
|