code-auditor-mcp 3.0.6 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +245 -0
- package/README.md +55 -1
- package/dist/analyzers/__tests__/fixtures/spec-17/anonymous-callback-map.d.ts +21 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/anonymous-callback-map.d.ts.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/anonymous-callback-map.js +21 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/anonymous-callback-map.js.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/barrel-test-migration.d.ts +13 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/barrel-test-migration.d.ts.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/barrel-test-migration.js +12 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/barrel-test-migration.js.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/direct-access-allow-config.d.ts +11 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/direct-access-allow-config.d.ts.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/direct-access-allow-config.js +22 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/direct-access-allow-config.js.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/exported-undocumented.d.ts +9 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/exported-undocumented.d.ts.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/exported-undocumented.js +13 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/exported-undocumented.js.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/for-loop-query.d.ts +13 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/for-loop-query.d.ts.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/for-loop-query.js +18 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/for-loop-query.js.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/import-node-builtin.d.ts +10 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/import-node-builtin.d.ts.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/import-node-builtin.js +16 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/import-node-builtin.js.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/jsx-event-handler.d.ts +14 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/jsx-event-handler.d.ts.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/jsx-event-handler.js +18 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/jsx-event-handler.js.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/nested-loops-query.d.ts +13 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/nested-loops-query.d.ts.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/nested-loops-query.js +22 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/nested-loops-query.js.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/nine-line-repeated.d.ts +10 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/nine-line-repeated.d.ts.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/nine-line-repeated.js +20 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/nine-line-repeated.js.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/private-methods.d.ts +18 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/private-methods.d.ts.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/private-methods.js +28 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/private-methods.js.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/scope-all-config.d.ts +11 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/scope-all-config.d.ts.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/scope-all-config.js +31 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/scope-all-config.js.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/sql-tagged-template.d.ts +11 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/sql-tagged-template.d.ts.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/sql-tagged-template.js +18 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/sql-tagged-template.js.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/structural-similar-methods.d.ts +13 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/structural-similar-methods.d.ts.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/structural-similar-methods.js +61 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/structural-similar-methods.js.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/template-prefix-table.d.ts +10 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/template-prefix-table.d.ts.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/template-prefix-table.js +15 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/template-prefix-table.js.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/token-identical-20-lines.d.ts +11 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/token-identical-20-lines.d.ts.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/token-identical-20-lines.js +49 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/token-identical-20-lines.js.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/tsx-no-db-usage.d.ts +10 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/tsx-no-db-usage.d.ts.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/tsx-no-db-usage.js +19 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/tsx-no-db-usage.js.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/twenty-methods-vs-complex.d.ts +37 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/twenty-methods-vs-complex.d.ts.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/twenty-methods-vs-complex.js +73 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/twenty-methods-vs-complex.js.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/unique-method-74-lines.d.ts +12 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/unique-method-74-lines.d.ts.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/unique-method-74-lines.js +82 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/unique-method-74-lines.js.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/word-the-in-comment.d.ts +10 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/word-the-in-comment.d.ts.map +1 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/word-the-in-comment.js +15 -0
- package/dist/analyzers/__tests__/fixtures/spec-17/word-the-in-comment.js.map +1 -0
- package/dist/analyzers/cross-language/APIContractAnalyzer.js +3 -3
- package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
- package/dist/analyzers/cross-language/CrossLanguageSOLIDAnalyzer.js +1 -1
- package/dist/analyzers/cross-language/CrossLanguageSOLIDAnalyzer.js.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js +2 -2
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.js +2 -2
- package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
- package/dist/analyzers/reactAnalyzer.js +7 -22
- package/dist/analyzers/reactAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +40 -2
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js +186 -29
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +30 -0
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +164 -11
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +51 -7
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +354 -48
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +8 -4
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +51 -38
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +59 -39
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +412 -153
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +26 -2
- package/dist/auditRunner.js.map +1 -1
- package/dist/cli.js +51 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/defaults.d.ts +32 -0
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +62 -4
- package/dist/config/defaults.js.map +1 -1
- package/dist/generators/AWSQConfigGenerator.d.ts +20 -0
- package/dist/generators/AWSQConfigGenerator.d.ts.map +1 -1
- package/dist/generators/AWSQConfigGenerator.js +48 -19
- package/dist/generators/AWSQConfigGenerator.js.map +1 -1
- package/dist/generators/AiderConfigGenerator.d.ts +18 -0
- package/dist/generators/AiderConfigGenerator.d.ts.map +1 -1
- package/dist/generators/AiderConfigGenerator.js +48 -10
- package/dist/generators/AiderConfigGenerator.js.map +1 -1
- package/dist/generators/ClaudeConfigGenerator.d.ts +9 -3
- package/dist/generators/ClaudeConfigGenerator.d.ts.map +1 -1
- package/dist/generators/ClaudeConfigGenerator.js +30 -42
- package/dist/generators/ClaudeConfigGenerator.js.map +1 -1
- package/dist/generators/ClineConfigGenerator.d.ts +13 -0
- package/dist/generators/ClineConfigGenerator.d.ts.map +1 -1
- package/dist/generators/ClineConfigGenerator.js +44 -11
- package/dist/generators/ClineConfigGenerator.js.map +1 -1
- package/dist/generators/CodeiumConfigGenerator.d.ts +15 -0
- package/dist/generators/CodeiumConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CodeiumConfigGenerator.js +45 -15
- package/dist/generators/CodeiumConfigGenerator.js.map +1 -1
- package/dist/generators/CodexConfigGenerator.d.ts +29 -0
- package/dist/generators/CodexConfigGenerator.d.ts.map +1 -0
- package/dist/generators/CodexConfigGenerator.js +80 -0
- package/dist/generators/CodexConfigGenerator.js.map +1 -0
- package/dist/generators/ConfigGeneratorFactory.d.ts +10 -1
- package/dist/generators/ConfigGeneratorFactory.d.ts.map +1 -1
- package/dist/generators/ConfigGeneratorFactory.js +22 -8
- package/dist/generators/ConfigGeneratorFactory.js.map +1 -1
- package/dist/generators/ContinueConfigGenerator.d.ts +16 -1
- package/dist/generators/ContinueConfigGenerator.d.ts.map +1 -1
- package/dist/generators/ContinueConfigGenerator.js +41 -73
- package/dist/generators/ContinueConfigGenerator.js.map +1 -1
- package/dist/generators/CopilotConfigGenerator.d.ts +13 -2
- package/dist/generators/CopilotConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CopilotConfigGenerator.js +31 -51
- package/dist/generators/CopilotConfigGenerator.js.map +1 -1
- package/dist/generators/CursorConfigGenerator.d.ts +8 -1
- package/dist/generators/CursorConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CursorConfigGenerator.js +27 -51
- package/dist/generators/CursorConfigGenerator.js.map +1 -1
- package/dist/generators/GeminiConfigGenerator.d.ts +23 -0
- package/dist/generators/GeminiConfigGenerator.d.ts.map +1 -0
- package/dist/generators/GeminiConfigGenerator.js +59 -0
- package/dist/generators/GeminiConfigGenerator.js.map +1 -0
- package/dist/generators/JetBrainsConfigGenerator.d.ts +11 -0
- package/dist/generators/JetBrainsConfigGenerator.d.ts.map +1 -1
- package/dist/generators/JetBrainsConfigGenerator.js +39 -21
- package/dist/generators/JetBrainsConfigGenerator.js.map +1 -1
- package/dist/generators/VSCodeConfigGenerator.d.ts +14 -0
- package/dist/generators/VSCodeConfigGenerator.d.ts.map +1 -1
- package/dist/generators/VSCodeConfigGenerator.js +43 -15
- package/dist/generators/VSCodeConfigGenerator.js.map +1 -1
- package/dist/hooks/codex.d.ts +20 -0
- package/dist/hooks/codex.d.ts.map +1 -0
- package/dist/hooks/codex.js +91 -0
- package/dist/hooks/codex.js.map +1 -0
- package/dist/hooks/core.d.ts +62 -0
- package/dist/hooks/core.d.ts.map +1 -0
- package/dist/hooks/core.js +119 -0
- package/dist/hooks/core.js.map +1 -0
- package/dist/hooks/cursor.d.ts +72 -0
- package/dist/hooks/cursor.d.ts.map +1 -0
- package/dist/hooks/cursor.js +163 -0
- package/dist/hooks/cursor.js.map +1 -0
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -4
- package/dist/index.js.map +1 -1
- package/dist/installer.d.ts +30 -0
- package/dist/installer.d.ts.map +1 -0
- package/dist/installer.js +355 -0
- package/dist/installer.js.map +1 -0
- package/dist/languages/UniversalAnalyzer.d.ts +7 -0
- package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
- package/dist/languages/UniversalAnalyzer.js +16 -1
- package/dist/languages/UniversalAnalyzer.js.map +1 -1
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/code-auditor/SKILL.md +13 -5
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Universal Schema Analyzer
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Universal Schema Analyzer — Spec 17 R2
|
|
3
|
+
*
|
|
4
|
+
* R2.1: SQL-context-only extraction (AST-based, not regex scan-all-strings).
|
|
5
|
+
* R2.2: File gate — only analyze files with DB usage indicators.
|
|
6
|
+
* R2.3: Template expressions resolve to wildcards for known-table matching.
|
|
7
|
+
* R2.4: Unknown-table findings include SQL kind, line, and Levenshtein suggestions.
|
|
8
|
+
* R2.5: Legacy scan-all-strings path DELETED.
|
|
9
|
+
* R7: schema/unknown-table severity is "suggestion".
|
|
5
10
|
*/
|
|
6
11
|
import { UniversalAnalyzer } from '../../languages/UniversalAnalyzer.js';
|
|
12
|
+
import picomatch from 'picomatch';
|
|
7
13
|
export const DEFAULT_SCHEMA_CONFIG = {
|
|
8
14
|
enableTableUsageTracking: true,
|
|
9
15
|
checkMissingReferences: true,
|
|
@@ -19,7 +25,13 @@ export const DEFAULT_SCHEMA_CONFIG = {
|
|
|
19
25
|
schemaFilePatterns: ['*.schema.json', '*-schema.json'],
|
|
20
26
|
dataFilePatterns: ['*.data.json', '*.example.json'],
|
|
21
27
|
strictMode: false,
|
|
22
|
-
allowAdditionalProperties: true
|
|
28
|
+
allowAdditionalProperties: true,
|
|
29
|
+
// Spec-17 R2 defaults
|
|
30
|
+
sqlTagNames: ['sql', 'db'],
|
|
31
|
+
dbReceiverNames: ['db', 'database', 'sql', 'stmt', 'connection', 'pool', 'client'],
|
|
32
|
+
dbCallMethods: ['exec', 'prepare', 'batch', 'run', 'all', 'first', 'query', 'get', 'each'],
|
|
33
|
+
dbBindingNames: ['env.DB'],
|
|
34
|
+
fileGateGlobs: ['**/*.sql', '**/migrations/**'],
|
|
23
35
|
};
|
|
24
36
|
import { promises as fs } from 'fs';
|
|
25
37
|
export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
@@ -29,22 +41,16 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
29
41
|
// Track references across files
|
|
30
42
|
tableReferences = new Map();
|
|
31
43
|
columnReferences = new Map();
|
|
32
|
-
/**
|
|
33
|
-
* Override analyze to handle both TypeScript files and JSON files
|
|
34
|
-
*/
|
|
35
44
|
async analyze(files, config) {
|
|
36
45
|
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
37
46
|
const codeFiles = files.filter(f => !f.endsWith('.json'));
|
|
38
|
-
// Analyze code files using the standard Universal approach
|
|
39
47
|
const codeResult = codeFiles.length > 0 ? await super.analyze(codeFiles, config) : {
|
|
40
48
|
violations: [],
|
|
41
49
|
errors: [],
|
|
42
50
|
filesProcessed: 0,
|
|
43
51
|
executionTime: 0
|
|
44
52
|
};
|
|
45
|
-
// Analyze JSON schema files
|
|
46
53
|
const jsonResult = await this.analyzeJsonSchemas(jsonFiles, config);
|
|
47
|
-
// Combine results
|
|
48
54
|
return {
|
|
49
55
|
violations: [...codeResult.violations, ...jsonResult.violations],
|
|
50
56
|
errors: [...(codeResult.errors || []), ...(jsonResult.errors || [])],
|
|
@@ -55,11 +61,14 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
55
61
|
async analyzeAST(ast, adapter, config, sourceCode) {
|
|
56
62
|
const violations = [];
|
|
57
63
|
const finalConfig = { ...DEFAULT_SCHEMA_CONFIG, ...config };
|
|
64
|
+
// R2.2 — File gate: only analyze files with DB context
|
|
65
|
+
if (!this.passesFileGate(ast.filePath, sourceCode, finalConfig)) {
|
|
66
|
+
return violations;
|
|
67
|
+
}
|
|
58
68
|
// Get available schemas
|
|
59
69
|
const schemas = finalConfig.schemas || [];
|
|
60
70
|
const allTables = new Set();
|
|
61
71
|
const tableColumns = new Map();
|
|
62
|
-
// Build lookup maps
|
|
63
72
|
for (const schema of schemas) {
|
|
64
73
|
for (const table of schema.tables) {
|
|
65
74
|
allTables.add(table.name);
|
|
@@ -67,22 +76,25 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
67
76
|
tableColumns.set(table.name, columns);
|
|
68
77
|
}
|
|
69
78
|
}
|
|
70
|
-
// Check if schemas are required but missing
|
|
71
79
|
if (finalConfig.requiredSchemas && finalConfig.requiredSchemas.length > 0 && schemas.length === 0) {
|
|
72
80
|
violations.push(this.createViolation(ast.filePath, { line: 1, column: 1 }, 'No database schemas loaded for analysis', 'warning', 'missing-schemas'));
|
|
73
81
|
return violations;
|
|
74
82
|
}
|
|
75
|
-
//
|
|
76
|
-
const tableRefs = this.findTableReferences(ast, adapter, sourceCode);
|
|
83
|
+
// R2.1 — AST-based table reference extraction (replaces legacy regex scan-all-strings)
|
|
84
|
+
const tableRefs = this.findTableReferences(ast, adapter, sourceCode, finalConfig);
|
|
77
85
|
const columnRefs = this.findColumnReferences(ast, adapter, sourceCode);
|
|
78
|
-
// Check for missing table references
|
|
86
|
+
// Check for missing table references — R2.4: Levenshtein suggestions
|
|
79
87
|
if (finalConfig.checkMissingReferences) {
|
|
80
88
|
for (const ref of tableRefs) {
|
|
81
89
|
if (!allTables.has(ref.table) && !this.isSystemTable(ref.table)) {
|
|
82
|
-
|
|
90
|
+
const suggestions = this.getNearestTableSuggestions(ref.table, allTables, 2);
|
|
91
|
+
const msg = suggestions.length > 0
|
|
92
|
+
? `Reference to unknown table '${ref.table}' (${ref.type}). Did you mean: ${suggestions.join(', ')}?`
|
|
93
|
+
: `Reference to unknown table '${ref.table}' (${ref.type})`;
|
|
94
|
+
violations.push(this.createViolation(ast.filePath, ref.location, msg, 'suggestion', // R7
|
|
95
|
+
'unknown-table'));
|
|
83
96
|
}
|
|
84
97
|
}
|
|
85
|
-
// Check column references
|
|
86
98
|
for (const ref of columnRefs) {
|
|
87
99
|
const columnSet = tableColumns.get(ref.table);
|
|
88
100
|
if (columnSet && !columnSet.has(ref.column)) {
|
|
@@ -98,61 +110,233 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
98
110
|
if (finalConfig.validateQueryPatterns) {
|
|
99
111
|
violations.push(...this.checkQueryPatterns(ast, adapter, sourceCode, finalConfig));
|
|
100
112
|
}
|
|
101
|
-
// Check for SQL injection patterns
|
|
113
|
+
// Check for SQL injection patterns — R7: no critical by default
|
|
102
114
|
violations.push(...this.checkSQLInjection(ast, adapter, sourceCode));
|
|
103
115
|
return violations;
|
|
104
116
|
}
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// R2.2 — File gate
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
105
120
|
/**
|
|
106
|
-
*
|
|
121
|
+
* Pre-filter: only analyze files that show DB usage.
|
|
122
|
+
* Checks: .sql/migration glob, D1/SQL imports, env-binding patterns, DB calls.
|
|
107
123
|
*/
|
|
108
|
-
|
|
124
|
+
passesFileGate(filePath, sourceCode, config) {
|
|
125
|
+
// Always pass .sql files and migration directories
|
|
126
|
+
const gateGlobs = config.fileGateGlobs ?? ['**/*.sql', '**/migrations/**'];
|
|
127
|
+
for (const glob of gateGlobs) {
|
|
128
|
+
if (picomatch.isMatch(filePath, glob)) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Check for D1 or SQL API imports
|
|
133
|
+
const importPatterns = [
|
|
134
|
+
/import\s+.*\b(D1Database|D1PreparedStatement|D1Result)\b/,
|
|
135
|
+
/import\s+.*from\s+['"].*d1['"]/,
|
|
136
|
+
/import\s+.*from\s+['"].*pg['"]/,
|
|
137
|
+
/import\s+.*from\s+['"].*mysql['"]/,
|
|
138
|
+
/import\s+.*from\s+['"].*sqlite['"]/,
|
|
139
|
+
/import\s+.*from\s+['"].*knex['"]/,
|
|
140
|
+
/import\s+.*from\s+['"].*drizzle['"]/,
|
|
141
|
+
/import\s+.*from\s+['"].*prisma['"]/,
|
|
142
|
+
];
|
|
143
|
+
for (const pat of importPatterns) {
|
|
144
|
+
if (pat.test(sourceCode))
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
// Check for env-binding patterns (e.g., env.DB in Cloudflare Workers)
|
|
148
|
+
const bindingNames = config.dbBindingNames ?? ['env.DB'];
|
|
149
|
+
for (const binding of bindingNames) {
|
|
150
|
+
if (sourceCode.includes(binding))
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
// Check for DB call patterns (receiver.method)
|
|
154
|
+
const receivers = config.dbReceiverNames ?? ['db', 'database', 'sql', 'stmt'];
|
|
155
|
+
const methods = config.dbCallMethods ?? ['exec', 'prepare', 'batch', 'run', 'all', 'first'];
|
|
156
|
+
for (const receiver of receivers) {
|
|
157
|
+
for (const method of methods) {
|
|
158
|
+
const pattern = new RegExp(`\\b${escapeRegex(receiver)}\\.${escapeRegex(method)}\\s*\\(`);
|
|
159
|
+
if (pattern.test(sourceCode))
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Check for SQL tagged template literals
|
|
164
|
+
const sqlTags = config.sqlTagNames ?? ['sql', 'db'];
|
|
165
|
+
for (const tag of sqlTags) {
|
|
166
|
+
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');
|
|
167
|
+
if (pattern.test(sourceCode))
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// R2.1 — AST-based table reference extraction (replaces legacy regex)
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
/**
|
|
176
|
+
* Extract table references exclusively from SQL contexts in the AST.
|
|
177
|
+
* R2.1: Only tagged template SQL and DB-call patterns produce candidates.
|
|
178
|
+
* R2.3: Template expressions (${var}) resolved to wildcards.
|
|
179
|
+
*/
|
|
180
|
+
findTableReferences(ast, adapter, sourceCode, config) {
|
|
109
181
|
const references = [];
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
182
|
+
const sqlTags = config.sqlTagNames ?? ['sql', 'db'];
|
|
183
|
+
const dbMethods = config.dbCallMethods ?? ['exec', 'prepare', 'batch', 'run', 'all', 'first', 'query'];
|
|
184
|
+
const dbReceivers = config.dbReceiverNames ?? ['db', 'database', 'sql', 'stmt'];
|
|
185
|
+
// (1) Tagged template SQL — e.g. sql`SELECT * FROM heroes`
|
|
186
|
+
const taggedTemplates = adapter.findNodes(ast, {
|
|
187
|
+
custom: (node) => {
|
|
188
|
+
if (node.type !== 'call_expression')
|
|
189
|
+
return false;
|
|
190
|
+
// Callee must be an identifier matching sqlTagNames
|
|
191
|
+
const callee = this.getCallee(node, adapter, sourceCode);
|
|
192
|
+
if (!callee || !sqlTags.includes(callee))
|
|
193
|
+
return false;
|
|
194
|
+
// Must have a template string argument
|
|
195
|
+
return this.hasTemplateArgument(node, adapter);
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
for (const callNode of taggedTemplates) {
|
|
199
|
+
const templateText = this.getTemplateText(callNode, adapter, sourceCode);
|
|
200
|
+
if (!templateText)
|
|
201
|
+
continue;
|
|
202
|
+
const location = this.getCallLocation(callNode);
|
|
203
|
+
const tableRefs = this.parseSqlTables(templateText, location, sourceCode);
|
|
204
|
+
references.push(...tableRefs);
|
|
205
|
+
}
|
|
206
|
+
// (2) DB-call patterns — e.g. db.exec("SELECT * FROM heroes")
|
|
207
|
+
const dbCalls = adapter.findNodes(ast, {
|
|
208
|
+
custom: (node) => {
|
|
209
|
+
if (node.type !== 'call_expression')
|
|
210
|
+
return false;
|
|
211
|
+
const callee = this.getCallee(node, adapter, sourceCode);
|
|
212
|
+
if (!callee)
|
|
213
|
+
return false;
|
|
214
|
+
// Check if callee is a member expression like db.exec or db.prepare
|
|
215
|
+
return this.isDbMemberCall(node, callee, dbMethods, dbReceivers, adapter, sourceCode);
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
for (const callNode of dbCalls) {
|
|
219
|
+
const firstArg = this.getFirstStringArgument(callNode, adapter, sourceCode);
|
|
220
|
+
if (!firstArg)
|
|
221
|
+
continue;
|
|
222
|
+
const location = this.getCallLocation(callNode);
|
|
223
|
+
const tableRefs = this.parseSqlTables(firstArg, location, sourceCode);
|
|
224
|
+
references.push(...tableRefs);
|
|
225
|
+
}
|
|
226
|
+
// (3) .sql files and migration directories — scan the entire source
|
|
227
|
+
if (ast.filePath.endsWith('.sql') ||
|
|
228
|
+
ast.filePath.includes('/migrations/') ||
|
|
229
|
+
ast.filePath.includes('\\migrations\\')) {
|
|
230
|
+
const fileRefs = this.parseSqlTables(sourceCode, { line: 1, column: 1 }, sourceCode);
|
|
231
|
+
references.push(...fileRefs);
|
|
232
|
+
}
|
|
233
|
+
return references;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Parse SQL table names from a SQL text string.
|
|
237
|
+
* R2.3: Template expressions (${...}) resolve portions to wildcards.
|
|
238
|
+
*/
|
|
239
|
+
parseSqlTables(sqlText, baseLocation, sourceCode) {
|
|
240
|
+
const references = [];
|
|
241
|
+
// R2.3: Strip template expressions — `${prefix}_builds` → `_builds`
|
|
242
|
+
// (the prefix is replaced with empty, the suffix remains for matching)
|
|
243
|
+
const cleaned = this.resolveTemplateExpressions(sqlText);
|
|
244
|
+
// SQL patterns anchored to SQL keywords (not arbitrary substrings)
|
|
245
|
+
const sqlPatterns = [
|
|
246
|
+
{ regex: /\bFROM\s+([`"']?)([a-zA-Z_]\w*)\1\b/gi, type: 'select' },
|
|
247
|
+
{ regex: /\bJOIN\s+([`"']?)([a-zA-Z_]\w*)\1\b/gi, type: 'select' },
|
|
248
|
+
{ regex: /\bINSERT\s+INTO\s+([`"']?)([a-zA-Z_]\w*)\1\b/gi, type: 'insert' },
|
|
249
|
+
{ regex: /\bUPDATE\s+([`"']?)([a-zA-Z_]\w*)\1\b/gi, type: 'update' },
|
|
250
|
+
{ regex: /\bDELETE\s+FROM\s+([`"']?)([a-zA-Z_]\w*)\1\b/gi, type: 'delete' },
|
|
251
|
+
{ regex: /\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([`"']?)([a-zA-Z_]\w*)\1\b/gi, type: 'create' },
|
|
127
252
|
];
|
|
128
|
-
for (const
|
|
253
|
+
for (const { regex, type } of sqlPatterns) {
|
|
129
254
|
let match;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
255
|
+
// Create fresh regex since we might consume with exec
|
|
256
|
+
const re = new RegExp(regex.source, regex.flags);
|
|
257
|
+
while ((match = re.exec(cleaned)) !== null) {
|
|
258
|
+
const table = match[2]; // The table name (capture group 2)
|
|
259
|
+
if (!table || this.isSystemTable(table))
|
|
260
|
+
continue;
|
|
261
|
+
// Skip common false positives: common variable names, keywords
|
|
262
|
+
if (this.isSqlKeyword(table))
|
|
263
|
+
continue;
|
|
264
|
+
// Calculate position in original source
|
|
265
|
+
const offset = sqlText.indexOf(match[0]);
|
|
266
|
+
const location = offset >= 0
|
|
267
|
+
? this.offsetToLocation(sourceCode, sourceCode.indexOf(cleaned) + offset, baseLocation)
|
|
268
|
+
: baseLocation;
|
|
136
269
|
references.push({
|
|
137
270
|
table,
|
|
138
|
-
type
|
|
139
|
-
location
|
|
140
|
-
context: match[0]
|
|
271
|
+
type,
|
|
272
|
+
location,
|
|
273
|
+
context: match[0].trim(),
|
|
141
274
|
});
|
|
142
275
|
}
|
|
143
276
|
}
|
|
144
277
|
return references;
|
|
145
278
|
}
|
|
146
279
|
/**
|
|
147
|
-
*
|
|
280
|
+
* R2.3: Resolve template expressions in SQL text.
|
|
281
|
+
* `${prefix}_builds` → `_builds` (dynamic prefix removed, suffix kept).
|
|
282
|
+
* `${identifier}` (fully dynamic) → empty string (no finding produced).
|
|
148
283
|
*/
|
|
284
|
+
resolveTemplateExpressions(text) {
|
|
285
|
+
// Replace dynamic segments with wildcard-aware placeholders
|
|
286
|
+
// ${var} alone → consume the entire segment (no table name to extract)
|
|
287
|
+
// ${p}_suffix → keep "_suffix" for matching
|
|
288
|
+
return text.replace(/\$\{[^}]+\}/g, '');
|
|
289
|
+
}
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// R2.4 — Levenshtein suggestions
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
/**
|
|
294
|
+
* Return known table names within edit distance ≤ maxDist.
|
|
295
|
+
*/
|
|
296
|
+
getNearestTableSuggestions(name, knownTables, maxDist) {
|
|
297
|
+
const results = [];
|
|
298
|
+
for (const known of knownTables) {
|
|
299
|
+
const dist = this.levenshteinDistance(name.toLowerCase(), known.toLowerCase());
|
|
300
|
+
if (dist <= maxDist) {
|
|
301
|
+
results.push({ table: known, distance: dist });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Sort by distance ascending
|
|
305
|
+
results.sort((a, b) => a.distance - b.distance);
|
|
306
|
+
return results.slice(0, 3).map(r => `'${r.table}'`);
|
|
307
|
+
}
|
|
308
|
+
levenshteinDistance(a, b) {
|
|
309
|
+
const m = a.length;
|
|
310
|
+
const n = b.length;
|
|
311
|
+
// Optimize: early exit if length difference exceeds threshold
|
|
312
|
+
if (Math.abs(m - n) > 3)
|
|
313
|
+
return Infinity;
|
|
314
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
315
|
+
for (let i = 0; i <= m; i++)
|
|
316
|
+
dp[i][0] = i;
|
|
317
|
+
for (let j = 0; j <= n; j++)
|
|
318
|
+
dp[0][j] = j;
|
|
319
|
+
for (let i = 1; i <= m; i++) {
|
|
320
|
+
for (let j = 1; j <= n; j++) {
|
|
321
|
+
if (a[i - 1] === b[j - 1]) {
|
|
322
|
+
dp[i][j] = dp[i - 1][j - 1];
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return dp[m][n];
|
|
330
|
+
}
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// Column references (unchanged for now)
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
149
334
|
findColumnReferences(ast, adapter, sourceCode) {
|
|
150
335
|
const references = [];
|
|
151
|
-
// Patterns for column references
|
|
152
336
|
const patterns = [
|
|
153
|
-
/(\w+)\.(\w+)\s*[=<>]/gi,
|
|
154
|
-
/SELECT\s+.*?(\w+)\.(\w+)/gi,
|
|
155
|
-
/WHERE\s+.*?(\w+)\.(\w+)/gi,
|
|
337
|
+
/(\w+)\.(\w+)\s*[=<>]/gi,
|
|
338
|
+
/SELECT\s+.*?(\w+)\.(\w+)/gi,
|
|
339
|
+
/WHERE\s+.*?(\w+)\.(\w+)/gi,
|
|
156
340
|
];
|
|
157
341
|
for (const pattern of patterns) {
|
|
158
342
|
let match;
|
|
@@ -172,17 +356,15 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
172
356
|
}
|
|
173
357
|
return references;
|
|
174
358
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
// Naming conventions
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
178
362
|
checkNamingConventions(references, filePath) {
|
|
179
363
|
const violations = [];
|
|
180
364
|
for (const ref of references) {
|
|
181
|
-
// Check for camelCase tables (should be snake_case)
|
|
182
365
|
if (/[A-Z]/.test(ref.table) && !ref.table.endsWith('Table')) {
|
|
183
366
|
violations.push(this.createViolation(filePath, ref.location, `Table name '${ref.table}' should use snake_case convention`, 'suggestion', 'naming-convention'));
|
|
184
367
|
}
|
|
185
|
-
// Check for reserved words
|
|
186
368
|
const reserved = ['user', 'order', 'group', 'table', 'column', 'index'];
|
|
187
369
|
if (reserved.includes(ref.table.toLowerCase())) {
|
|
188
370
|
violations.push(this.createViolation(filePath, ref.location, `Table name '${ref.table}' is a reserved word. Consider using a different name.`, 'warning', 'reserved-word'));
|
|
@@ -190,12 +372,11 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
190
372
|
}
|
|
191
373
|
return violations;
|
|
192
374
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
// Query patterns
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
196
378
|
checkQueryPatterns(ast, adapter, sourceCode, config) {
|
|
197
379
|
const violations = [];
|
|
198
|
-
// Count queries per function
|
|
199
380
|
const functions = adapter.extractFunctions(ast);
|
|
200
381
|
for (const func of functions) {
|
|
201
382
|
const funcNode = this.findNodeByLocation(ast.root, func.location.start);
|
|
@@ -207,34 +388,172 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
207
388
|
violations.push(this.createViolation(ast.filePath, func.location.start, `Function '${func.name}' has ${queryCount} queries, exceeding the maximum of ${config.maxQueriesPerFunction}`, 'warning', 'too-many-queries'));
|
|
208
389
|
}
|
|
209
390
|
}
|
|
210
|
-
//
|
|
391
|
+
// N+1 query check — only flag if we have actual DB calls, not grep
|
|
211
392
|
if (sourceCode.includes('.map') && (sourceCode.includes('.query') || sourceCode.includes('SELECT'))) {
|
|
212
393
|
violations.push(this.createViolation(ast.filePath, { line: 1, column: 1 }, 'Potential N+1 query pattern detected. Consider using a join or batch query.', 'warning', 'n-plus-one'));
|
|
213
394
|
}
|
|
214
395
|
return violations;
|
|
215
396
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
// SQL injection — R7: severity changed from critical to warning
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
219
400
|
checkSQLInjection(ast, adapter, sourceCode) {
|
|
220
401
|
const violations = [];
|
|
221
|
-
// Patterns that indicate potential SQL injection
|
|
222
402
|
const dangerousPatterns = [
|
|
223
|
-
/query\s*\(\s*`[^`]*\$\{[^}]+\}[^`]*`/g,
|
|
224
|
-
/query\s*\(\s*['"][^'"]*['"]?\s*\+/g,
|
|
225
|
-
/execute\s*\(\s*['"][^'"]*['"]?\s*\+/g,
|
|
403
|
+
/query\s*\(\s*`[^`]*\$\{[^}]+\}[^`]*`/g,
|
|
404
|
+
/query\s*\(\s*['"][^'"]*['"]?\s*\+/g,
|
|
405
|
+
/execute\s*\(\s*['"][^'"]*['"]?\s*\+/g,
|
|
226
406
|
];
|
|
227
407
|
for (const pattern of dangerousPatterns) {
|
|
228
408
|
if (pattern.test(sourceCode)) {
|
|
229
|
-
violations.push(this.createViolation(ast.filePath, { line: 1, column: 1 }, 'Potential SQL injection vulnerability. Use parameterized queries.', '
|
|
409
|
+
violations.push(this.createViolation(ast.filePath, { line: 1, column: 1 }, 'Potential SQL injection vulnerability. Use parameterized queries.', 'warning', // R7: was 'critical'
|
|
410
|
+
'sql-injection'));
|
|
230
411
|
break;
|
|
231
412
|
}
|
|
232
413
|
}
|
|
233
414
|
return violations;
|
|
234
415
|
}
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
// AST helpers for R2.1
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
235
419
|
/**
|
|
236
|
-
*
|
|
420
|
+
* Extract the callee text from a call_expression node.
|
|
237
421
|
*/
|
|
422
|
+
getCallee(node, adapter, sourceCode) {
|
|
423
|
+
// For db.exec() → callee is "db.exec"
|
|
424
|
+
if (!node.children)
|
|
425
|
+
return null;
|
|
426
|
+
for (const child of node.children) {
|
|
427
|
+
const type = adapter.getNodeType(child);
|
|
428
|
+
if (type === 'identifier' ||
|
|
429
|
+
type === 'member_expression' ||
|
|
430
|
+
type === 'call_expression') {
|
|
431
|
+
return adapter.getNodeText(child, sourceCode).trim();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Check if call_expression has a template string argument.
|
|
438
|
+
*/
|
|
439
|
+
hasTemplateArgument(node, adapter) {
|
|
440
|
+
if (!node.children)
|
|
441
|
+
return false;
|
|
442
|
+
for (const child of node.children) {
|
|
443
|
+
const type = adapter.getNodeType(child);
|
|
444
|
+
if (type === 'template_string' || type === 'template_literal') {
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Get the text of the first template string argument.
|
|
452
|
+
*/
|
|
453
|
+
getTemplateText(node, adapter, sourceCode) {
|
|
454
|
+
if (!node.children)
|
|
455
|
+
return null;
|
|
456
|
+
for (const child of node.children) {
|
|
457
|
+
const type = adapter.getNodeType(child);
|
|
458
|
+
if (type === 'template_string' || type === 'template_literal') {
|
|
459
|
+
return adapter.getNodeText(child, sourceCode).trim();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Get the first string/template argument from a call expression.
|
|
466
|
+
*/
|
|
467
|
+
getFirstStringArgument(node, adapter, sourceCode) {
|
|
468
|
+
if (!node.children)
|
|
469
|
+
return null;
|
|
470
|
+
// Look for 'arguments' child first
|
|
471
|
+
for (const child of node.children) {
|
|
472
|
+
const type = adapter.getNodeType(child);
|
|
473
|
+
if (type === 'arguments' && child.children) {
|
|
474
|
+
for (const arg of child.children) {
|
|
475
|
+
const argType = adapter.getNodeType(arg);
|
|
476
|
+
if (argType === 'string' ||
|
|
477
|
+
argType === 'template_string' ||
|
|
478
|
+
argType === 'template_literal') {
|
|
479
|
+
const text = adapter.getNodeText(arg, sourceCode).trim();
|
|
480
|
+
// Strip surrounding quotes from string literals
|
|
481
|
+
if ((text.startsWith("'") && text.endsWith("'")) ||
|
|
482
|
+
(text.startsWith('"') && text.endsWith('"')) ||
|
|
483
|
+
(text.startsWith('`') && text.endsWith('`'))) {
|
|
484
|
+
return text.slice(1, -1);
|
|
485
|
+
}
|
|
486
|
+
return text;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Check if a callee is a DB member call like db.exec, database.query, etc.
|
|
495
|
+
*/
|
|
496
|
+
isDbMemberCall(node, calleeText, methods, receivers, adapter, sourceCode) {
|
|
497
|
+
// calleeText might be like "db.exec"
|
|
498
|
+
const dotIdx = calleeText.indexOf('.');
|
|
499
|
+
if (dotIdx === -1)
|
|
500
|
+
return false;
|
|
501
|
+
const receiver = calleeText.substring(0, dotIdx);
|
|
502
|
+
const method = calleeText.substring(dotIdx + 1);
|
|
503
|
+
return receivers.includes(receiver) && methods.includes(method);
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Get the line/column location of the call expression.
|
|
507
|
+
*/
|
|
508
|
+
getCallLocation(node) {
|
|
509
|
+
return node.location.start;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Convert a character offset to a line/column location.
|
|
513
|
+
*/
|
|
514
|
+
offsetToLocation(sourceCode, offset, base) {
|
|
515
|
+
if (offset < 0 || offset >= sourceCode.length)
|
|
516
|
+
return base;
|
|
517
|
+
const before = sourceCode.substring(0, offset);
|
|
518
|
+
const lineOffset = before.split('\n').length - 1;
|
|
519
|
+
const lastNewline = before.lastIndexOf('\n');
|
|
520
|
+
const column = lastNewline >= 0 ? offset - lastNewline : offset + 1;
|
|
521
|
+
return { line: base.line + lineOffset, column };
|
|
522
|
+
}
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
// General helpers
|
|
525
|
+
// ---------------------------------------------------------------------------
|
|
526
|
+
isSystemTable(table) {
|
|
527
|
+
const systemTables = [
|
|
528
|
+
'information_schema',
|
|
529
|
+
'pg_catalog',
|
|
530
|
+
'mysql',
|
|
531
|
+
'performance_schema',
|
|
532
|
+
'sys',
|
|
533
|
+
'sqlite_master',
|
|
534
|
+
'sqlite_sequence',
|
|
535
|
+
];
|
|
536
|
+
return systemTables.some(st => table.toLowerCase() === st || table.toLowerCase().startsWith(st + '.'));
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Common SQL keywords and identifiers that are not real table names.
|
|
540
|
+
*/
|
|
541
|
+
isSqlKeyword(word) {
|
|
542
|
+
const keywords = new Set([
|
|
543
|
+
'select', 'from', 'where', 'join', 'inner', 'outer', 'left', 'right',
|
|
544
|
+
'full', 'cross', 'on', 'and', 'or', 'not', 'in', 'as', 'is', 'null',
|
|
545
|
+
'like', 'between', 'order', 'group', 'by', 'having', 'limit', 'offset',
|
|
546
|
+
'union', 'all', 'distinct', 'case', 'when', 'then', 'else', 'end',
|
|
547
|
+
'insert', 'into', 'values', 'update', 'set', 'delete', 'create',
|
|
548
|
+
'table', 'alter', 'drop', 'index', 'view', 'if', 'exists', 'primary',
|
|
549
|
+
'key', 'foreign', 'references', 'constraint', 'default', 'unique',
|
|
550
|
+
'check', 'asc', 'desc', 'count', 'sum', 'avg', 'min', 'max',
|
|
551
|
+
'integer', 'text', 'varchar', 'text', 'boolean', 'float', 'blob',
|
|
552
|
+
'real', 'timestamp', 'date', 'time', 'datetime', 'serial', 'bigint',
|
|
553
|
+
'the', 'a', 'an',
|
|
554
|
+
]);
|
|
555
|
+
return keywords.has(word.toLowerCase());
|
|
556
|
+
}
|
|
238
557
|
getQueryType(context) {
|
|
239
558
|
const upper = context.toUpperCase();
|
|
240
559
|
if (upper.includes('SELECT'))
|
|
@@ -249,16 +568,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
249
568
|
return 'create';
|
|
250
569
|
return 'reference';
|
|
251
570
|
}
|
|
252
|
-
isSystemTable(table) {
|
|
253
|
-
const systemTables = [
|
|
254
|
-
'information_schema',
|
|
255
|
-
'pg_catalog',
|
|
256
|
-
'mysql',
|
|
257
|
-
'performance_schema',
|
|
258
|
-
'sys'
|
|
259
|
-
];
|
|
260
|
-
return systemTables.some(st => table.toLowerCase().startsWith(st));
|
|
261
|
-
}
|
|
262
571
|
countQueries(text) {
|
|
263
572
|
const patterns = [
|
|
264
573
|
/\.query\s*\(/g,
|
|
@@ -266,7 +575,7 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
266
575
|
/SELECT\s+/gi,
|
|
267
576
|
/INSERT\s+INTO/gi,
|
|
268
577
|
/UPDATE\s+/gi,
|
|
269
|
-
/DELETE\s+FROM/gi
|
|
578
|
+
/DELETE\s+FROM/gi,
|
|
270
579
|
];
|
|
271
580
|
let count = 0;
|
|
272
581
|
for (const pattern of patterns) {
|
|
@@ -290,9 +599,9 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
290
599
|
}
|
|
291
600
|
return null;
|
|
292
601
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
603
|
+
// JSON Schema validation (unchanged from original)
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
296
605
|
async analyzeJsonSchemas(files, config) {
|
|
297
606
|
const violations = [];
|
|
298
607
|
const errors = [];
|
|
@@ -302,18 +611,15 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
302
611
|
if (!finalConfig.validateJsonSchemas) {
|
|
303
612
|
return { violations, errors, filesProcessed, executionTime: 0 };
|
|
304
613
|
}
|
|
305
|
-
// Categorize files
|
|
306
614
|
const schemaFiles = this.identifySchemaFiles(files, finalConfig);
|
|
307
615
|
const dataFiles = this.identifyDataFiles(files, finalConfig);
|
|
308
616
|
const unknownJsonFiles = files.filter(f => !schemaFiles.includes(f) && !dataFiles.includes(f));
|
|
309
|
-
// Load and validate schema files
|
|
310
617
|
const schemas = new Map();
|
|
311
618
|
for (const file of schemaFiles) {
|
|
312
619
|
try {
|
|
313
620
|
const content = await fs.readFile(file, 'utf8');
|
|
314
621
|
const schema = JSON.parse(content);
|
|
315
622
|
schemas.set(file, schema);
|
|
316
|
-
// Validate the JSON Schema itself
|
|
317
623
|
const fileViolations = this.validateJsonSchema(schema, file, finalConfig);
|
|
318
624
|
violations.push(...fileViolations);
|
|
319
625
|
filesProcessed++;
|
|
@@ -324,7 +630,7 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
324
630
|
file,
|
|
325
631
|
line: 1,
|
|
326
632
|
column: 1,
|
|
327
|
-
severity: '
|
|
633
|
+
severity: 'warning',
|
|
328
634
|
message: `Invalid JSON: ${error.message}`,
|
|
329
635
|
rule: 'invalid-json',
|
|
330
636
|
analyzer: 'schema'
|
|
@@ -339,9 +645,7 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
339
645
|
filesProcessed++;
|
|
340
646
|
}
|
|
341
647
|
}
|
|
342
|
-
// Validate data files against schemas
|
|
343
648
|
if (finalConfig.schemaDataPairs) {
|
|
344
|
-
// Use explicit schema-data pairs
|
|
345
649
|
for (const pair of finalConfig.schemaDataPairs) {
|
|
346
650
|
const schema = schemas.get(pair.schema) || await this.loadSchema(pair.schema);
|
|
347
651
|
if (schema) {
|
|
@@ -357,7 +661,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
357
661
|
}
|
|
358
662
|
}
|
|
359
663
|
else {
|
|
360
|
-
// Try to match data files with schemas based on naming conventions
|
|
361
664
|
for (const dataFile of [...dataFiles, ...unknownJsonFiles]) {
|
|
362
665
|
const matchedSchema = this.findMatchingSchema(dataFile, schemas, finalConfig);
|
|
363
666
|
if (matchedSchema) {
|
|
@@ -365,7 +668,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
365
668
|
violations.push(...dataViolations);
|
|
366
669
|
}
|
|
367
670
|
else if (unknownJsonFiles.includes(dataFile)) {
|
|
368
|
-
// For unknown files, just check if they're valid JSON
|
|
369
671
|
try {
|
|
370
672
|
const content = await fs.readFile(dataFile, 'utf8');
|
|
371
673
|
JSON.parse(content);
|
|
@@ -375,7 +677,7 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
375
677
|
file: dataFile,
|
|
376
678
|
line: 1,
|
|
377
679
|
column: 1,
|
|
378
|
-
severity: '
|
|
680
|
+
severity: 'warning',
|
|
379
681
|
message: `Invalid JSON: ${error instanceof Error ? error.message : 'Parse error'}`,
|
|
380
682
|
rule: 'invalid-json',
|
|
381
683
|
analyzer: 'schema'
|
|
@@ -392,12 +694,8 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
392
694
|
executionTime: Date.now() - startTime
|
|
393
695
|
};
|
|
394
696
|
}
|
|
395
|
-
/**
|
|
396
|
-
* Validate a JSON Schema
|
|
397
|
-
*/
|
|
398
697
|
validateJsonSchema(schema, filePath, config) {
|
|
399
698
|
const violations = [];
|
|
400
|
-
// Check for $schema property
|
|
401
699
|
if (!schema.$schema && config.jsonSchemaVersion) {
|
|
402
700
|
violations.push({
|
|
403
701
|
file: filePath,
|
|
@@ -409,11 +707,8 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
409
707
|
analyzer: 'schema'
|
|
410
708
|
});
|
|
411
709
|
}
|
|
412
|
-
// Validate types recursively
|
|
413
710
|
this.validateSchemaTypes(schema, filePath, config, violations);
|
|
414
|
-
// Check for common issues
|
|
415
711
|
if (schema.type === 'object' && schema.properties) {
|
|
416
|
-
// Check for inconsistent required fields
|
|
417
712
|
if (schema.required && Array.isArray(schema.required)) {
|
|
418
713
|
for (const field of schema.required) {
|
|
419
714
|
if (!schema.properties[field]) {
|
|
@@ -421,7 +716,7 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
421
716
|
file: filePath,
|
|
422
717
|
line: 1,
|
|
423
718
|
column: 1,
|
|
424
|
-
severity: '
|
|
719
|
+
severity: 'warning',
|
|
425
720
|
message: `Required field "${field}" not defined in properties`,
|
|
426
721
|
rule: 'undefined-required-field',
|
|
427
722
|
analyzer: 'schema'
|
|
@@ -432,13 +727,9 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
432
727
|
}
|
|
433
728
|
return violations;
|
|
434
729
|
}
|
|
435
|
-
/**
|
|
436
|
-
* Recursively validate schema types
|
|
437
|
-
*/
|
|
438
730
|
validateSchemaTypes(schema, filePath, config, violations, path = '') {
|
|
439
731
|
if (!schema || typeof schema !== 'object')
|
|
440
732
|
return;
|
|
441
|
-
// Check type validity
|
|
442
733
|
if (schema.type && config.allowedJsonTypes) {
|
|
443
734
|
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
444
735
|
for (const type of types) {
|
|
@@ -447,7 +738,7 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
447
738
|
file: filePath,
|
|
448
739
|
line: 1,
|
|
449
740
|
column: 1,
|
|
450
|
-
severity: '
|
|
741
|
+
severity: 'warning',
|
|
451
742
|
message: `Invalid type "${type}" at ${path || 'root'}. Allowed types: ${config.allowedJsonTypes.join(', ')}`,
|
|
452
743
|
rule: 'invalid-type',
|
|
453
744
|
analyzer: 'schema'
|
|
@@ -455,7 +746,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
455
746
|
}
|
|
456
747
|
}
|
|
457
748
|
}
|
|
458
|
-
// Check numeric constraints
|
|
459
749
|
if (schema.type === 'integer' || schema.type === 'number') {
|
|
460
750
|
if (schema.minimum !== undefined && schema.maximum !== undefined) {
|
|
461
751
|
if (schema.minimum > schema.maximum) {
|
|
@@ -463,7 +753,7 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
463
753
|
file: filePath,
|
|
464
754
|
line: 1,
|
|
465
755
|
column: 1,
|
|
466
|
-
severity: '
|
|
756
|
+
severity: 'warning',
|
|
467
757
|
message: `Invalid range at ${path}: minimum (${schema.minimum}) > maximum (${schema.maximum})`,
|
|
468
758
|
rule: 'invalid-range',
|
|
469
759
|
analyzer: 'schema'
|
|
@@ -471,24 +761,18 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
471
761
|
}
|
|
472
762
|
}
|
|
473
763
|
}
|
|
474
|
-
// Recursively check properties
|
|
475
764
|
if (schema.properties) {
|
|
476
765
|
for (const [key, value] of Object.entries(schema.properties)) {
|
|
477
766
|
this.validateSchemaTypes(value, filePath, config, violations, `${path}.${key}`);
|
|
478
767
|
}
|
|
479
768
|
}
|
|
480
|
-
// Check array items
|
|
481
769
|
if (schema.items) {
|
|
482
770
|
this.validateSchemaTypes(schema.items, filePath, config, violations, `${path}[items]`);
|
|
483
771
|
}
|
|
484
|
-
// Check additional properties
|
|
485
772
|
if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
|
486
773
|
this.validateSchemaTypes(schema.additionalProperties, filePath, config, violations, `${path}[additionalProperties]`);
|
|
487
774
|
}
|
|
488
775
|
}
|
|
489
|
-
/**
|
|
490
|
-
* Identify schema files based on patterns
|
|
491
|
-
*/
|
|
492
776
|
identifySchemaFiles(files, config) {
|
|
493
777
|
if (!config.schemaFilePatterns)
|
|
494
778
|
return [];
|
|
@@ -500,9 +784,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
500
784
|
});
|
|
501
785
|
});
|
|
502
786
|
}
|
|
503
|
-
/**
|
|
504
|
-
* Identify data files based on patterns
|
|
505
|
-
*/
|
|
506
787
|
identifyDataFiles(files, config) {
|
|
507
788
|
if (!config.dataFilePatterns)
|
|
508
789
|
return [];
|
|
@@ -514,9 +795,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
514
795
|
});
|
|
515
796
|
});
|
|
516
797
|
}
|
|
517
|
-
/**
|
|
518
|
-
* Load a schema from file
|
|
519
|
-
*/
|
|
520
798
|
async loadSchema(schemaPath) {
|
|
521
799
|
try {
|
|
522
800
|
const content = await fs.readFile(schemaPath, 'utf8');
|
|
@@ -526,13 +804,9 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
526
804
|
return null;
|
|
527
805
|
}
|
|
528
806
|
}
|
|
529
|
-
/**
|
|
530
|
-
* Find matching schema for a data file
|
|
531
|
-
*/
|
|
532
807
|
findMatchingSchema(dataFile, schemas, config) {
|
|
533
808
|
const dataFileName = dataFile.split('/').pop() || '';
|
|
534
809
|
const dataBaseName = dataFileName.replace(/\.(data|example|test)\.json$/, '');
|
|
535
|
-
// Look for schema with matching base name
|
|
536
810
|
for (const [schemaFile, schema] of schemas) {
|
|
537
811
|
const schemaFileName = schemaFile.split('/').pop() || '';
|
|
538
812
|
const schemaBaseName = schemaFileName.replace(/[.-]?schema\.json$/, '');
|
|
@@ -540,21 +814,16 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
540
814
|
return schema;
|
|
541
815
|
}
|
|
542
816
|
}
|
|
543
|
-
// If only one schema exists, use it for all data files
|
|
544
817
|
if (schemas.size === 1) {
|
|
545
818
|
return schemas.values().next().value;
|
|
546
819
|
}
|
|
547
820
|
return null;
|
|
548
821
|
}
|
|
549
|
-
/**
|
|
550
|
-
* Validate data against a JSON Schema
|
|
551
|
-
*/
|
|
552
822
|
async validateDataAgainstSchema(dataFile, schema, config) {
|
|
553
823
|
const violations = [];
|
|
554
824
|
try {
|
|
555
825
|
const content = await fs.readFile(dataFile, 'utf8');
|
|
556
826
|
const data = JSON.parse(content);
|
|
557
|
-
// Validate the data against the schema
|
|
558
827
|
this.validateAgainstSchema(data, schema, dataFile, violations, config);
|
|
559
828
|
}
|
|
560
829
|
catch (error) {
|
|
@@ -563,7 +832,7 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
563
832
|
file: dataFile,
|
|
564
833
|
line: 1,
|
|
565
834
|
column: 1,
|
|
566
|
-
severity: '
|
|
835
|
+
severity: 'warning',
|
|
567
836
|
message: `Invalid JSON: ${error.message}`,
|
|
568
837
|
rule: 'invalid-json',
|
|
569
838
|
analyzer: 'schema'
|
|
@@ -574,7 +843,7 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
574
843
|
file: dataFile,
|
|
575
844
|
line: 1,
|
|
576
845
|
column: 1,
|
|
577
|
-
severity: '
|
|
846
|
+
severity: 'warning',
|
|
578
847
|
message: `Error reading file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
579
848
|
rule: 'file-error',
|
|
580
849
|
analyzer: 'schema'
|
|
@@ -583,17 +852,12 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
583
852
|
}
|
|
584
853
|
return violations;
|
|
585
854
|
}
|
|
586
|
-
/**
|
|
587
|
-
* Validate data against schema recursively
|
|
588
|
-
*/
|
|
589
855
|
validateAgainstSchema(data, schema, filePath, violations, config, path = '') {
|
|
590
|
-
// Type validation
|
|
591
856
|
if (schema.type) {
|
|
592
857
|
const actualType = Array.isArray(data) ? 'array' :
|
|
593
858
|
data === null ? 'null' :
|
|
594
859
|
typeof data;
|
|
595
860
|
const allowedTypes = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
596
|
-
// Special handling for integer vs number
|
|
597
861
|
const isValidType = allowedTypes.some((type) => {
|
|
598
862
|
if (type === 'integer') {
|
|
599
863
|
return typeof data === 'number' && Number.isInteger(data);
|
|
@@ -605,15 +869,14 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
605
869
|
file: filePath,
|
|
606
870
|
line: 1,
|
|
607
871
|
column: 1,
|
|
608
|
-
severity: '
|
|
872
|
+
severity: 'warning',
|
|
609
873
|
message: `Type mismatch at ${path || 'root'}: expected ${allowedTypes.join(' | ')}, got ${actualType}`,
|
|
610
874
|
rule: 'type-mismatch',
|
|
611
875
|
analyzer: 'schema'
|
|
612
876
|
});
|
|
613
|
-
return;
|
|
877
|
+
return;
|
|
614
878
|
}
|
|
615
879
|
}
|
|
616
|
-
// String validations
|
|
617
880
|
if (schema.type === 'string' && typeof data === 'string') {
|
|
618
881
|
if (schema.minLength !== undefined && data.length < schema.minLength) {
|
|
619
882
|
violations.push({
|
|
@@ -652,7 +915,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
652
915
|
}
|
|
653
916
|
}
|
|
654
917
|
if (schema.format) {
|
|
655
|
-
// Basic format validation
|
|
656
918
|
switch (schema.format) {
|
|
657
919
|
case 'email':
|
|
658
920
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data)) {
|
|
@@ -680,11 +942,9 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
680
942
|
});
|
|
681
943
|
}
|
|
682
944
|
break;
|
|
683
|
-
// Add more formats as needed
|
|
684
945
|
}
|
|
685
946
|
}
|
|
686
947
|
}
|
|
687
|
-
// Number validations
|
|
688
948
|
if ((schema.type === 'number' || schema.type === 'integer') && typeof data === 'number') {
|
|
689
949
|
if (schema.minimum !== undefined && data < schema.minimum) {
|
|
690
950
|
violations.push({
|
|
@@ -709,7 +969,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
709
969
|
});
|
|
710
970
|
}
|
|
711
971
|
}
|
|
712
|
-
// Array validations
|
|
713
972
|
if (schema.type === 'array' && Array.isArray(data)) {
|
|
714
973
|
if (schema.minItems !== undefined && data.length < schema.minItems) {
|
|
715
974
|
violations.push({
|
|
@@ -739,9 +998,7 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
739
998
|
});
|
|
740
999
|
}
|
|
741
1000
|
}
|
|
742
|
-
// Object validations
|
|
743
1001
|
if (schema.type === 'object' && typeof data === 'object' && data !== null) {
|
|
744
|
-
// Check required fields
|
|
745
1002
|
if (schema.required && Array.isArray(schema.required)) {
|
|
746
1003
|
for (const requiredField of schema.required) {
|
|
747
1004
|
if (!(requiredField in data)) {
|
|
@@ -749,7 +1006,7 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
749
1006
|
file: filePath,
|
|
750
1007
|
line: 1,
|
|
751
1008
|
column: 1,
|
|
752
|
-
severity: '
|
|
1009
|
+
severity: 'warning',
|
|
753
1010
|
message: `Missing required field "${requiredField}" at ${path}`,
|
|
754
1011
|
rule: 'missing-required-field',
|
|
755
1012
|
analyzer: 'schema'
|
|
@@ -757,7 +1014,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
757
1014
|
}
|
|
758
1015
|
}
|
|
759
1016
|
}
|
|
760
|
-
// Validate properties
|
|
761
1017
|
if (schema.properties) {
|
|
762
1018
|
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
763
1019
|
if (key in data) {
|
|
@@ -765,7 +1021,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
765
1021
|
}
|
|
766
1022
|
}
|
|
767
1023
|
}
|
|
768
|
-
// Check additional properties
|
|
769
1024
|
if (schema.additionalProperties === false || (config.strictMode && !schema.additionalProperties)) {
|
|
770
1025
|
const definedKeys = new Set(Object.keys(schema.properties || {}));
|
|
771
1026
|
const actualKeys = Object.keys(data);
|
|
@@ -775,7 +1030,7 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
775
1030
|
file: filePath,
|
|
776
1031
|
line: 1,
|
|
777
1032
|
column: 1,
|
|
778
|
-
severity:
|
|
1033
|
+
severity: 'warning',
|
|
779
1034
|
message: `Unexpected property "${key}" at ${path}`,
|
|
780
1035
|
rule: 'unexpected-property',
|
|
781
1036
|
analyzer: 'schema'
|
|
@@ -784,7 +1039,6 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
784
1039
|
}
|
|
785
1040
|
}
|
|
786
1041
|
else if (typeof schema.additionalProperties === 'object') {
|
|
787
|
-
// Validate additional properties against schema
|
|
788
1042
|
const definedKeys = new Set(Object.keys(schema.properties || {}));
|
|
789
1043
|
for (const [key, value] of Object.entries(data)) {
|
|
790
1044
|
if (!definedKeys.has(key)) {
|
|
@@ -793,14 +1047,13 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
793
1047
|
}
|
|
794
1048
|
}
|
|
795
1049
|
}
|
|
796
|
-
// Enum validation
|
|
797
1050
|
if (schema.enum && Array.isArray(schema.enum)) {
|
|
798
1051
|
if (!schema.enum.includes(data)) {
|
|
799
1052
|
violations.push({
|
|
800
1053
|
file: filePath,
|
|
801
1054
|
line: 1,
|
|
802
1055
|
column: 1,
|
|
803
|
-
severity: '
|
|
1056
|
+
severity: 'warning',
|
|
804
1057
|
message: `Value at ${path} not in enum: ${JSON.stringify(data)}. Allowed: ${schema.enum.join(', ')}`,
|
|
805
1058
|
rule: 'enum-mismatch',
|
|
806
1059
|
analyzer: 'schema'
|
|
@@ -809,4 +1062,10 @@ export class UniversalSchemaAnalyzer extends UniversalAnalyzer {
|
|
|
809
1062
|
}
|
|
810
1063
|
}
|
|
811
1064
|
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Escape regex special characters in a string.
|
|
1067
|
+
*/
|
|
1068
|
+
function escapeRegex(s) {
|
|
1069
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1070
|
+
}
|
|
812
1071
|
//# sourceMappingURL=UniversalSchemaAnalyzer.js.map
|