@rigour-labs/core 3.0.4 → 3.0.6

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 (47) hide show
  1. package/dist/gates/deprecated-apis-rules-lang.d.ts +21 -0
  2. package/dist/gates/deprecated-apis-rules-lang.js +311 -0
  3. package/dist/gates/deprecated-apis-rules-node.d.ts +19 -0
  4. package/dist/gates/deprecated-apis-rules-node.js +199 -0
  5. package/dist/gates/deprecated-apis-rules.d.ts +6 -0
  6. package/dist/gates/deprecated-apis-rules.js +6 -0
  7. package/dist/gates/deprecated-apis.js +1 -502
  8. package/dist/gates/hallucinated-imports-lang.d.ts +16 -0
  9. package/dist/gates/hallucinated-imports-lang.js +374 -0
  10. package/dist/gates/hallucinated-imports-stdlib.d.ts +12 -0
  11. package/dist/gates/hallucinated-imports-stdlib.js +228 -0
  12. package/dist/gates/hallucinated-imports.d.ts +0 -98
  13. package/dist/gates/hallucinated-imports.js +10 -678
  14. package/dist/gates/phantom-apis-data.d.ts +33 -0
  15. package/dist/gates/phantom-apis-data.js +398 -0
  16. package/dist/gates/phantom-apis.js +1 -393
  17. package/dist/gates/phantom-apis.test.js +52 -0
  18. package/dist/gates/promise-safety-helpers.d.ts +19 -0
  19. package/dist/gates/promise-safety-helpers.js +101 -0
  20. package/dist/gates/promise-safety-rules.d.ts +7 -0
  21. package/dist/gates/promise-safety-rules.js +19 -0
  22. package/dist/gates/promise-safety.d.ts +1 -21
  23. package/dist/gates/promise-safety.js +51 -257
  24. package/dist/gates/test-quality-lang.d.ts +30 -0
  25. package/dist/gates/test-quality-lang.js +188 -0
  26. package/dist/gates/test-quality.d.ts +0 -14
  27. package/dist/gates/test-quality.js +13 -186
  28. package/dist/pattern-index/indexer-helpers.d.ts +38 -0
  29. package/dist/pattern-index/indexer-helpers.js +111 -0
  30. package/dist/pattern-index/indexer-lang.d.ts +13 -0
  31. package/dist/pattern-index/indexer-lang.js +244 -0
  32. package/dist/pattern-index/indexer-ts.d.ts +22 -0
  33. package/dist/pattern-index/indexer-ts.js +258 -0
  34. package/dist/pattern-index/indexer.d.ts +4 -106
  35. package/dist/pattern-index/indexer.js +58 -707
  36. package/dist/pattern-index/staleness-data.d.ts +6 -0
  37. package/dist/pattern-index/staleness-data.js +262 -0
  38. package/dist/pattern-index/staleness.js +1 -258
  39. package/dist/templates/index.d.ts +12 -16
  40. package/dist/templates/index.js +11 -527
  41. package/dist/templates/paradigms.d.ts +2 -0
  42. package/dist/templates/paradigms.js +46 -0
  43. package/dist/templates/presets.d.ts +14 -0
  44. package/dist/templates/presets.js +227 -0
  45. package/dist/templates/universal-config.d.ts +2 -0
  46. package/dist/templates/universal-config.js +171 -0
  47. package/package.json +1 -1
@@ -2,15 +2,7 @@
2
2
  * Async & Error Safety Gate (Multi-Language)
3
3
  *
4
4
  * Detects unsafe async/promise/error patterns that AI code generators commonly produce.
5
- * LLMs understand synchronous control flow well but frequently produce incomplete
6
- * async and error-handling patterns across all languages.
7
- *
8
- * Supported languages:
9
- * - JS/TS: .then() without .catch(), JSON.parse without try/catch, async without await, fetch without error handling
10
- * - Python: json.loads without try/except, async def without await, requests/httpx without error handling, bare except
11
- * - Go: ignored error returns (_, err pattern), json.Unmarshal without error check, http calls without error check
12
- * - Ruby: JSON.parse without begin/rescue, Net::HTTP without begin/rescue
13
- * - C#/.NET: JsonSerializer without try/catch, HttpClient without try/catch, async without await, .Result/.Wait() deadlocks
5
+ * Supports: JS/TS, Python, Go, Ruby, C#/.NET
14
6
  *
15
7
  * @since v2.17.0
16
8
  */
