code-auditor-mcp 3.0.7 → 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.
Files changed (128) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +245 -0
  3. package/README.md +24 -0
  4. package/dist/analyzers/__tests__/fixtures/spec-17/anonymous-callback-map.d.ts +21 -0
  5. package/dist/analyzers/__tests__/fixtures/spec-17/anonymous-callback-map.d.ts.map +1 -0
  6. package/dist/analyzers/__tests__/fixtures/spec-17/anonymous-callback-map.js +21 -0
  7. package/dist/analyzers/__tests__/fixtures/spec-17/anonymous-callback-map.js.map +1 -0
  8. package/dist/analyzers/__tests__/fixtures/spec-17/barrel-test-migration.d.ts +13 -0
  9. package/dist/analyzers/__tests__/fixtures/spec-17/barrel-test-migration.d.ts.map +1 -0
  10. package/dist/analyzers/__tests__/fixtures/spec-17/barrel-test-migration.js +12 -0
  11. package/dist/analyzers/__tests__/fixtures/spec-17/barrel-test-migration.js.map +1 -0
  12. package/dist/analyzers/__tests__/fixtures/spec-17/direct-access-allow-config.d.ts +11 -0
  13. package/dist/analyzers/__tests__/fixtures/spec-17/direct-access-allow-config.d.ts.map +1 -0
  14. package/dist/analyzers/__tests__/fixtures/spec-17/direct-access-allow-config.js +22 -0
  15. package/dist/analyzers/__tests__/fixtures/spec-17/direct-access-allow-config.js.map +1 -0
  16. package/dist/analyzers/__tests__/fixtures/spec-17/exported-undocumented.d.ts +9 -0
  17. package/dist/analyzers/__tests__/fixtures/spec-17/exported-undocumented.d.ts.map +1 -0
  18. package/dist/analyzers/__tests__/fixtures/spec-17/exported-undocumented.js +13 -0
  19. package/dist/analyzers/__tests__/fixtures/spec-17/exported-undocumented.js.map +1 -0
  20. package/dist/analyzers/__tests__/fixtures/spec-17/for-loop-query.d.ts +13 -0
  21. package/dist/analyzers/__tests__/fixtures/spec-17/for-loop-query.d.ts.map +1 -0
  22. package/dist/analyzers/__tests__/fixtures/spec-17/for-loop-query.js +18 -0
  23. package/dist/analyzers/__tests__/fixtures/spec-17/for-loop-query.js.map +1 -0
  24. package/dist/analyzers/__tests__/fixtures/spec-17/import-node-builtin.d.ts +10 -0
  25. package/dist/analyzers/__tests__/fixtures/spec-17/import-node-builtin.d.ts.map +1 -0
  26. package/dist/analyzers/__tests__/fixtures/spec-17/import-node-builtin.js +16 -0
  27. package/dist/analyzers/__tests__/fixtures/spec-17/import-node-builtin.js.map +1 -0
  28. package/dist/analyzers/__tests__/fixtures/spec-17/jsx-event-handler.d.ts +14 -0
  29. package/dist/analyzers/__tests__/fixtures/spec-17/jsx-event-handler.d.ts.map +1 -0
  30. package/dist/analyzers/__tests__/fixtures/spec-17/jsx-event-handler.js +18 -0
  31. package/dist/analyzers/__tests__/fixtures/spec-17/jsx-event-handler.js.map +1 -0
  32. package/dist/analyzers/__tests__/fixtures/spec-17/nested-loops-query.d.ts +13 -0
  33. package/dist/analyzers/__tests__/fixtures/spec-17/nested-loops-query.d.ts.map +1 -0
  34. package/dist/analyzers/__tests__/fixtures/spec-17/nested-loops-query.js +22 -0
  35. package/dist/analyzers/__tests__/fixtures/spec-17/nested-loops-query.js.map +1 -0
  36. package/dist/analyzers/__tests__/fixtures/spec-17/nine-line-repeated.d.ts +10 -0
  37. package/dist/analyzers/__tests__/fixtures/spec-17/nine-line-repeated.d.ts.map +1 -0
  38. package/dist/analyzers/__tests__/fixtures/spec-17/nine-line-repeated.js +20 -0
  39. package/dist/analyzers/__tests__/fixtures/spec-17/nine-line-repeated.js.map +1 -0
  40. package/dist/analyzers/__tests__/fixtures/spec-17/private-methods.d.ts +18 -0
  41. package/dist/analyzers/__tests__/fixtures/spec-17/private-methods.d.ts.map +1 -0
  42. package/dist/analyzers/__tests__/fixtures/spec-17/private-methods.js +28 -0
  43. package/dist/analyzers/__tests__/fixtures/spec-17/private-methods.js.map +1 -0
  44. package/dist/analyzers/__tests__/fixtures/spec-17/scope-all-config.d.ts +11 -0
  45. package/dist/analyzers/__tests__/fixtures/spec-17/scope-all-config.d.ts.map +1 -0
  46. package/dist/analyzers/__tests__/fixtures/spec-17/scope-all-config.js +31 -0
  47. package/dist/analyzers/__tests__/fixtures/spec-17/scope-all-config.js.map +1 -0
  48. package/dist/analyzers/__tests__/fixtures/spec-17/sql-tagged-template.d.ts +11 -0
  49. package/dist/analyzers/__tests__/fixtures/spec-17/sql-tagged-template.d.ts.map +1 -0
  50. package/dist/analyzers/__tests__/fixtures/spec-17/sql-tagged-template.js +18 -0
  51. package/dist/analyzers/__tests__/fixtures/spec-17/sql-tagged-template.js.map +1 -0
  52. package/dist/analyzers/__tests__/fixtures/spec-17/structural-similar-methods.d.ts +13 -0
  53. package/dist/analyzers/__tests__/fixtures/spec-17/structural-similar-methods.d.ts.map +1 -0
  54. package/dist/analyzers/__tests__/fixtures/spec-17/structural-similar-methods.js +61 -0
  55. package/dist/analyzers/__tests__/fixtures/spec-17/structural-similar-methods.js.map +1 -0
  56. package/dist/analyzers/__tests__/fixtures/spec-17/template-prefix-table.d.ts +10 -0
  57. package/dist/analyzers/__tests__/fixtures/spec-17/template-prefix-table.d.ts.map +1 -0
  58. package/dist/analyzers/__tests__/fixtures/spec-17/template-prefix-table.js +15 -0
  59. package/dist/analyzers/__tests__/fixtures/spec-17/template-prefix-table.js.map +1 -0
  60. package/dist/analyzers/__tests__/fixtures/spec-17/token-identical-20-lines.d.ts +11 -0
  61. package/dist/analyzers/__tests__/fixtures/spec-17/token-identical-20-lines.d.ts.map +1 -0
  62. package/dist/analyzers/__tests__/fixtures/spec-17/token-identical-20-lines.js +49 -0
  63. package/dist/analyzers/__tests__/fixtures/spec-17/token-identical-20-lines.js.map +1 -0
  64. package/dist/analyzers/__tests__/fixtures/spec-17/tsx-no-db-usage.d.ts +10 -0
  65. package/dist/analyzers/__tests__/fixtures/spec-17/tsx-no-db-usage.d.ts.map +1 -0
  66. package/dist/analyzers/__tests__/fixtures/spec-17/tsx-no-db-usage.js +19 -0
  67. package/dist/analyzers/__tests__/fixtures/spec-17/tsx-no-db-usage.js.map +1 -0
  68. package/dist/analyzers/__tests__/fixtures/spec-17/twenty-methods-vs-complex.d.ts +37 -0
  69. package/dist/analyzers/__tests__/fixtures/spec-17/twenty-methods-vs-complex.d.ts.map +1 -0
  70. package/dist/analyzers/__tests__/fixtures/spec-17/twenty-methods-vs-complex.js +73 -0
  71. package/dist/analyzers/__tests__/fixtures/spec-17/twenty-methods-vs-complex.js.map +1 -0
  72. package/dist/analyzers/__tests__/fixtures/spec-17/unique-method-74-lines.d.ts +12 -0
  73. package/dist/analyzers/__tests__/fixtures/spec-17/unique-method-74-lines.d.ts.map +1 -0
  74. package/dist/analyzers/__tests__/fixtures/spec-17/unique-method-74-lines.js +82 -0
  75. package/dist/analyzers/__tests__/fixtures/spec-17/unique-method-74-lines.js.map +1 -0
  76. package/dist/analyzers/__tests__/fixtures/spec-17/word-the-in-comment.d.ts +10 -0
  77. package/dist/analyzers/__tests__/fixtures/spec-17/word-the-in-comment.d.ts.map +1 -0
  78. package/dist/analyzers/__tests__/fixtures/spec-17/word-the-in-comment.js +15 -0
  79. package/dist/analyzers/__tests__/fixtures/spec-17/word-the-in-comment.js.map +1 -0
  80. package/dist/analyzers/cross-language/APIContractAnalyzer.js +3 -3
  81. package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
  82. package/dist/analyzers/cross-language/CrossLanguageSOLIDAnalyzer.js +1 -1
  83. package/dist/analyzers/cross-language/CrossLanguageSOLIDAnalyzer.js.map +1 -1
  84. package/dist/analyzers/cross-language/DependencyGraphBuilder.js +2 -2
  85. package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
  86. package/dist/analyzers/cross-language/SchemaValidator.js +2 -2
  87. package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
  88. package/dist/analyzers/reactAnalyzer.js +7 -22
  89. package/dist/analyzers/reactAnalyzer.js.map +1 -1
  90. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts +40 -2
  91. package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
  92. package/dist/analyzers/universal/UniversalDRYAnalyzer.js +186 -29
  93. package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
  94. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +30 -0
  95. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  96. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +164 -11
  97. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  98. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +51 -7
  99. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
  100. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +354 -48
  101. package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
  102. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +8 -4
  103. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
  104. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +51 -38
  105. package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
  106. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +59 -39
  107. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
  108. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +412 -153
  109. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
  110. package/dist/auditRunner.d.ts.map +1 -1
  111. package/dist/auditRunner.js +26 -2
  112. package/dist/auditRunner.js.map +1 -1
  113. package/dist/cli.js.map +1 -1
  114. package/dist/config/defaults.d.ts +32 -0
  115. package/dist/config/defaults.d.ts.map +1 -1
  116. package/dist/config/defaults.js +62 -4
  117. package/dist/config/defaults.js.map +1 -1
  118. package/dist/index.d.ts +0 -3
  119. package/dist/index.d.ts.map +1 -1
  120. package/dist/index.js +2 -4
  121. package/dist/index.js.map +1 -1
  122. package/dist/languages/UniversalAnalyzer.d.ts +7 -0
  123. package/dist/languages/UniversalAnalyzer.d.ts.map +1 -1
  124. package/dist/languages/UniversalAnalyzer.js +16 -1
  125. package/dist/languages/UniversalAnalyzer.js.map +1 -1
  126. package/dist/mcp-tools/whitelistTools.d.ts +1 -1
  127. package/package.json +2 -1
  128. package/plugin/.claude-plugin/plugin.json +1 -1
