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
|
@@ -0,0 +1,1146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline adapter factories (Spec 24).
|
|
3
|
+
*
|
|
4
|
+
* Each factory converts an existing analyzer into a Stage2Visitor,
|
|
5
|
+
* Stage3Reducer, or Stage4Reducer for the pipeline. Lazy-loads analyzer
|
|
6
|
+
* classes via dynamic import() to avoid circular dependency issues.
|
|
7
|
+
*
|
|
8
|
+
* Key patterns:
|
|
9
|
+
* - Per-file (solid, data-access, doc, schema): wrap analyzeAST()
|
|
10
|
+
* - DRY: wraps analyzeAST(), accumulates dryPairs internally →
|
|
11
|
+
* factory returns { visitor, getDryPairs } for post-pipeline extraction
|
|
12
|
+
* - React: wraps scanFile() per file, accumulates scan results →
|
|
13
|
+
* factory returns { visitor, finalizeCrossComponent }
|
|
14
|
+
* - DB-based (styles, conventions, cross-domain): reducers call
|
|
15
|
+
* analyzer.analyze([]) with rawDb injected through config
|
|
16
|
+
* - Invariants: Stage 3 reducer — receives full file list, runs rule engine
|
|
17
|
+
*/
|
|
18
|
+
import { createHash } from 'crypto';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { RULE_REGISTRY } from './analyzers/ruleRegistry.js';
|
|
21
|
+
import { walkAST, isExported, getLineAndColumn, calculateComplexity, getFunctionBody, } from './languages/adapterBridge.js';
|
|
22
|
+
import { buildImportMap, extractFunctionCalls, } from './utils/dependencyExtractor.js';
|
|
23
|
+
import { isReactComponent, detectComponentType, getComponentName, } from './utils/reactDetection.js';
|
|
24
|
+
import { ALL_EXTENSIONS } from './utils/fileDiscovery.js';
|
|
25
|
+
// ── Rule ID helpers ──────────────────────────────────────────────────────────
|
|
26
|
+
function getRuleIdsFor(name) {
|
|
27
|
+
return Object.entries(RULE_REGISTRY)
|
|
28
|
+
.filter(([, entry]) => entry.analyzer === name)
|
|
29
|
+
.map(([id]) => id);
|
|
30
|
+
}
|
|
31
|
+
// ── Lazy import singleton ────────────────────────────────────────────────────
|
|
32
|
+
function lazySingleton(loader) {
|
|
33
|
+
let promise = null;
|
|
34
|
+
return () => {
|
|
35
|
+
if (!promise)
|
|
36
|
+
promise = loader();
|
|
37
|
+
return promise;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// ── File-sources infrastructure visitor ──────────────────────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* Infrastructure visitor that records every file's source code so Stage 3
|
|
43
|
+
* reducers can access it without calling readFileSync(). Always registered;
|
|
44
|
+
* declares all known extensions so it covers both parsed and raw tuples.
|
|
45
|
+
*/
|
|
46
|
+
export function createFileSourcesVisitor() {
|
|
47
|
+
return {
|
|
48
|
+
name: 'file-sources',
|
|
49
|
+
stage: 'visitor',
|
|
50
|
+
extensions: ALL_EXTENSIONS,
|
|
51
|
+
getRuleIds: () => [],
|
|
52
|
+
async visit(_ast, _adapter, context, sourceCode) {
|
|
53
|
+
return {
|
|
54
|
+
violations: [],
|
|
55
|
+
facts: { [context.filePath]: sourceCode },
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
defaultConfig: {},
|
|
59
|
+
description: 'Records every file source for Stage 3 reducers (infrastructure)',
|
|
60
|
+
category: 'infrastructure',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// ── SOLID visitor ────────────────────────────────────────────────────────────
|
|
64
|
+
export function createSolidVisitor() {
|
|
65
|
+
const getAnalyzer = lazySingleton(() => import('./analyzers/universal/UniversalSOLIDAnalyzer.js').then((m) => new m.UniversalSOLIDAnalyzer()));
|
|
66
|
+
return {
|
|
67
|
+
name: 'solid',
|
|
68
|
+
stage: 'visitor',
|
|
69
|
+
getRuleIds: () => getRuleIdsFor('solid'),
|
|
70
|
+
async visit(ast, adapter, context, sourceCode) {
|
|
71
|
+
const a = await getAnalyzer();
|
|
72
|
+
const violations = await a.analyzeAST(ast, adapter, context.config, sourceCode);
|
|
73
|
+
return {
|
|
74
|
+
violations,
|
|
75
|
+
facts: {},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
defaultConfig: {},
|
|
79
|
+
description: 'Detects violations of SOLID principles',
|
|
80
|
+
category: 'architecture',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function createDryVisitor(fullFunctionIndex) {
|
|
84
|
+
const getAnalyzer = lazySingleton(() => import('./analyzers/universal/UniversalDRYAnalyzer.js').then((m) => new m.UniversalDRYAnalyzer()));
|
|
85
|
+
const visitor = {
|
|
86
|
+
name: 'dry',
|
|
87
|
+
stage: 'visitor',
|
|
88
|
+
getRuleIds: () => getRuleIdsFor('dry'),
|
|
89
|
+
async visit(ast, adapter, context, sourceCode) {
|
|
90
|
+
const a = await getAnalyzer();
|
|
91
|
+
const config = fullFunctionIndex
|
|
92
|
+
? { ...context.config, fullFunctionIndex }
|
|
93
|
+
: context.config;
|
|
94
|
+
const violations = await a.analyzeAST(ast, adapter, config, sourceCode);
|
|
95
|
+
return { violations, facts: {} };
|
|
96
|
+
},
|
|
97
|
+
defaultConfig: {},
|
|
98
|
+
description: 'Detects code duplication across the codebase',
|
|
99
|
+
category: 'maintainability',
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
visitor,
|
|
103
|
+
getDryPairs: async () => {
|
|
104
|
+
try {
|
|
105
|
+
const a = await getAnalyzer();
|
|
106
|
+
return a.dryPairs ?? [];
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// ── Data-Access visitor ──────────────────────────────────────────────────────
|
|
115
|
+
export function createDataAccessVisitor() {
|
|
116
|
+
const getAnalyzer = lazySingleton(() => import('./analyzers/universal/UniversalDataAccessAnalyzer.js').then((m) => new m.UniversalDataAccessAnalyzer()));
|
|
117
|
+
return {
|
|
118
|
+
name: 'data-access',
|
|
119
|
+
stage: 'visitor',
|
|
120
|
+
getRuleIds: () => getRuleIdsFor('data-access'),
|
|
121
|
+
async visit(ast, adapter, context, sourceCode) {
|
|
122
|
+
const a = await getAnalyzer();
|
|
123
|
+
const violations = await a.analyzeAST(ast, adapter, context.config, sourceCode);
|
|
124
|
+
return { violations, facts: {} };
|
|
125
|
+
},
|
|
126
|
+
defaultConfig: {},
|
|
127
|
+
description: 'Analyzes database access patterns and data layer interactions',
|
|
128
|
+
category: 'security',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// ── Documentation visitor ────────────────────────────────────────────────────
|
|
132
|
+
export function createDocumentationVisitor() {
|
|
133
|
+
const getAnalyzer = lazySingleton(() => import('./analyzers/universal/UniversalDocumentationAnalyzer.js').then((m) => new m.UniversalDocumentationAnalyzer()));
|
|
134
|
+
return {
|
|
135
|
+
name: 'documentation',
|
|
136
|
+
stage: 'visitor',
|
|
137
|
+
getRuleIds: () => getRuleIdsFor('documentation'),
|
|
138
|
+
async visit(ast, adapter, context, sourceCode) {
|
|
139
|
+
const a = await getAnalyzer();
|
|
140
|
+
const violations = await a.analyzeAST(ast, adapter, context.config, sourceCode);
|
|
141
|
+
return { violations, facts: {} };
|
|
142
|
+
},
|
|
143
|
+
defaultConfig: {},
|
|
144
|
+
description: 'Checks documentation completeness',
|
|
145
|
+
category: 'style',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// ── Function-Index visitor (infrastructure) ──────────────────────────────────
|
|
149
|
+
// Always-on Stage 2 visitor that populates the `functions` table with
|
|
150
|
+
// per-file definitions so the conventions and cross-domain reducers have data
|
|
151
|
+
// to mine — even on a cold run with no prior `index sync`.
|
|
152
|
+
// The function_calls table is rebuilt post-pipeline after functions rows have
|
|
153
|
+
// their auto-increment IDs assigned.
|
|
154
|
+
function getLanguageFromPath(filePath) {
|
|
155
|
+
const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
|
|
156
|
+
switch (ext) {
|
|
157
|
+
case '.ts':
|
|
158
|
+
case '.tsx':
|
|
159
|
+
return 'typescript';
|
|
160
|
+
case '.js':
|
|
161
|
+
case '.jsx':
|
|
162
|
+
return 'javascript';
|
|
163
|
+
default:
|
|
164
|
+
return 'unknown';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function computeContentHash(body, signature) {
|
|
168
|
+
const normalized = (body ?? '').replace(/\s+/g, ' ').trim() + '|' + (signature ?? '').trim();
|
|
169
|
+
return createHash('sha256').update(normalized).digest('hex');
|
|
170
|
+
}
|
|
171
|
+
/** Raw text from an ASTNode (tree-sitter node stored on `.raw`). */
|
|
172
|
+
function rawText(node) {
|
|
173
|
+
return node.raw?.text ?? '';
|
|
174
|
+
}
|
|
175
|
+
export function createFunctionIndexVisitor() {
|
|
176
|
+
return {
|
|
177
|
+
name: 'function-index',
|
|
178
|
+
stage: 'visitor',
|
|
179
|
+
getRuleIds: () => [],
|
|
180
|
+
async visit(ast, _adapter, context, sourceCode) {
|
|
181
|
+
const root = ast.root;
|
|
182
|
+
const filePath = context.filePath;
|
|
183
|
+
if (!filePath)
|
|
184
|
+
return { violations: [], facts: {} };
|
|
185
|
+
const indexFacts = [];
|
|
186
|
+
// Only index TS/JS files
|
|
187
|
+
const lang = getLanguageFromPath(filePath);
|
|
188
|
+
if (lang === 'unknown')
|
|
189
|
+
return { violations: [], facts: {} };
|
|
190
|
+
// Clear existing entries for this file
|
|
191
|
+
indexFacts.push({
|
|
192
|
+
table: 'functions',
|
|
193
|
+
data: { _action: 'clear-by-file', file_path: filePath },
|
|
194
|
+
});
|
|
195
|
+
// Build import map once per file for resolving call targets
|
|
196
|
+
const importMap = buildImportMap(root);
|
|
197
|
+
// Collect function-like nodes in two passes:
|
|
198
|
+
// 1) function_declaration + method_definition nodes
|
|
199
|
+
// 2) arrow functions assigned to variables (variable_declarator children)
|
|
200
|
+
const fnEntries = [];
|
|
201
|
+
// Pass 1 — named function declarations and methods
|
|
202
|
+
walkAST(root, (node) => {
|
|
203
|
+
if (node.type === 'function_declaration') {
|
|
204
|
+
const nameNode = node.children?.find((c) => c.type === 'identifier');
|
|
205
|
+
if (!nameNode)
|
|
206
|
+
return;
|
|
207
|
+
const name = rawText(nameNode);
|
|
208
|
+
if (!name)
|
|
209
|
+
return;
|
|
210
|
+
const { line } = getLineAndColumn(node);
|
|
211
|
+
const raw = node.raw;
|
|
212
|
+
const endLine = line + (raw?.endPosition?.row ?? raw?.startPosition?.row ?? 0) - (raw?.startPosition?.row ?? line) + 1;
|
|
213
|
+
const body = getFunctionBody(node, sourceCode);
|
|
214
|
+
const calls = extractFunctionCalls(node, sourceCode, importMap);
|
|
215
|
+
const callNames = [...new Set(calls.map((c) => c.callee))];
|
|
216
|
+
fnEntries.push({
|
|
217
|
+
name,
|
|
218
|
+
line,
|
|
219
|
+
endLine,
|
|
220
|
+
entityType: 'function',
|
|
221
|
+
componentType: null,
|
|
222
|
+
isExported: isExported(node),
|
|
223
|
+
complexity: calculateComplexity(node),
|
|
224
|
+
body,
|
|
225
|
+
functionCalls: callNames,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
// Class methods
|
|
229
|
+
if (node.type === 'method_definition') {
|
|
230
|
+
const nameNode = node.children?.find((c) => c.type === 'identifier');
|
|
231
|
+
if (!nameNode)
|
|
232
|
+
return;
|
|
233
|
+
const methodName = rawText(nameNode);
|
|
234
|
+
if (!methodName)
|
|
235
|
+
return;
|
|
236
|
+
// Walk up to find class name
|
|
237
|
+
let parent = node.parent;
|
|
238
|
+
let className = 'AnonymousClass';
|
|
239
|
+
while (parent) {
|
|
240
|
+
if (parent.type === 'class_declaration') {
|
|
241
|
+
const cn = parent.children?.find((c) => c.type === 'identifier');
|
|
242
|
+
if (cn)
|
|
243
|
+
className = rawText(cn);
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
parent = parent.parent;
|
|
247
|
+
}
|
|
248
|
+
const { line } = getLineAndColumn(node);
|
|
249
|
+
const raw = node.raw;
|
|
250
|
+
const endLine = line + (raw?.endPosition?.row ?? raw?.startPosition?.row ?? 0) - (raw?.startPosition?.row ?? line) + 1;
|
|
251
|
+
const body = getFunctionBody(node, sourceCode);
|
|
252
|
+
const calls = extractFunctionCalls(node, sourceCode, importMap);
|
|
253
|
+
const callNames = [...new Set(calls.map((c) => c.callee))];
|
|
254
|
+
fnEntries.push({
|
|
255
|
+
name: `${className}.${methodName}`,
|
|
256
|
+
line,
|
|
257
|
+
endLine,
|
|
258
|
+
entityType: 'method',
|
|
259
|
+
componentType: null,
|
|
260
|
+
isExported: isExported(node),
|
|
261
|
+
complexity: calculateComplexity(node),
|
|
262
|
+
body,
|
|
263
|
+
functionCalls: callNames,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
// Pass 2 — arrow functions assigned to variables
|
|
268
|
+
walkAST(root, (node) => {
|
|
269
|
+
if (node.type !== 'variable_declarator')
|
|
270
|
+
return;
|
|
271
|
+
const nameNode = node.children?.find((c) => c.type === 'identifier');
|
|
272
|
+
const arrowFunc = node.children?.find((c) => c.type === 'arrow_function');
|
|
273
|
+
if (!nameNode || !arrowFunc)
|
|
274
|
+
return;
|
|
275
|
+
const name = rawText(nameNode);
|
|
276
|
+
if (!name)
|
|
277
|
+
return;
|
|
278
|
+
// Don't duplicate if already covered as function_declaration
|
|
279
|
+
// (shouldn't happen — function_declaration is a different node type)
|
|
280
|
+
const { line } = getLineAndColumn(arrowFunc);
|
|
281
|
+
const raw = arrowFunc.raw;
|
|
282
|
+
const endLine = line + (raw?.endPosition?.row ?? raw?.startPosition?.row ?? 0) - (raw?.startPosition?.row ?? line) + 1;
|
|
283
|
+
const body = getFunctionBody(arrowFunc, sourceCode);
|
|
284
|
+
const calls = extractFunctionCalls(arrowFunc, sourceCode, importMap);
|
|
285
|
+
const callNames = [...new Set(calls.map((c) => c.callee))];
|
|
286
|
+
fnEntries.push({
|
|
287
|
+
name,
|
|
288
|
+
line,
|
|
289
|
+
endLine,
|
|
290
|
+
entityType: 'function',
|
|
291
|
+
componentType: null,
|
|
292
|
+
isExported: isExported(node),
|
|
293
|
+
complexity: calculateComplexity(arrowFunc),
|
|
294
|
+
body,
|
|
295
|
+
functionCalls: callNames,
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
// Pass 3 — React component detection
|
|
299
|
+
// Detect JSX-returning functions to set entity_type='component' and
|
|
300
|
+
// component_type, matching what functionScanner's deepSync produces.
|
|
301
|
+
// The convention miner's classifyExportKind() uses these columns to
|
|
302
|
+
// partition naming conventions.
|
|
303
|
+
const hasReactImport = [...importMap.values()].some((v) => v.modulePath === 'react');
|
|
304
|
+
if (filePath.endsWith('.tsx') ||
|
|
305
|
+
filePath.endsWith('.jsx') ||
|
|
306
|
+
(filePath.endsWith('.js') && hasReactImport)) {
|
|
307
|
+
walkAST(root, (node) => {
|
|
308
|
+
if (!isReactComponent(node))
|
|
309
|
+
return;
|
|
310
|
+
const ct = detectComponentType(node);
|
|
311
|
+
if (!ct)
|
|
312
|
+
return;
|
|
313
|
+
const cName = getComponentName(node);
|
|
314
|
+
if (!cName || cName === 'AnonymousComponent')
|
|
315
|
+
return;
|
|
316
|
+
const existing = fnEntries.find((f) => f.name === cName);
|
|
317
|
+
if (existing) {
|
|
318
|
+
// Upgrade existing function_declaration or arrow-function entry
|
|
319
|
+
existing.entityType = 'component';
|
|
320
|
+
existing.componentType = ct;
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
// New entry — class component, function_expression, or memo/forwardRef
|
|
324
|
+
// wrapper not already captured by passes 1 or 2.
|
|
325
|
+
const { line } = getLineAndColumn(node);
|
|
326
|
+
const raw = node.raw;
|
|
327
|
+
const endLine = line +
|
|
328
|
+
(raw?.endPosition?.row ?? raw?.startPosition?.row ?? 0) -
|
|
329
|
+
(raw?.startPosition?.row ?? line) +
|
|
330
|
+
1;
|
|
331
|
+
const body = getFunctionBody(node, sourceCode);
|
|
332
|
+
fnEntries.push({
|
|
333
|
+
name: cName,
|
|
334
|
+
line,
|
|
335
|
+
endLine,
|
|
336
|
+
entityType: 'component',
|
|
337
|
+
componentType: ct,
|
|
338
|
+
isExported: isExported(node),
|
|
339
|
+
complexity: calculateComplexity(node),
|
|
340
|
+
body,
|
|
341
|
+
functionCalls: [],
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
// Build IndexFactsEntry for each function
|
|
347
|
+
for (const fn of fnEntries) {
|
|
348
|
+
const metadata = {
|
|
349
|
+
entityType: fn.entityType,
|
|
350
|
+
isExported: fn.isExported,
|
|
351
|
+
complexity: fn.complexity,
|
|
352
|
+
functionCalls: fn.functionCalls,
|
|
353
|
+
body: fn.body,
|
|
354
|
+
};
|
|
355
|
+
const contentHash = computeContentHash(fn.body, '');
|
|
356
|
+
const now = new Date().toISOString();
|
|
357
|
+
indexFacts.push({
|
|
358
|
+
table: 'functions',
|
|
359
|
+
data: {
|
|
360
|
+
name: fn.name,
|
|
361
|
+
file_path: filePath,
|
|
362
|
+
line_number: fn.line,
|
|
363
|
+
start_line: fn.line,
|
|
364
|
+
end_line: fn.endLine,
|
|
365
|
+
language: lang,
|
|
366
|
+
entity_type: fn.entityType,
|
|
367
|
+
component_type: fn.componentType,
|
|
368
|
+
signature: '',
|
|
369
|
+
return_type: null,
|
|
370
|
+
complexity: fn.complexity,
|
|
371
|
+
is_exported: fn.isExported ? 1 : 0,
|
|
372
|
+
has_jsdoc: 0,
|
|
373
|
+
jsdoc_description: '',
|
|
374
|
+
parameters: null,
|
|
375
|
+
body: fn.body ?? null,
|
|
376
|
+
content_hash: contentHash,
|
|
377
|
+
last_modified: now,
|
|
378
|
+
metadata_json: JSON.stringify(metadata),
|
|
379
|
+
},
|
|
380
|
+
conflictKey: 'name, file_path, line_number',
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
// ── Extract imports and exports from AST for downstream consumers ──
|
|
384
|
+
// B1: Replaces regex-based extractImports()/extractExportedSymbols() in
|
|
385
|
+
// ruleEngine.ts. Also consumed by conventions (B2: detectExportForm).
|
|
386
|
+
const langAdapter = _adapter;
|
|
387
|
+
const langAst = ast;
|
|
388
|
+
// Static imports via the adapter's canonical extractImports()
|
|
389
|
+
const staticImportInfos = langAdapter.extractImports(langAst);
|
|
390
|
+
const staticImports = staticImportInfos.map((imp) => ({
|
|
391
|
+
moduleSpecifier: imp.source,
|
|
392
|
+
isStatic: true,
|
|
393
|
+
isDynamic: false,
|
|
394
|
+
isRequire: false,
|
|
395
|
+
line: imp.location.start.line,
|
|
396
|
+
}));
|
|
397
|
+
// Dynamic import() and require() — first attempt with pure NodePattern
|
|
398
|
+
// Plan note: import keyword is an anonymous tree-sitter node, so
|
|
399
|
+
// hasChild cannot see it. We use `custom` with raw node access.
|
|
400
|
+
const dynamicCallNodes = langAdapter.findNodes(langAst, {
|
|
401
|
+
type: 'call_expression',
|
|
402
|
+
custom: (node) => {
|
|
403
|
+
const raw = node.raw;
|
|
404
|
+
const fn = raw?.firstChild;
|
|
405
|
+
return (fn?.type === 'import') ||
|
|
406
|
+
(fn?.type === 'identifier' && fn.text === 'require');
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
const dynamicImports = [];
|
|
410
|
+
for (const node of dynamicCallNodes) {
|
|
411
|
+
const raw = node.raw;
|
|
412
|
+
const fn = raw?.firstChild;
|
|
413
|
+
const isImport = fn?.type === 'import';
|
|
414
|
+
const isRequire = !isImport && (fn?.type === 'identifier' && fn.text === 'require');
|
|
415
|
+
// Walk the raw tree to find the string argument
|
|
416
|
+
const argsNode = raw?.children?.find((c) => c.type === 'arguments');
|
|
417
|
+
const stringNode = argsNode?.children?.find((c) => c.type === 'string');
|
|
418
|
+
if (stringNode) {
|
|
419
|
+
const text = stringNode.text;
|
|
420
|
+
if (text.length >= 2) {
|
|
421
|
+
dynamicImports.push({
|
|
422
|
+
moduleSpecifier: text.slice(1, -1), // strip quotes
|
|
423
|
+
isStatic: false,
|
|
424
|
+
isDynamic: isImport,
|
|
425
|
+
isRequire,
|
|
426
|
+
line: node.location.start.line,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// Exports via the adapter's canonical extractExports()
|
|
432
|
+
// Returns ExportInfo[] with isDefault — used by both invariants and conventions (B2)
|
|
433
|
+
const exportInfos = langAdapter.extractExports(langAst);
|
|
434
|
+
return {
|
|
435
|
+
violations: [],
|
|
436
|
+
facts: {
|
|
437
|
+
[filePath]: {
|
|
438
|
+
imports: [...staticImports, ...dynamicImports],
|
|
439
|
+
exports: exportInfos,
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
indexFacts,
|
|
443
|
+
};
|
|
444
|
+
},
|
|
445
|
+
defaultConfig: {},
|
|
446
|
+
description: 'Indexes function definitions and extracts imports/exports for downstream analyzers',
|
|
447
|
+
category: 'infrastructure',
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
export function createReactVisitor() {
|
|
451
|
+
const scanResults = [];
|
|
452
|
+
const visitor = {
|
|
453
|
+
name: 'react',
|
|
454
|
+
stage: 'visitor',
|
|
455
|
+
getRuleIds: () => getRuleIdsFor('react'),
|
|
456
|
+
async visit(_ast, _adapter, context, _sourceCode) {
|
|
457
|
+
const filePath = context.filePath;
|
|
458
|
+
if (!filePath)
|
|
459
|
+
return { violations: [], facts: {} };
|
|
460
|
+
try {
|
|
461
|
+
const { scanFile } = await import('./componentScanner.js');
|
|
462
|
+
const { analyzeComponent: analyzeComp, DEFAULT_REACT_CONFIG } = await import('./analyzers/reactAnalyzer.js');
|
|
463
|
+
// Merge with defaults so partial configs get all required fields
|
|
464
|
+
const cfg = { ...DEFAULT_REACT_CONFIG, ...context.config };
|
|
465
|
+
const sr = await scanFile(filePath, {
|
|
466
|
+
includeTests: false,
|
|
467
|
+
includeStories: false,
|
|
468
|
+
extractProps: true,
|
|
469
|
+
extractHooks: cfg.checkHooksRules,
|
|
470
|
+
extractImports: true,
|
|
471
|
+
detectComplexity: true,
|
|
472
|
+
});
|
|
473
|
+
scanResults.push(sr);
|
|
474
|
+
if (sr.parseErrors && sr.parseErrors.length > 0) {
|
|
475
|
+
return { violations: [], facts: {} };
|
|
476
|
+
}
|
|
477
|
+
// Per-component checks — same logic as reactAnalyzer.analyze()
|
|
478
|
+
const violations = [];
|
|
479
|
+
for (const comp of sr.components ?? []) {
|
|
480
|
+
violations.push(...analyzeComp(comp, cfg, sr));
|
|
481
|
+
}
|
|
482
|
+
return { violations, facts: {} };
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
return { violations: [], facts: {} };
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
defaultConfig: {},
|
|
489
|
+
description: 'Analyzes React components for best practices and performance',
|
|
490
|
+
category: 'frontend',
|
|
491
|
+
};
|
|
492
|
+
return {
|
|
493
|
+
visitor,
|
|
494
|
+
finalizeCrossComponent: async (config) => {
|
|
495
|
+
if (scanResults.length === 0)
|
|
496
|
+
return [];
|
|
497
|
+
const violations = [];
|
|
498
|
+
try {
|
|
499
|
+
const { checkCircularDependencies, checkErrorBoundaryUsage, checkRawElements, } = await import('./analyzers/reactAnalyzer.js');
|
|
500
|
+
// Build component tree and check for circular dependencies
|
|
501
|
+
const { buildComponentTree } = await import('./componentScanner.js');
|
|
502
|
+
const tree = buildComponentTree(scanResults);
|
|
503
|
+
violations.push(...checkCircularDependencies(tree));
|
|
504
|
+
// Check for missing error boundaries
|
|
505
|
+
if (config.requireErrorBoundaries !== false) {
|
|
506
|
+
violations.push(...checkErrorBoundaryUsage(scanResults));
|
|
507
|
+
}
|
|
508
|
+
// Check for raw element usage (Spec 10 R4)
|
|
509
|
+
if (config.rawElementCheck !== false) {
|
|
510
|
+
violations.push(...checkRawElements(scanResults, config));
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
// Cross-component checks are advisory
|
|
515
|
+
}
|
|
516
|
+
return violations;
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
// ── Styles reducer (stage 3) ─────────────────────────────────────────────────
|
|
521
|
+
// ── Styles CSS visitor (Spec 26 Phase 2) ───────────────────────────────────────
|
|
522
|
+
/**
|
|
523
|
+
* Styles CSS visitor — extracts declarations, tokens, and class usage from
|
|
524
|
+
* tree-sitter-css parsed ASTs. Replaces the regex-based CSS extraction in
|
|
525
|
+
* styleExtractor.ts for .css files only.
|
|
526
|
+
*
|
|
527
|
+
* tree-sitter-css is NOT an SCSS grammar — .scss files stay on the regex path.
|
|
528
|
+
*/
|
|
529
|
+
export function createStylesCssVisitor() {
|
|
530
|
+
return {
|
|
531
|
+
name: 'styles-css',
|
|
532
|
+
stage: 'visitor',
|
|
533
|
+
extensions: ['.css'],
|
|
534
|
+
getRuleIds: () => [],
|
|
535
|
+
async visit(ast, adapter, context, sourceCode) {
|
|
536
|
+
// Lazy-load to avoid circular dependency issues at module load time
|
|
537
|
+
const { extractDeclarationsFromCSSAst, extractTokensFromCSSAst, extractClassUsageFromCSSAst } = await import('./styles/cssAstExtractor.js');
|
|
538
|
+
const cssAst = ast;
|
|
539
|
+
const cssAdapter = adapter;
|
|
540
|
+
const filePath = context.filePath;
|
|
541
|
+
return {
|
|
542
|
+
violations: [],
|
|
543
|
+
facts: {
|
|
544
|
+
[filePath]: {
|
|
545
|
+
declarations: extractDeclarationsFromCSSAst(cssAst, cssAdapter, filePath, sourceCode),
|
|
546
|
+
tokens: extractTokensFromCSSAst(cssAst, cssAdapter, filePath),
|
|
547
|
+
classUsage: extractClassUsageFromCSSAst(cssAst, cssAdapter, filePath),
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
},
|
|
552
|
+
defaultConfig: {},
|
|
553
|
+
description: 'Extracts CSS declarations from tree-sitter-css ASTs (.css only)',
|
|
554
|
+
category: 'style',
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Styles reducer — runs all style detectors on existing style_* tables.
|
|
559
|
+
* Uses the analyzer.analyze([]) pattern: passes an empty file list so the
|
|
560
|
+
* analyzer skips file iteration and only queries the DB.
|
|
561
|
+
*
|
|
562
|
+
* Spec 26 Phase 2: CSS facts from the styles-css visitor are inserted into
|
|
563
|
+
* the DB before the analyzer runs. For .css files, the AST extraction replaces
|
|
564
|
+
* the regex-based styleIndexer path (which now skips .css files).
|
|
565
|
+
*/
|
|
566
|
+
export function createStylesReducer() {
|
|
567
|
+
return {
|
|
568
|
+
name: 'styles',
|
|
569
|
+
stage: 'reducer',
|
|
570
|
+
consumes: [],
|
|
571
|
+
getRuleIds: () => getRuleIdsFor('styles'),
|
|
572
|
+
async reduce(_allFacts, context) {
|
|
573
|
+
if (!context.indexHandle)
|
|
574
|
+
return { violations: [], facts: {} };
|
|
575
|
+
try {
|
|
576
|
+
// ── Insert CSS visitor facts into style_* tables ──────────────────────
|
|
577
|
+
const cssFacts = _allFacts['styles-css'];
|
|
578
|
+
if (cssFacts) {
|
|
579
|
+
const run = context.indexHandle.run.bind(context.indexHandle);
|
|
580
|
+
for (const [filePath, facts] of Object.entries(cssFacts)) {
|
|
581
|
+
// Delete old entries for this file (replaces the styleIndexer path)
|
|
582
|
+
run('DELETE FROM style_declarations WHERE file_path = ?', [filePath]);
|
|
583
|
+
run('DELETE FROM style_tokens WHERE file_path = ?', [filePath]);
|
|
584
|
+
run('DELETE FROM style_class_usage WHERE file_path = ?', [filePath]);
|
|
585
|
+
// Compute content hash for the file
|
|
586
|
+
const contentStr = JSON.stringify({ declarations: facts.declarations.length, tokens: facts.tokens.length, classUsage: facts.classUsage.length });
|
|
587
|
+
const contentHash = createHash('sha256').update(contentStr).digest('hex').slice(0, 16);
|
|
588
|
+
// Insert declarations
|
|
589
|
+
for (const decl of facts.declarations) {
|
|
590
|
+
run('INSERT INTO style_declarations (property, raw_value, normalized_value, mechanism, file_path, line, context, variant_context, token_ref, content_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [decl.property, decl.rawValue, JSON.stringify(decl.normalizedValue), decl.mechanism, decl.filePath, decl.line, decl.context, decl.variantContext, decl.tokenRef, contentHash]);
|
|
591
|
+
}
|
|
592
|
+
// Insert tokens
|
|
593
|
+
for (const tok of facts.tokens) {
|
|
594
|
+
run('INSERT INTO style_tokens (name, value, file_path, mechanism) VALUES (?, ?, ?, ?)', [tok.name, tok.value, tok.filePath, tok.mechanism]);
|
|
595
|
+
}
|
|
596
|
+
// Insert class usage
|
|
597
|
+
for (const cu of facts.classUsage) {
|
|
598
|
+
run('INSERT INTO style_class_usage (class_name, file_path, line, mechanism, unresolvable) VALUES (?, ?, ?, ?, ?)', [cu.className, cu.filePath, cu.line, cu.mechanism, cu.unresolvable ? 1 : 0]);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// ── Run the analyzer (queries the now-populated style_* tables) ───────
|
|
603
|
+
const { UniversalStylesAnalyzer } = await import('./analyzers/universal/UniversalStylesAnalyzer.js');
|
|
604
|
+
const analyzer = new UniversalStylesAnalyzer();
|
|
605
|
+
const result = await analyzer.analyze([], { ...context.config, indexHandle: context.indexHandle });
|
|
606
|
+
const factsConsumed = context.indexHandle.count('style_declarations')
|
|
607
|
+
+ context.indexHandle.count('style_tokens')
|
|
608
|
+
+ context.indexHandle.count('style_class_usage');
|
|
609
|
+
return { violations: result.violations ?? [], facts: {}, factsConsumed };
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
return { violations: [], facts: {} };
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
defaultConfig: {},
|
|
616
|
+
description: 'Detects style fragmentation, value drift, token bypass, and z-index sprawl',
|
|
617
|
+
category: 'style',
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
// ── Conventions reducer (stage 3) ────────────────────────────────────────────
|
|
621
|
+
/**
|
|
622
|
+
* Conventions reducer — queries conventions table and flags deviations
|
|
623
|
+
* (usage-pair, import-form, error-handling, export-shape, naming).
|
|
624
|
+
* Uses analyzer.analyze([]) with rawDb injected.
|
|
625
|
+
*/
|
|
626
|
+
export function createConventionsReducer() {
|
|
627
|
+
return {
|
|
628
|
+
name: 'conventions',
|
|
629
|
+
stage: 'reducer',
|
|
630
|
+
consumes: [],
|
|
631
|
+
getRuleIds: () => getRuleIdsFor('conventions'),
|
|
632
|
+
async reduce(_allFacts, context) {
|
|
633
|
+
if (!context.indexHandle)
|
|
634
|
+
return { violations: [], facts: {} };
|
|
635
|
+
try {
|
|
636
|
+
const { UniversalConventionsAnalyzer } = await import('./analyzers/universal/UniversalConventionsAnalyzer.js');
|
|
637
|
+
const analyzer = new UniversalConventionsAnalyzer();
|
|
638
|
+
// Extract per-file source map from the file-sources infrastructure visitor
|
|
639
|
+
const fileSources = _allFacts['file-sources'];
|
|
640
|
+
const sourceMap = fileSources
|
|
641
|
+
? new Map(Object.entries(fileSources))
|
|
642
|
+
: undefined;
|
|
643
|
+
// B2: Build exports map from function-index visitor facts (AST-extracted)
|
|
644
|
+
const functionIndexFacts = _allFacts['function-index'];
|
|
645
|
+
const exportsMap = functionIndexFacts
|
|
646
|
+
? new Map(Object.entries(functionIndexFacts)
|
|
647
|
+
.filter(([, data]) => data?.exports?.length)
|
|
648
|
+
.map(([filePath, data]) => [filePath, data.exports]))
|
|
649
|
+
: undefined;
|
|
650
|
+
const config = { ...context.config, indexHandle: context.indexHandle, projectRoot: context.projectRoot, sourceMap, exportsMap };
|
|
651
|
+
const result = await analyzer.analyze([], config);
|
|
652
|
+
const factsConsumed = context.indexHandle.count('conventions');
|
|
653
|
+
return { violations: result.violations ?? [], facts: {}, factsConsumed };
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
return { violations: [], facts: {} };
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
defaultConfig: {},
|
|
660
|
+
description: 'Mines codebase conventions and flags deviations',
|
|
661
|
+
category: 'style',
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
// ── Cross-Domain reducer (stage 4) ───────────────────────────────────────────
|
|
665
|
+
/**
|
|
666
|
+
* Cross-domain reducer — runs written-never-read, read-never-written,
|
|
667
|
+
* transaction-boundary risk, validation-bypass, and uncovered-risk detectors.
|
|
668
|
+
* Uses analyzer.analyze([]) with rawDb injected.
|
|
669
|
+
*/
|
|
670
|
+
export function createCrossDomainReducer() {
|
|
671
|
+
return {
|
|
672
|
+
name: 'cross-domain',
|
|
673
|
+
stage: 'derivedReducer',
|
|
674
|
+
consumes: [],
|
|
675
|
+
getRuleIds: () => getRuleIdsFor('cross-domain'),
|
|
676
|
+
async reduce(_allFacts, context) {
|
|
677
|
+
if (!context.indexHandle)
|
|
678
|
+
return { violations: [], facts: {} };
|
|
679
|
+
try {
|
|
680
|
+
const { CrossDomainAnalyzer } = await import('./analyzers/crossDomain/CrossDomainAnalyzer.js');
|
|
681
|
+
const analyzer = new CrossDomainAnalyzer();
|
|
682
|
+
const config = { ...context.config, indexHandle: context.indexHandle, projectRoot: context.projectRoot };
|
|
683
|
+
const result = await analyzer.analyze([], config);
|
|
684
|
+
// Count DB rows consumed across primary cross-domain tables
|
|
685
|
+
let factsConsumed = 0;
|
|
686
|
+
try {
|
|
687
|
+
factsConsumed += context.indexHandle.count('schema_usage');
|
|
688
|
+
}
|
|
689
|
+
catch { /* table may not exist */ }
|
|
690
|
+
try {
|
|
691
|
+
factsConsumed += context.indexHandle.count('indexed_functions');
|
|
692
|
+
}
|
|
693
|
+
catch { /* table may not exist */ }
|
|
694
|
+
return { violations: result.violations ?? [], facts: {}, factsConsumed };
|
|
695
|
+
}
|
|
696
|
+
catch (e) {
|
|
697
|
+
console.error('[cross-domain reducer] error:', e.message);
|
|
698
|
+
return { violations: [], facts: {} };
|
|
699
|
+
}
|
|
700
|
+
},
|
|
701
|
+
defaultConfig: {},
|
|
702
|
+
description: 'Detects cross-domain issues (schema lifecycle, validation bypass, coverage gaps)',
|
|
703
|
+
category: 'architecture',
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
// ── Invariants reducer (stage 3) ──────────────────────────────────────────────
|
|
707
|
+
/**
|
|
708
|
+
* Invariants reducer — enforces user-defined invariant rules from .codeauditor.json.
|
|
709
|
+
* Receives the full file list via context.files and runs the rule engine across
|
|
710
|
+
* all files at once (call-constraint rules inherently need cross-file scope).
|
|
711
|
+
*/
|
|
712
|
+
export function createInvariantsReducer() {
|
|
713
|
+
return {
|
|
714
|
+
name: 'invariants',
|
|
715
|
+
stage: 'reducer',
|
|
716
|
+
consumes: [],
|
|
717
|
+
getRuleIds: () => getRuleIdsFor('invariants'),
|
|
718
|
+
async reduce(_allFacts, context) {
|
|
719
|
+
try {
|
|
720
|
+
const { analyzeInvariants } = await import('./analyzers/invariantsAnalyzer.js');
|
|
721
|
+
// Full file list is in _infra.files (merged into context.config via pipeline)
|
|
722
|
+
const files = context.config.files ?? [];
|
|
723
|
+
// Extract per-file source map from the file-sources infrastructure visitor
|
|
724
|
+
const fileSources = _allFacts['file-sources'];
|
|
725
|
+
const sourceMap = fileSources
|
|
726
|
+
? new Map(Object.entries(fileSources))
|
|
727
|
+
: undefined;
|
|
728
|
+
const knownFiles = sourceMap
|
|
729
|
+
? new Set(sourceMap.keys())
|
|
730
|
+
: undefined;
|
|
731
|
+
// B1: Build fileData from the function-index visitor's AST-extracted
|
|
732
|
+
// imports/exports — replaces regex-based extractImports/extractExportedSymbols
|
|
733
|
+
const functionIndexFacts = _allFacts['function-index'];
|
|
734
|
+
let fileData;
|
|
735
|
+
if (functionIndexFacts) {
|
|
736
|
+
fileData = new Map();
|
|
737
|
+
const projectRoot = context.projectRoot;
|
|
738
|
+
for (const [filePath, data] of Object.entries(functionIndexFacts)) {
|
|
739
|
+
if (data && (data.imports || data.exports)) {
|
|
740
|
+
// Normalize absolute paths to project-relative (matching checkRules normalization)
|
|
741
|
+
const normalized = path.isAbsolute(filePath)
|
|
742
|
+
? path.relative(projectRoot, filePath)
|
|
743
|
+
: filePath;
|
|
744
|
+
fileData.set(normalized, {
|
|
745
|
+
imports: (data.imports || []).map((imp) => ({
|
|
746
|
+
moduleSpecifier: imp.moduleSpecifier,
|
|
747
|
+
isStatic: imp.isStatic,
|
|
748
|
+
isDynamic: imp.isDynamic,
|
|
749
|
+
isRequire: imp.isRequire,
|
|
750
|
+
line: imp.line,
|
|
751
|
+
})),
|
|
752
|
+
exports: (data.exports || []).map((exp) => ({
|
|
753
|
+
name: exp.name,
|
|
754
|
+
line: exp.location?.start?.line ?? 0,
|
|
755
|
+
})),
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const result = await analyzeInvariants(files, { ...context.config, sourceMap, knownFiles, fileData }, { projectRoot: context.projectRoot, indexHandle: context.indexHandle });
|
|
761
|
+
return {
|
|
762
|
+
violations: result.violations ?? [],
|
|
763
|
+
facts: {},
|
|
764
|
+
factsConsumed: files.length,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
return { violations: [], facts: {} };
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
defaultConfig: {},
|
|
772
|
+
description: 'Enforces custom invariant rules (import bans, call constraints, module boundaries, naming)',
|
|
773
|
+
category: 'architecture',
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
// ── Schema visitors and reducer (Task #41 Part A) ──────────────────────────
|
|
777
|
+
// Need provenance for per-file gate + reference extraction
|
|
778
|
+
let _provenanceModule = null;
|
|
779
|
+
async function _getProvenanceModule() {
|
|
780
|
+
if (!_provenanceModule) {
|
|
781
|
+
_provenanceModule = await import('./analyzers/provenance.js');
|
|
782
|
+
}
|
|
783
|
+
return _provenanceModule;
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Schema SQL visitor (.sql files) — extracts raw DDL source for the Stage 3
|
|
787
|
+
* reducer to replay into the known-tables catalog.
|
|
788
|
+
*/
|
|
789
|
+
export function createSchemaSqlVisitor() {
|
|
790
|
+
return {
|
|
791
|
+
name: 'schema-sql',
|
|
792
|
+
stage: 'visitor',
|
|
793
|
+
extensions: ['.sql'],
|
|
794
|
+
getRuleIds: () => [],
|
|
795
|
+
async visit(_ast, _adapter, context, sourceCode) {
|
|
796
|
+
return {
|
|
797
|
+
violations: [],
|
|
798
|
+
facts: { [context.filePath]: { ddlSource: sourceCode } },
|
|
799
|
+
};
|
|
800
|
+
},
|
|
801
|
+
defaultConfig: {},
|
|
802
|
+
description: 'Captures SQL DDL source for known-table catalog',
|
|
803
|
+
category: 'database',
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Schema code visitor (.ts/.tsx/.js/.jsx) — per-file analysis for naming conventions,
|
|
808
|
+
* query patterns, SQL injection, table references, and ORM table extraction.
|
|
809
|
+
*/
|
|
810
|
+
export function createSchemaCodeVisitor() {
|
|
811
|
+
const getAnalyzer = lazySingleton(() => import('./analyzers/universal/UniversalSchemaAnalyzer.js').then((m) => ({
|
|
812
|
+
analyzer: new m.UniversalSchemaAnalyzer(),
|
|
813
|
+
defaults: m.DEFAULT_SCHEMA_CONFIG,
|
|
814
|
+
})));
|
|
815
|
+
return {
|
|
816
|
+
name: 'schema-code',
|
|
817
|
+
stage: 'visitor',
|
|
818
|
+
extensions: ['.ts', '.tsx', '.js', '.jsx'],
|
|
819
|
+
getRuleIds: () => getRuleIdsFor('schema'),
|
|
820
|
+
async visit(ast, adapter, context, sourceCode) {
|
|
821
|
+
const { analyzer: a, defaults } = await getAnalyzer();
|
|
822
|
+
const pm = await _getProvenanceModule();
|
|
823
|
+
const violations = [];
|
|
824
|
+
const indexFacts = [];
|
|
825
|
+
// Extract ORM table names (Drizzle) from source
|
|
826
|
+
const ormTables = [];
|
|
827
|
+
const drizzleRe = /(?:pgTable|mysqlTable|sqliteTable)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
828
|
+
let dm;
|
|
829
|
+
while ((dm = drizzleRe.exec(sourceCode)) !== null) {
|
|
830
|
+
ormTables.push(dm[1]);
|
|
831
|
+
}
|
|
832
|
+
// Extract DDL from sql.exec(...) string literals inside Durable Object classes.
|
|
833
|
+
// These are CREATE TABLE / DROP TABLE statements at runtime that migration
|
|
834
|
+
// discovery never sees. Extracting them completes the authoritative catalog
|
|
835
|
+
// so unknown-table doesn't false-positive on DO-local tables.
|
|
836
|
+
const doDDL = [];
|
|
837
|
+
const doTemplateDDL = /`([^`]*(?:CREATE|DROP|ALTER)\s+(?:TABLE|VIRTUAL\s+TABLE)\s+[^`]+)`/gis;
|
|
838
|
+
const doStringDDL = /(["'])((?:\s*(?:CREATE|DROP|ALTER)\s+(?:TABLE|VIRTUAL\s+TABLE)\s+[^"']+))\1/gis;
|
|
839
|
+
let ddlMatch;
|
|
840
|
+
while ((ddlMatch = doTemplateDDL.exec(sourceCode)) !== null) {
|
|
841
|
+
const sql = ddlMatch[1].trim();
|
|
842
|
+
if (sql)
|
|
843
|
+
doDDL.push(sql);
|
|
844
|
+
}
|
|
845
|
+
while ((ddlMatch = doStringDDL.exec(sourceCode)) !== null) {
|
|
846
|
+
const sql = ddlMatch[2].trim();
|
|
847
|
+
if (sql)
|
|
848
|
+
doDDL.push(sql);
|
|
849
|
+
}
|
|
850
|
+
// Build provenance context for this file — defaults from DEFAULT_SCHEMA_CONFIG
|
|
851
|
+
const schemaConfig = (context.config ?? {});
|
|
852
|
+
const detectionMode = schemaConfig.detection?.mode ?? 'hybrid';
|
|
853
|
+
const provenanceContext = pm.buildProvenanceContext(ast, adapter, sourceCode, {
|
|
854
|
+
mode: detectionMode,
|
|
855
|
+
dbReceiverNames: schemaConfig.dbReceiverNames ?? defaults.dbReceiverNames,
|
|
856
|
+
dbBindingNames: schemaConfig.dbBindingNames ?? defaults.dbBindingNames,
|
|
857
|
+
dbCallMethods: schemaConfig.dbCallMethods ?? defaults.dbCallMethods,
|
|
858
|
+
});
|
|
859
|
+
// File gate — skip files without DB usage
|
|
860
|
+
if (!a.passesFileGate(context.filePath, sourceCode, schemaConfig, provenanceContext)) {
|
|
861
|
+
const facts = { [context.filePath]: { tableRefs: [], ormTables } };
|
|
862
|
+
if (doDDL.length > 0)
|
|
863
|
+
facts[context.filePath].ddlSource = doDDL.join(';\n');
|
|
864
|
+
return { violations: [], facts };
|
|
865
|
+
}
|
|
866
|
+
// Build known-tables set from schemas config (pre-pipeline + DB-loaded schemas)
|
|
867
|
+
const schemas = schemaConfig.schemas ?? [];
|
|
868
|
+
const knownTablesArr = schemaConfig.knownTables ?? [];
|
|
869
|
+
const allTables = new Set();
|
|
870
|
+
for (const t of knownTablesArr)
|
|
871
|
+
allTables.add(t);
|
|
872
|
+
for (const schema of schemas) {
|
|
873
|
+
for (const table of (schema.tables ?? [])) {
|
|
874
|
+
allTables.add(table.name);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
// Find table references (per-file, uses allTables for short-id false-positive filtering)
|
|
878
|
+
const tableRefs = a.findTableReferences(ast, adapter, sourceCode, schemaConfig, provenanceContext, allTables);
|
|
879
|
+
// Record schema usage → emit as indexFacts via the shared instance
|
|
880
|
+
a.recordTableUsage(ast, adapter, context.filePath, tableRefs);
|
|
881
|
+
const pending = a.getPendingSchemaRecords();
|
|
882
|
+
// Emit clear-by-file + per-usage index facts
|
|
883
|
+
if (pending.clearFiles.length > 0) {
|
|
884
|
+
for (const filePath of pending.clearFiles) {
|
|
885
|
+
indexFacts.push({ table: 'schema_usage', data: { _action: 'clear-by-file', file_path: filePath } });
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
for (const usage of pending.usages) {
|
|
889
|
+
indexFacts.push({
|
|
890
|
+
table: 'schema_usage',
|
|
891
|
+
data: {
|
|
892
|
+
file_path: usage.filePath,
|
|
893
|
+
table_name: usage.tableName,
|
|
894
|
+
function_name: usage.functionName,
|
|
895
|
+
usage_type: usage.usageType,
|
|
896
|
+
line: usage.line,
|
|
897
|
+
column: usage.column,
|
|
898
|
+
raw_query: usage.rawQuery,
|
|
899
|
+
},
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
// Check naming conventions
|
|
903
|
+
if (schemaConfig.checkNamingConventions !== false) {
|
|
904
|
+
violations.push(...a.checkNamingConventions(tableRefs, context.filePath));
|
|
905
|
+
}
|
|
906
|
+
// Check query patterns
|
|
907
|
+
if (schemaConfig.validateQueryPatterns !== false) {
|
|
908
|
+
violations.push(...a.checkQueryPatterns(ast, adapter, sourceCode, schemaConfig));
|
|
909
|
+
}
|
|
910
|
+
// Check SQL injection
|
|
911
|
+
violations.push(...a.checkSQLInjection(ast, adapter, sourceCode));
|
|
912
|
+
// Emit facts for the Stage 3 reducer
|
|
913
|
+
const fileFacts = {
|
|
914
|
+
tableRefs: tableRefs.map((r) => ({
|
|
915
|
+
table: r.table,
|
|
916
|
+
type: r.type,
|
|
917
|
+
line: r.location.line,
|
|
918
|
+
column: r.location.column,
|
|
919
|
+
context: r.context,
|
|
920
|
+
})),
|
|
921
|
+
ormTables,
|
|
922
|
+
};
|
|
923
|
+
if (doDDL.length > 0)
|
|
924
|
+
fileFacts.ddlSource = doDDL.join(';\n');
|
|
925
|
+
return {
|
|
926
|
+
violations,
|
|
927
|
+
facts: { [context.filePath]: fileFacts },
|
|
928
|
+
indexFacts: indexFacts.length > 0 ? indexFacts : undefined,
|
|
929
|
+
};
|
|
930
|
+
},
|
|
931
|
+
defaultConfig: {},
|
|
932
|
+
description: 'Per-file schema analysis: naming conventions, query patterns, SQL injection, table references',
|
|
933
|
+
category: 'database',
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Schema Prisma visitor (.prisma files) — extracts model names for known-table catalog.
|
|
938
|
+
*/
|
|
939
|
+
export function createSchemaPrismaVisitor() {
|
|
940
|
+
return {
|
|
941
|
+
name: 'schema-prisma',
|
|
942
|
+
stage: 'visitor',
|
|
943
|
+
extensions: ['.prisma'],
|
|
944
|
+
getRuleIds: () => [],
|
|
945
|
+
async visit(_ast, _adapter, context, sourceCode) {
|
|
946
|
+
const models = [];
|
|
947
|
+
const modelRe = /model\s+(\w+)\s*\{/g;
|
|
948
|
+
let m;
|
|
949
|
+
while ((m = modelRe.exec(sourceCode)) !== null) {
|
|
950
|
+
models.push(m[1]);
|
|
951
|
+
}
|
|
952
|
+
return {
|
|
953
|
+
violations: [],
|
|
954
|
+
facts: { [context.filePath]: { prismaModels: models } },
|
|
955
|
+
};
|
|
956
|
+
},
|
|
957
|
+
defaultConfig: {},
|
|
958
|
+
description: 'Extracts Prisma model names for known-table catalog',
|
|
959
|
+
category: 'database',
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Schema JSON visitor (.json files) — parses JSON for the Stage 3 reducer to validate.
|
|
964
|
+
*/
|
|
965
|
+
export function createSchemaJsonVisitor() {
|
|
966
|
+
return {
|
|
967
|
+
name: 'schema-json',
|
|
968
|
+
stage: 'visitor',
|
|
969
|
+
extensions: ['.json'],
|
|
970
|
+
getRuleIds: () => [],
|
|
971
|
+
async visit(_ast, _adapter, context, sourceCode) {
|
|
972
|
+
let parsed = null;
|
|
973
|
+
try {
|
|
974
|
+
parsed = JSON.parse(sourceCode);
|
|
975
|
+
}
|
|
976
|
+
catch {
|
|
977
|
+
// Invalid JSON — skip
|
|
978
|
+
}
|
|
979
|
+
return {
|
|
980
|
+
violations: [],
|
|
981
|
+
facts: { [context.filePath]: { jsonParsed: parsed, jsonRaw: sourceCode } },
|
|
982
|
+
};
|
|
983
|
+
},
|
|
984
|
+
defaultConfig: {},
|
|
985
|
+
description: 'Parses JSON files for schema validation',
|
|
986
|
+
category: 'database',
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Schema Stage 3 reducer — cross-file unknown-table detection and JSON schema validation.
|
|
991
|
+
*
|
|
992
|
+
* Consumes facts from schema-sql, schema-code, schema-prisma, and schema-json visitors.
|
|
993
|
+
* Builds the complete known-tables catalog and checks all table references against it.
|
|
994
|
+
*/
|
|
995
|
+
export function createSchemaReducer() {
|
|
996
|
+
const getAnalyzer = lazySingleton(() => import('./analyzers/universal/UniversalSchemaAnalyzer.js').then((m) => ({
|
|
997
|
+
analyzer: new m.UniversalSchemaAnalyzer(),
|
|
998
|
+
defaults: m.DEFAULT_SCHEMA_CONFIG,
|
|
999
|
+
})));
|
|
1000
|
+
return {
|
|
1001
|
+
name: 'schema',
|
|
1002
|
+
stage: 'reducer',
|
|
1003
|
+
consumes: [],
|
|
1004
|
+
getRuleIds: () => getRuleIdsFor('schema'),
|
|
1005
|
+
async reduce(allFacts, context) {
|
|
1006
|
+
const { analyzer: a } = await getAnalyzer();
|
|
1007
|
+
const violations = [];
|
|
1008
|
+
const schemaConfig = (context.config ?? {});
|
|
1009
|
+
// allFacts is keyed by visitor name, each value is { [filePath]: { ...per-file facts } }
|
|
1010
|
+
// Flatten to per-file iteration helpers.
|
|
1011
|
+
const perFile = function* () {
|
|
1012
|
+
for (const [, visitorFacts] of Object.entries(allFacts)) {
|
|
1013
|
+
if (typeof visitorFacts === 'object' && visitorFacts !== null) {
|
|
1014
|
+
for (const [filePath, fact] of Object.entries(visitorFacts)) {
|
|
1015
|
+
if (typeof fact === 'object' && fact !== null) {
|
|
1016
|
+
yield [filePath, fact];
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
// ── 1. Build known-tables catalog ──────────────────────────────────────
|
|
1023
|
+
const knownTables = new Set();
|
|
1024
|
+
// 1a. SQL DDL replay — collect all .sql file facts, sort by numeric prefix, replay
|
|
1025
|
+
const sqlFiles = [];
|
|
1026
|
+
for (const [filePath, fact] of perFile()) {
|
|
1027
|
+
if ('ddlSource' in fact) {
|
|
1028
|
+
sqlFiles.push({ filePath, source: fact.ddlSource });
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
// Sort by numeric prefix in basename: "009_something" < "0010_rename"
|
|
1032
|
+
// Falls back to localeCompare for non-numeric-prefixed names.
|
|
1033
|
+
const numericPrefix = (p) => {
|
|
1034
|
+
const base = p.split('/').pop() ?? p;
|
|
1035
|
+
const m = base.match(/^(\d+)/);
|
|
1036
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
1037
|
+
};
|
|
1038
|
+
sqlFiles.sort((a, b) => {
|
|
1039
|
+
const na = numericPrefix(a.filePath);
|
|
1040
|
+
const nb = numericPrefix(b.filePath);
|
|
1041
|
+
if (na !== nb)
|
|
1042
|
+
return na - nb;
|
|
1043
|
+
return a.filePath.localeCompare(b.filePath);
|
|
1044
|
+
});
|
|
1045
|
+
for (const sqlFile of sqlFiles) {
|
|
1046
|
+
a.processMigrationSource(sqlFile.source, knownTables);
|
|
1047
|
+
}
|
|
1048
|
+
// 1b. ORM tables from code files
|
|
1049
|
+
for (const [, fact] of perFile()) {
|
|
1050
|
+
const ormTables = fact.ormTables ?? [];
|
|
1051
|
+
for (const t of ormTables)
|
|
1052
|
+
knownTables.add(t);
|
|
1053
|
+
}
|
|
1054
|
+
// 1c. Prisma models
|
|
1055
|
+
for (const [, fact] of perFile()) {
|
|
1056
|
+
const prismaModels = fact.prismaModels ?? [];
|
|
1057
|
+
for (const m of prismaModels)
|
|
1058
|
+
knownTables.add(m);
|
|
1059
|
+
}
|
|
1060
|
+
// ── 2. Unknown-table detection ────────────────────────────────────────
|
|
1061
|
+
//
|
|
1062
|
+
// Fail-open guardrail: we can only accuse when the table catalog is
|
|
1063
|
+
// built from authoritative sources. Empty catalog → cannot accuse.
|
|
1064
|
+
//
|
|
1065
|
+
// Authoritative sources (independent of query sites, not circular):
|
|
1066
|
+
// • SQL migration files via schema-sql (step 1a)
|
|
1067
|
+
// • ORM model definitions via schema-code (step 1b)
|
|
1068
|
+
// • Prisma schemas via schema-prisma (step 1c)
|
|
1069
|
+
//
|
|
1070
|
+
// External config tables (schemas from CodeIndexDB, wrangler.toml
|
|
1071
|
+
// pre-discovered tables) are added as a bonus but are not required.
|
|
1072
|
+
//
|
|
1073
|
+
// NOT authoritative: tables inferred from query-text alone. Those
|
|
1074
|
+
// never enter the catalog (table refs are for checking, not building).
|
|
1075
|
+
// Merge external tables from config (bonus, not required).
|
|
1076
|
+
const externalKnownTables = schemaConfig.knownTables ?? [];
|
|
1077
|
+
for (const t of externalKnownTables)
|
|
1078
|
+
knownTables.add(t);
|
|
1079
|
+
if (knownTables.size > 0) {
|
|
1080
|
+
// Collect all table references across all files
|
|
1081
|
+
const allTableRefs = [];
|
|
1082
|
+
for (const [filePath, fact] of perFile()) {
|
|
1083
|
+
const refs = fact.tableRefs ?? [];
|
|
1084
|
+
for (const ref of refs) {
|
|
1085
|
+
allTableRefs.push({ file: filePath, ...ref });
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
// 10:1 fail-open ratio guard
|
|
1089
|
+
const unknownRefs = allTableRefs.filter(ref => !knownTables.has(ref.table));
|
|
1090
|
+
if (unknownRefs.length / Math.max(knownTables.size, 1) <= 10) {
|
|
1091
|
+
for (const ref of unknownRefs) {
|
|
1092
|
+
const suggestions = a.getNearestTableSuggestions(ref.table, knownTables, 2);
|
|
1093
|
+
const msg = suggestions.length > 0
|
|
1094
|
+
? `Reference to unknown table '${ref.table}' (${ref.type}). Did you mean: ${suggestions.join(', ')}?`
|
|
1095
|
+
: `Reference to unknown table '${ref.table}' (${ref.type})`;
|
|
1096
|
+
violations.push({
|
|
1097
|
+
file: ref.file,
|
|
1098
|
+
line: ref.line,
|
|
1099
|
+
column: ref.column,
|
|
1100
|
+
severity: 'suggestion',
|
|
1101
|
+
message: msg,
|
|
1102
|
+
rule: 'unknown-table',
|
|
1103
|
+
analyzer: 'schema',
|
|
1104
|
+
symbol: ref.table,
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
// ── 3. JSON schema validation ──────────────────────────────────────────
|
|
1110
|
+
if (schemaConfig.validateJsonSchemas !== false) {
|
|
1111
|
+
try {
|
|
1112
|
+
// Build content Map from json facts for the refactored analyzeJsonSchemas
|
|
1113
|
+
const jsonContents = new Map();
|
|
1114
|
+
let jsonFactCount = 0;
|
|
1115
|
+
for (const [filePath, fact] of perFile()) {
|
|
1116
|
+
if ('jsonParsed' in fact) {
|
|
1117
|
+
jsonFactCount++;
|
|
1118
|
+
const raw = (fact.jsonParsed ?? null);
|
|
1119
|
+
// parsed is the result of JSON.parse: null on failure, or the
|
|
1120
|
+
// deserialized value. Only accept objects as usable parsed content.
|
|
1121
|
+
const parsed = raw !== null && typeof raw === 'object' ? raw : null;
|
|
1122
|
+
jsonContents.set(filePath, {
|
|
1123
|
+
parsed,
|
|
1124
|
+
raw: (fact.jsonRaw ?? fact.sourceCode ?? ''),
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
const jsonResult = a.analyzeJsonSchemas(jsonContents, schemaConfig);
|
|
1129
|
+
violations.push(...(jsonResult?.violations ?? []));
|
|
1130
|
+
}
|
|
1131
|
+
catch (e) {
|
|
1132
|
+
// JSON schema validation is best-effort (non-fatal)
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
violations,
|
|
1137
|
+
facts: {},
|
|
1138
|
+
factsConsumed: [...perFile()].length,
|
|
1139
|
+
};
|
|
1140
|
+
},
|
|
1141
|
+
defaultConfig: {},
|
|
1142
|
+
description: 'Cross-file schema analysis: unknown-table detection and JSON schema validation',
|
|
1143
|
+
category: 'database',
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
//# sourceMappingURL=pipelineAdapters.js.map
|