@@ -52,17 +44,5 @@ export declare class PromiseSafetyGate extends Gate {
52
44
  private detectUnsafeFetchCSharp;
53
45
  private detectAsyncWithoutAwaitCSharp;
54
46
  private detectDeadlockRiskCSharp;
55
- private extractBraceBody;
56
- /** Extract Python indented body after a colon */
57
- private extractIndentedBody;
58
- /** Check if line is inside try block (JS/TS/C# — brace-based) */
59
- private isInsideTryBlock;
60
- /** Check if line is inside Python try block (indent-based) */
61
- private isInsidePythonTry;
62
- /** Check if line is inside Ruby begin/rescue block */
63
- private isInsideRubyRescue;
64
- private hasCatchAhead;
65
- private hasStatusCheckAhead;
66
- private stripStrings;
67
47
  private buildFailures;
68
48
  }
@@ -2,39 +2,17 @@
2
2
  * Async & Error Safety Gate (Multi-Language)
3
3
  *
4
4
  * Detects unsafe async/promise/error patterns that AI code generators commonly produce.
5
- * LLMs understand synchronous control flow well but frequently produce incomplete
6
- * async and error-handling patterns across all languages.
7
- *
8
- * Supported languages:
9
- * - JS/TS: .then() without .catch(), JSON.parse without try/catch, async without await, fetch without error handling
10
- * - Python: json.loads without try/except, async def without await, requests/httpx without error handling, bare except
11
- * - Go: ignored error returns (_, err pattern), json.Unmarshal without error check, http calls without error check
12
- * - Ruby: JSON.parse without begin/rescue, Net::HTTP without begin/rescue
13
- * - C#/.NET: JsonSerializer without try/catch, HttpClient without try/catch, async without await, .Result/.Wait() deadlocks
5
+ * Supports: JS/TS, Python, Go, Ruby, C#/.NET
14
6
  *
15
7
  * @since v2.17.0
16
8
  */
17
9
  import { Gate } from './base.js';
18
10
  import { FileScanner } from '../utils/scanner.js';
19
11
  import { Logger } from '../utils/logger.js';
12
+ import { LANG_EXTENSIONS, LANG_GLOBS } from './promise-safety-rules.js';
13
+ import { extractBraceBody, extractIndentedBody, isInsideTryBlock, isInsidePythonTry, isInsideRubyRescue, hasCatchAhead, hasStatusCheckAhead } from './promise-safety-helpers.js';
20
14
  import fs from 'fs-extra';
21
15
  import path from 'path';