@@ -1,9 +1,15 @@
1
1
  /**
2
- * Universal Schema Analyzer
3
- * Works across multiple programming languages using the adapter pattern
4
- * Analyzes code against database schemas
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
- // Find table references in the code
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
- violations.push(this.createViolation(ast.filePath, ref.location, `Reference to unknown table '${ref.table}'`, 'warning', 'unknown-table'));
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
- * Find table references in the code
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
- findTableReferences(ast, adapter, sourceCode) {
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
- // Common patterns for table references
111
- const patterns = [
112
- // ORM patterns
113
- /from\s*\(\s*["'`]?(\w+)["'`]?\s*\)/gi,
114
- /table\s*[:=]\s*["'`](\w+)["'`]/gi,
115
- /\.(\w+Table)\s*\(/gi,
116
- // SQL patterns
117
- /FROM\s+["'`]?(\w+)["'`]?/gi,
118
- /JOIN\s+["'`]?(\w+)["'`]?/gi,
119
- /UPDATE\s+["'`]?(\w+)["'`]?/gi,
120
- /INSERT\s+INTO\s+["'`]?(\w+)["'`]?/gi,
121
- /DELETE\s+FROM\s+["'`]?(\w+)["'`]?/gi,
122
- /CREATE\s+TABLE\s+["'`]?(\w+)["'`]?/gi,
123
- // Schema definition patterns
124
- /pgTable\s*\(\s*["'`](\w+)["'`]/gi,
125
- /mysqlTable\s*\(\s*["'`](\w+)["'`]/gi,
126
- /sqliteTable\s*\(\s*["'`](\w+)["'`]/gi
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 pattern of patterns) {
253
+ for (const { regex, type } of sqlPatterns) {
129
254
  let match;
130
- while ((match = pattern.exec(sourceCode)) !== null) {
131
- const table = match[1];
132
- const index = match.index;
133
- const lines = sourceCode.substring(0, index).split('\n');
134
- const line = lines.length;
135
- const column = lines[lines.length - 1].length + 1;
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: this.getQueryType(match[0]),
139
- location: { line, column },
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
- * Find column references
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, // table.column comparison
154
- /SELECT\s+.*?(\w+)\.(\w+)/gi, // SELECT table.column
155
- /WHERE\s+.*?(\w+)\.(\w+)/gi, // WHERE table.column
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
- * Check naming conventions
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
- * Check query patterns
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
- // Check for N+1 query patterns
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
- * Check for SQL injection vulnerabilities
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, // Template literals in queries
224
- /query\s*\(\s*['"][^'"]*['"]?\s*\+/g, // String concatenation
225
- /execute\s*\(\s*['"][^'"]*['"]?\s*\+/g, // String concatenation in execute
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.', 'critical', 'sql-injection'));
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
- * Helper methods
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
- * Analyze JSON Schema files
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: 'critical',
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: 'critical',
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: 'critical',
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: 'critical',
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: 'critical',
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: 'critical',
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: 'critical',
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: 'critical',
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; // Don't validate further if type is wrong
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: 'critical',
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: config.strictMode ? 'critical' : 'warning',
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: 'critical',
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