22
- // ─── Language Detection ───────────────────────────────────────────
23
- const LANG_EXTENSIONS = {
24
- '.ts': 'js', '.tsx': 'js', '.js': 'js', '.jsx': 'js', '.mjs': 'js', '.cjs': 'js',
25
- '.py': 'python', '.pyw': 'python',
26
- '.go': 'go',
27
- '.rb': 'ruby', '.rake': 'ruby',
28
- '.cs': 'csharp',
29
- };
30
- const LANG_GLOBS = {
31
- js: ['**/*.{ts,js,tsx,jsx,mjs,cjs}'],
32
- python: ['**/*.py'],
33
- go: ['**/*.go'],
34
- ruby: ['**/*.rb'],
35
- csharp: ['**/*.cs'],
36
- unknown: [],
37
- };
38
16
  function detectLang(filePath) {
39
17
  const ext = path.extname(filePath).toLowerCase();
40
18
  return LANG_EXTENSIONS[ext] || 'unknown';
@@ -57,7 +35,6 @@ export class PromiseSafetyGate extends Gate {
57
35
  if (!this.config.enabled)
58
36
  return [];
59
37
  const violations = [];
60
- // Scan all supported languages
61
38
  const allPatterns = Object.values(LANG_GLOBS).flat();
62
39
  const files = await FileScanner.findFiles({
63
40
  cwd: context.cwd,
@@ -79,33 +56,19 @@ export class PromiseSafetyGate extends Gate {
79
56
  const lines = content.split('\n');
80
57
  this.scanFile(lang, lines, content, file, violations);
81
58
  }
82
- catch { /* skip unreadable files */ }
59
+ catch { /* skip */ }
83
60
  }
84
61
  return this.buildFailures(violations);
85
62
  }
86
- // ─── Multi-Language Dispatcher ────────────────────────
87
63
  scanFile(lang, lines, content, file, violations) {
88
64
  switch (lang) {
89
- case 'js':
90
- this.scanJS(lines, content, file, violations);
91
- break;
92
- case 'python':
93
- this.scanPython(lines, content, file, violations);
94
- break;
95
- case 'go':
96
- this.scanGo(lines, content, file, violations);
97
- break;
98
- case 'ruby':
99
- this.scanRuby(lines, content, file, violations);
100
- break;
101
- case 'csharp':
102
- this.scanCSharp(lines, content, file, violations);
103
- break;
65
+ case 'js': return this.scanJS(lines, content, file, violations);
66
+ case 'python': return this.scanPython(lines, content, file, violations);
67
+ case 'go': return this.scanGo(lines, content, file, violations);
68
+ case 'ruby': return this.scanRuby(lines, content, file, violations);
69
+ case 'csharp': return this.scanCSharp(lines, content, file, violations);
104
70
  }
105
71
  }
106
- // ═══════════════════════════════════════════════════════
107
- // JS/TS Checks
108
- // ═══════════════════════════════════════════════════════
109
72
  scanJS(lines, content, file, violations) {
110
73
  if (this.config.check_unhandled_then)
111
74
  this.detectUnhandledThen(lines, file, violations);
@@ -129,36 +92,29 @@ export class PromiseSafetyGate extends Gate {
129
92
  if (j > i && /^(?:const|let|var|function|class|export|import|if|for|while|return)\b/.test(lines[j].trim()))
130
93
  break;
131
94
  }
132
- if (!hasCatch)
133
- hasCatch = this.isInsideTryBlock(lines, i);
134
- const isStored = /(?:const|let|var)\s+\w+\s*=/.test(lines[i]);
135
- if (!hasCatch && !isStored) {
95
+ if (!hasCatch && !isInsideTryBlock(lines, i) && !/(?:const|let|var)\s+\w+\s*=/.test(lines[i])) {
136
96
  violations.push({ file, line: i + 1, type: 'unhandled-then', code: lines[i].trim().substring(0, 80), reason: `.then() chain without .catch() — unhandled promise rejection` });
137
97
  }
138
98
  }
139
99
  }
140
100
  detectUnsafeParseJS(lines, file, violations) {
141
101
  for (let i = 0; i < lines.length; i++) {
142
- if (/JSON\.parse\s*\(/.test(lines[i]) && !this.isInsideTryBlock(lines, i)) {
102
+ if (/JSON\.parse\s*\(/.test(lines[i]) && !isInsideTryBlock(lines, i)) {
143
103
  violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `JSON.parse() without try/catch — crashes on malformed input` });
144
104
  }
145
105
  }
146
106
  }
147
107
  detectAsyncWithoutAwaitJS(content, file, violations) {
148
- const patterns = [
149
- /async\s+function\s+(\w+)\s*\([^)]*\)\s*\{/g,
150
- /(?:const|let|var)\s+(\w+)\s*=\s*async\s+(?:\([^)]*\)|[a-zA-Z_$]\w*)\s*=>\s*\{/g,
151
- /async\s+(\w+)\s*\([^)]*\)\s*(?::\s*[^{]+)?\s*\{/g,
152
- ];
108
+ const patterns = [/async\s+function\s+(\w+)\s*\([^)]*\)\s*\{/g, /(?:const|let|var)\s+(\w+)\s*=\s*async\s+(?:\([^)]*\)|[a-zA-Z_$]\w*)\s*=>\s*\{/g];
153
109
  for (const pattern of patterns) {
154
110
  pattern.lastIndex = 0;
155
111
  let match;
156
112
  while ((match = pattern.exec(content)) !== null) {
157
113
  const funcName = match[1];
158
- const body = this.extractBraceBody(content, match.index + match[0].length);
114
+ const body = extractBraceBody(content, match.index + match[0].length);
159
115
  if (body && !/\bawait\b/.test(body) && body.trim().split('\n').length > 2) {
160
116
  const lineNum = content.substring(0, match.index).split('\n').length;
161
- violations.push({ file, line: lineNum, type: 'async-no-await', code: `async ${funcName}()`, reason: `async function '${funcName}' never uses await — unnecessary async or missing await` });
117
+ violations.push({ file, line: lineNum, type: 'async-no-await', code: `async ${funcName}()`, reason: `async function never uses await — unnecessary async or missing await` });
162
118
  }
163
119
  }
164
120
  }
@@ -167,16 +123,11 @@ export class PromiseSafetyGate extends Gate {
167
123
  for (let i = 0; i < lines.length; i++) {
168
124
  if (!/\bfetch\s*\(/.test(lines[i]) && !/\baxios\.\w+\s*\(/.test(lines[i]))
169
125
  continue;
170
- if (this.isInsideTryBlock(lines, i))
171
- continue;
172
- if (this.hasCatchAhead(lines, i) || this.hasStatusCheckAhead(lines, i))
126
+ if (isInsideTryBlock(lines, i) || hasCatchAhead(lines, i) || hasStatusCheckAhead(lines, i))
173
127
  continue;
174
- violations.push({ file, line: i + 1, type: 'unsafe-fetch', code: lines[i].trim().substring(0, 80), reason: `HTTP call without error handling (no try/catch, no .catch(), no .ok check)` });
128
+ violations.push({ file, line: i + 1, type: 'unsafe-fetch', code: lines[i].trim().substring(0, 80), reason: `HTTP call without error handling` });
175
129
  }
176
130
  }
177
- // ═══════════════════════════════════════════════════════
178
- // Python Checks
179
- // ═══════════════════════════════════════════════════════
180
131
  scanPython(lines, content, file, violations) {
181
132
  if (this.config.check_unsafe_parse)
182
133
  this.detectUnsafeParsePython(lines, file, violations);
@@ -188,11 +139,8 @@ export class PromiseSafetyGate extends Gate {
188
139
  }
189
140
  detectUnsafeParsePython(lines, file, violations) {
190
141
  for (let i = 0; i < lines.length; i++) {
191
- if (/json\.loads?\s*\(/.test(lines[i]) && !this.isInsidePythonTry(lines, i)) {
192
- violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `json.loads() without try/except — crashes on malformed input` });
193
- }
194
- if (/yaml\.safe_load\s*\(/.test(lines[i]) && !this.isInsidePythonTry(lines, i)) {
195
- violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `yaml.safe_load() without try/except — crashes on malformed input` });
142
+ if (/json\.loads?\s*\(/.test(lines[i]) && !isInsidePythonTry(lines, i)) {
143
+ violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `json.loads() without try/except` });
196
144
  }
197
145
  }
198
146
  }
@@ -201,30 +149,27 @@ export class PromiseSafetyGate extends Gate {
201
149
  let match;
202
150
  while ((match = pattern.exec(content)) !== null) {
203
151
  const funcName = match[1];
204
- const body = this.extractIndentedBody(content, match.index + match[0].length);
152
+ const body = extractIndentedBody(content, match.index + match[0].length);
205
153
  if (body && !/\bawait\b/.test(body) && body.trim().split('\n').length > 2) {
206
154
  const lineNum = content.substring(0, match.index).split('\n').length;
207
- violations.push({ file, line: lineNum, type: 'async-no-await', code: `async def ${funcName}()`, reason: `async def '${funcName}' never uses await — unnecessary async or missing await` });
155
+ violations.push({ file, line: lineNum, type: 'async-no-await', code: `async def ${funcName}()`, reason: `async def never uses await` });
208
156
  }
209
157
  }
210
158
  }
211
159
  detectUnsafeFetchPython(lines, file, violations) {
212
- const httpPatterns = /\b(?:requests\.(?:get|post|put|patch|delete)|httpx\.(?:get|post|put|patch|delete)|aiohttp\.ClientSession|urllib\.request\.urlopen)\s*\(/;
160
+ const httpPatterns = /\b(?:requests|httpx|aiohttp|urllib)\.(?:get|post|ClientSession|urlopen)\s*\(/;
213
161
  for (let i = 0; i < lines.length; i++) {
214
- if (!httpPatterns.test(lines[i]))
162
+ if (!httpPatterns.test(lines[i]) || isInsidePythonTry(lines, i))
215
163
  continue;
216
- if (this.isInsidePythonTry(lines, i))
217
- continue;
218
- // Check for raise_for_status() within 10 lines
219
164
  let hasCheck = false;
220
165
  for (let j = i; j < Math.min(i + 10, lines.length); j++) {
221
- if (/raise_for_status\s*\(/.test(lines[j]) || /\.status_code\b/.test(lines[j])) {
166
+ if (/raise_for_status|status_code/.test(lines[j])) {
222
167
  hasCheck = true;
223
168
  break;
224
169
  }
225
170
  }
226
171
  if (!hasCheck) {
227
- violations.push({ file, line: i + 1, type: 'unsafe-fetch', code: lines[i].trim().substring(0, 80), reason: `HTTP call without error handling (no try/except, no raise_for_status)` });
172
+ violations.push({ file, line: i + 1, type: 'unsafe-fetch', code: lines[i].trim().substring(0, 80), reason: `HTTP call without error handling` });
228
173
  }
229
174
  }
230
175
  }
@@ -232,17 +177,13 @@ export class PromiseSafetyGate extends Gate {
232
177
  for (let i = 0; i < lines.length; i++) {
233
178
  const trimmed = lines[i].trim();
234
179
  if (/^except\s*:/.test(trimmed) || /^except\s+Exception\s*:/.test(trimmed)) {
235
- // Check if the except block just passes (swallows errors silently)
236
180
  const nextLine = i + 1 < lines.length ? lines[i + 1].trim() : '';
237
181
  if (nextLine === 'pass' || nextLine === '...') {
238
- violations.push({ file, line: i + 1, type: 'bare-except', code: trimmed, reason: `Bare except with pass — silently swallows all errors` });
182
+ violations.push({ file, line: i + 1, type: 'bare-except', code: trimmed, reason: `Bare except with pass — silently swallows errors` });
239
183
  }
240
184
  }
241
185
  }
242
186
  }
243
- // ═══════════════════════════════════════════════════════
244
- // Go Checks
245
- // ═══════════════════════════════════════════════════════
246
187
  scanGo(lines, content, file, violations) {
247
188
  if (this.config.check_unsafe_parse)
248
189
  this.detectUnsafeParseGo(lines, file, violations);
@@ -252,52 +193,26 @@ export class PromiseSafetyGate extends Gate {
252
193
  }
253
194
  detectUnsafeParseGo(lines, file, violations) {
254
195
  for (let i = 0; i < lines.length; i++) {
255
- // json.Unmarshal returns error check if error is ignored
256
- if (/json\.(?:Unmarshal|NewDecoder)/.test(lines[i])) {
257
- if (/\b_\s*(?:=|:=)/.test(lines[i]) || !/\berr\b/.test(lines[i])) {
258
- violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `JSON decode with ignored error return — crashes on malformed input` });
259
- }
196
+ if (/json\.(?:Unmarshal|NewDecoder)/.test(lines[i]) && (/\b_\s*(?:=|:=)/.test(lines[i]) || !/\berr\b/.test(lines[i]))) {
197
+ violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `JSON decode with ignored error` });
260
198
  }
261
199
  }
262
200
  }
263
201
  detectUnsafeFetchGo(lines, file, violations) {
264
202
  for (let i = 0; i < lines.length; i++) {
265
- if (/http\.(?:Get|Post|Do|Head)\s*\(/.test(lines[i])) {
266
- if (/\b_\s*(?:=|:=)/.test(lines[i]) || !/\berr\b/.test(lines[i])) {
267
- violations.push({ file, line: i + 1, type: 'unsafe-fetch', code: lines[i].trim().substring(0, 80), reason: `HTTP call with ignored error return — unhandled network errors` });
268
- }
269
- // Also check if resp.Body is closed (defer resp.Body.Close())
270
- let hasClose = false;
271
- for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
272
- if (/defer\s+.*\.Body\.Close\(\)/.test(lines[j]) || /\.Body\.Close\(\)/.test(lines[j])) {
273
- hasClose = true;
274
- break;
275
- }
276
- }
277
- if (!hasClose && /\berr\b/.test(lines[i])) {
278
- // Don't flag if error IS checked — only flag missing Body.Close
279
- // This is a softer check, skip for now to reduce noise
280
- }
203
+ if (/http\.(?:Get|Post|Do|Head)\s*\(/.test(lines[i]) && (/\b_\s*(?:=|:=)/.test(lines[i]) || !/\berr\b/.test(lines[i]))) {
204
+ violations.push({ file, line: i + 1, type: 'unsafe-fetch', code: lines[i].trim().substring(0, 80), reason: `HTTP call with ignored error` });
281
205
  }
282
206
  }
283
207
  }
284
208
  detectIgnoredErrorsGo(lines, file, violations) {
285
209
  for (let i = 0; i < lines.length; i++) {
286
- // Detect: result, _ := someFunc() or _ = someFunc()
287
- // Only flag when the ignored return is likely an error
288
210
  const match = lines[i].match(/(\w+)\s*,\s*_\s*(?::=|=)\s*(\w+)\./);
289
- if (match) {
290
- const funcCall = lines[i].trim();
291
- // Common error-returning functions
292
- if (/\b(?:os\.|io\.|ioutil\.|bufio\.|sql\.|net\.|http\.|json\.|xml\.|yaml\.|strconv\.)/.test(funcCall)) {
293
- violations.push({ file, line: i + 1, type: 'ignored-error', code: funcCall.substring(0, 80), reason: `Error return ignored with _ — unhandled error can cause silent failures` });
294
- }
211
+ if (match && /\b(?:os|io|ioutil|bufio|sql|net|http|json|xml|yaml|strconv)\./.test(lines[i].trim())) {
212
+ violations.push({ file, line: i + 1, type: 'ignored-error', code: lines[i].trim().substring(0, 80), reason: `Error return ignored with _` });
295
213
  }
296
214
  }
297
215
  }
298
- // ═══════════════════════════════════════════════════════
299
- // Ruby Checks
300
- // ═══════════════════════════════════════════════════════
301
216
  scanRuby(lines, content, file, violations) {
302
217
  if (this.config.check_unsafe_parse)
303
218
  this.detectUnsafeParseRuby(lines, file, violations);
@@ -306,26 +221,18 @@ export class PromiseSafetyGate extends Gate {
306
221
  }
307
222
  detectUnsafeParseRuby(lines, file, violations) {
308
223
  for (let i = 0; i < lines.length; i++) {
309
- if (/JSON\.parse\s*\(/.test(lines[i]) && !this.isInsideRubyRescue(lines, i)) {
310
- violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `JSON.parse without begin/rescue — crashes on malformed input` });
311
- }
312
- if (/YAML\.(?:safe_)?load\s*\(/.test(lines[i]) && !this.isInsideRubyRescue(lines, i)) {
313
- violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `YAML.load without begin/rescue — crashes on malformed input` });
224
+ if (/JSON\.parse\s*\(/.test(lines[i]) && !isInsideRubyRescue(lines, i)) {
225
+ violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `JSON.parse without begin/rescue` });
314
226
  }
315
227
  }
316
228
  }
317
229
  detectUnsafeFetchRuby(lines, file, violations) {
318
230
  for (let i = 0; i < lines.length; i++) {
319
- if (/Net::HTTP\.(?:get|post|start)\s*\(/.test(lines[i]) || /HTTParty\.(?:get|post)\s*\(/.test(lines[i]) || /Faraday\.(?:get|post)\s*\(/.test(lines[i])) {
320
- if (!this.isInsideRubyRescue(lines, i)) {
321
- violations.push({ file, line: i + 1, type: 'unsafe-fetch', code: lines[i].trim().substring(0, 80), reason: `HTTP call without begin/rescue — unhandled network errors` });
322
- }
231
+ if (/(?:Net::HTTP|HTTParty|Faraday)\.(?:get|post|start)\s*\(/.test(lines[i]) && !isInsideRubyRescue(lines, i)) {
232
+ violations.push({ file, line: i + 1, type: 'unsafe-fetch', code: lines[i].trim().substring(0, 80), reason: `HTTP call without begin/rescue` });
323
233
  }
324
234
  }
325
235
  }
326
- // ═══════════════════════════════════════════════════════
327
- // C# / .NET Checks
328
- // ═══════════════════════════════════════════════════════
329
236
  scanCSharp(lines, content, file, violations) {
330
237
  if (this.config.check_unsafe_parse)
331
238
  this.detectUnsafeParseCSharp(lines, file, violations);
@@ -337,28 +244,24 @@ export class PromiseSafetyGate extends Gate {
337
244
  }
338
245
  detectUnsafeParseCSharp(lines, file, violations) {
339
246
  for (let i = 0; i < lines.length; i++) {
340
- if (/JsonSerializer\.Deserialize/.test(lines[i]) || /JsonConvert\.DeserializeObject/.test(lines[i]) || /JObject\.Parse/.test(lines[i])) {
341
- if (!this.isInsideTryBlock(lines, i)) {
342
- violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `JSON deserialization without try/catch — crashes on malformed input` });
343
- }
247
+ if (/JsonSerializer|JsonConvert|JObject/.test(lines[i]) && !isInsideTryBlock(lines, i)) {
248
+ violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `JSON deserialization without try/catch` });
344
249
  }
345
250
  }
346
251
  }
347
252
  detectUnsafeFetchCSharp(lines, file, violations) {
348
253
  for (let i = 0; i < lines.length; i++) {
349
- if (/\.GetAsync\s*\(/.test(lines[i]) || /\.PostAsync\s*\(/.test(lines[i]) || /\.SendAsync\s*\(/.test(lines[i]) || /HttpClient\.\w+Async/.test(lines[i])) {
350
- if (!this.isInsideTryBlock(lines, i)) {
351
- let hasCheck = false;
352
- for (let j = i; j < Math.min(i + 10, lines.length); j++) {
353
- if (/EnsureSuccessStatusCode/.test(lines[j]) || /\.IsSuccessStatusCode/.test(lines[j]) || /\.StatusCode/.test(lines[j])) {
354
- hasCheck = true;
355
- break;
356
- }
357
- }
358
- if (!hasCheck) {
359
- violations.push({ file, line: i + 1, type: 'unsafe-fetch', code: lines[i].trim().substring(0, 80), reason: `HTTP call without error handling (no try/catch, no status check)` });
254
+ if (/\.(?:GetAsync|PostAsync|SendAsync)\s*\(/.test(lines[i]) && !isInsideTryBlock(lines, i)) {
255
+ let hasCheck = false;
256
+ for (let j = i; j < Math.min(i + 10, lines.length); j++) {
257
+ if (/EnsureSuccess|IsSuccessStatusCode|StatusCode/.test(lines[j])) {
258
+ hasCheck = true;
259
+ break;
360
260
  }
361
261
  }
262
+ if (!hasCheck) {
263
+ violations.push({ file, line: i + 1, type: 'unsafe-fetch', code: lines[i].trim().substring(0, 80), reason: `HTTP call without error handling` });
264
+ }
362
265
  }
363
266
  }
364
267
  }
@@ -367,127 +270,20 @@ export class PromiseSafetyGate extends Gate {
367
270
  let match;
368
271
  while ((match = pattern.exec(content)) !== null) {
369
272
  const funcName = match[1];
370
- const body = this.extractBraceBody(content, match.index + match[0].length);
273
+ const body = extractBraceBody(content, match.index + match[0].length);
371
274
  if (body && !/\bawait\b/.test(body) && body.trim().split('\n').length > 2) {
372
275
  const lineNum = content.substring(0, match.index).split('\n').length;
373
- violations.push({ file, line: lineNum, type: 'async-no-await', code: `async Task ${funcName}()`, reason: `async method '${funcName}' never uses await — unnecessary async or missing await` });
276
+ violations.push({ file, line: lineNum, type: 'async-no-await', code: `async Task ${funcName}()`, reason: `async method never uses await` });
374
277
  }
375
278
  }
376
279
  }
377
280
  detectDeadlockRiskCSharp(lines, file, violations) {
378
281
  for (let i = 0; i < lines.length; i++) {
379
- if (/\.Result\b/.test(lines[i]) || /\.Wait\(\)/.test(lines[i]) || /\.GetAwaiter\(\)\.GetResult\(\)/.test(lines[i])) {
380
- // Common AI mistake: using .Result or .Wait() on async tasks causes deadlocks
381
- violations.push({ file, line: i + 1, type: 'deadlock-risk', code: lines[i].trim().substring(0, 80), reason: `.Result/.Wait() on async task — deadlock risk in synchronous context` });
382
- }
383
- }
384
- }
385
- // ═══════════════════════════════════════════════════════
386
- // Shared Helpers
387
- // ═══════════════════════════════════════════════════════
388
- extractBraceBody(content, startIdx) {
389
- let depth = 1;
390
- let idx = startIdx;
391
- while (depth > 0 && idx < content.length) {
392
- if (content[idx] === '{')
393
- depth++;
394
- if (content[idx] === '}')
395
- depth--;
396
- idx++;
397
- }
398
- return depth === 0 ? content.substring(startIdx, idx - 1) : null;
399
- }
400
- /** Extract Python indented body after a colon */
401
- extractIndentedBody(content, startIdx) {
402
- const rest = content.substring(startIdx);
403
- const lines = rest.split('\n');
404
- if (lines.length < 2)
405
- return null;
406
- // Find indent level of first non-empty line after the def
407
- let bodyIndent = -1;
408
- const bodyLines = [];
409
- for (let i = 1; i < lines.length; i++) {
410
- const line = lines[i];
411
- if (line.trim() === '' || line.trim().startsWith('#')) {
412
- bodyLines.push(line);
413
- continue;
414
- }
415
- const indent = line.length - line.trimStart().length;
416
- if (bodyIndent === -1) {
417
- bodyIndent = indent;
282
+ if (/\.Result\b|\.Wait\(\)|\.GetAwaiter\(\)\.GetResult\(\)/.test(lines[i])) {
283
+ violations.push({ file, line: i + 1, type: 'deadlock-risk', code: lines[i].trim().substring(0, 80), reason: `.Result/.Wait() on async task deadlock risk` });
418
284
  }
419
- if (indent < bodyIndent)
420
- break;
421
- bodyLines.push(line);
422
285
  }
423
- return bodyLines.join('\n');
424
286
  }
425
- /** Check if line is inside try block (JS/TS/C# — brace-based) */
426
- isInsideTryBlock(lines, lineIdx) {
427
- let braceDepth = 0;
428
- for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - 30); j--) {
429
- const prevLine = this.stripStrings(lines[j]);
430
- for (const ch of prevLine) {
431
- if (ch === '}')
432
- braceDepth++;
433
- if (ch === '{')
434
- braceDepth--;
435
- }
436
- if (/\btry\s*\{/.test(prevLine) && braceDepth <= 0)
437
- return true;
438
- if (/\}\s*catch\s*\(/.test(prevLine))
439
- return false;
440
- }
441
- return false;
442
- }
443
- /** Check if line is inside Python try block (indent-based) */
444
- isInsidePythonTry(lines, lineIdx) {
445
- const lineIndent = lines[lineIdx].length - lines[lineIdx].trimStart().length;
446
- for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - 30); j--) {
447
- const trimmed = lines[j].trim();
448
- if (trimmed === '')
449
- continue;
450
- const indent = lines[j].length - lines[j].trimStart().length;
451
- if (indent < lineIndent && /^\s*try\s*:/.test(lines[j]))
452
- return true;
453
- if (indent < lineIndent && /^\s*(?:except|finally)\s*/.test(lines[j]))
454
- return false;
455
- if (indent === 0 && /^(?:def|class|async\s+def)\s/.test(trimmed))
456
- break;
457
- }
458
- return false;
459
- }
460
- /** Check if line is inside Ruby begin/rescue block */
461
- isInsideRubyRescue(lines, lineIdx) {
462
- for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - 30); j--) {
463
- const trimmed = lines[j].trim();
464
- if (trimmed === 'begin')
465
- return true;
466
- if (/^rescue\b/.test(trimmed))
467
- return false;
468
- if (/^(?:def|class|module)\s/.test(trimmed))
469
- break;
470
- }
471
- return false;
472
- }
473
- hasCatchAhead(lines, idx) {
474
- for (let j = idx; j < Math.min(idx + 10, lines.length); j++) {
475
- if (/\.catch\s*\(/.test(lines[j]))
476
- return true;
477
- }
478
- return false;
479
- }
480
- hasStatusCheckAhead(lines, idx) {
481
- for (let j = idx; j < Math.min(idx + 10, lines.length); j++) {
482
- if (/\.\s*ok\b/.test(lines[j]) || /\.status(?:Text)?\b/.test(lines[j]))
483
- return true;
484
- }
485
- return false;
486
- }
487
- stripStrings(line) {
488
- return line.replace(/`[^`]*`/g, '""').replace(/"[^"]*"/g, '""').replace(/'[^']*'/g, '""');
489
- }
490
- // ─── Failure Aggregation ──────────────────────────────
491
287
  buildFailures(violations) {
492
288
  const byFile = new Map();
493
289
  for (const v of violations) {
@@ -500,9 +296,7 @@ export class PromiseSafetyGate extends Gate {
500
296
  const details = fileViolations.map(v => ` L${v.line}: [${v.type}] ${v.reason}`).join('\n');
501
297
  const hasHighSev = fileViolations.some(v => v.type !== 'async-no-await');
502
298
  const severity = hasHighSev ? 'high' : 'medium';
503
- const lang = detectLang(file);
504
- const langLabel = lang === 'js' ? 'JS/TS' : lang === 'csharp' ? 'C#' : lang.charAt(0).toUpperCase() + lang.slice(1);
505
- failures.push(this.createFailure(`Unsafe async/error patterns in ${file}:\n${details}`, [file], `AI code generators often produce incomplete error handling in ${langLabel}. Ensure all parse operations are wrapped in error handling, async functions use await, and HTTP calls check for errors.`, 'Async & Error Safety Violation', fileViolations[0].line, undefined, severity));
299
+ failures.push(this.createFailure(`Unsafe async/error patterns in ${file}:\n${details}`, [file], `Review and fix async/error handling patterns.`, 'Async & Error Safety Violation', fileViolations[0].line, undefined, severity));
506
300
  }
507
301
  return failures;
508
302
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Language-specific test quality checks for Go and Java/Kotlin.
3
+ * Extracted from test-quality.ts to keep it under 500 lines.
4
+ */
5
+ export interface TestQualityIssue {
6
+ file: string;
7
+ line: number;
8
+ pattern: string;
9
+ reason: string;
10
+ }
11
+ export interface TestQualityConfig {
12
+ check_empty_tests?: boolean;
13
+ check_tautological?: boolean;
14
+ check_mock_heavy?: boolean;
15
+ max_mocks_per_test?: number;
16
+ }
17
+ /**
18
+ * Go test quality checks.
19
+ * Go tests use func TestXxx(t *testing.T) pattern.
20
+ * Assertions via t.Fatal, t.Error, t.Fatalf, t.Errorf, t.Fail, t.FailNow.
21
+ * Also checks for t.Run subtests and table-driven patterns.
22
+ */
23
+ export declare function checkGoTestQuality(content: string, file: string, issues: TestQualityIssue[], config: TestQualityConfig): void;
24
+ /**
25
+ * Java/Kotlin test quality checks.
26
+ * JUnit 4: @Test + Assert.assertEquals, assertTrue, etc.
27
+ * JUnit 5: @Test + Assertions.assertEquals, assertThrows, etc.
28
+ * Kotlin: @Test + kotlin.test assertEquals, etc.
29
+ */
30
+ export declare function checkJavaKotlinTestQuality(content: string, file: string, ext: string, issues: TestQualityIssue[], config: TestQualityConfig): void;