@rigour-labs/core 3.0.5 → 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.
- package/dist/gates/deprecated-apis-rules-lang.d.ts +21 -0
- package/dist/gates/deprecated-apis-rules-lang.js +311 -0
- package/dist/gates/deprecated-apis-rules-node.d.ts +19 -0
- package/dist/gates/deprecated-apis-rules-node.js +199 -0
- package/dist/gates/deprecated-apis-rules.d.ts +6 -0
- package/dist/gates/deprecated-apis-rules.js +6 -0
- package/dist/gates/deprecated-apis.js +1 -502
- package/dist/gates/hallucinated-imports-lang.d.ts +16 -0
- package/dist/gates/hallucinated-imports-lang.js +374 -0
- package/dist/gates/hallucinated-imports-stdlib.d.ts +12 -0
- package/dist/gates/hallucinated-imports-stdlib.js +228 -0
- package/dist/gates/hallucinated-imports.d.ts +0 -98
- package/dist/gates/hallucinated-imports.js +10 -678
- package/dist/gates/phantom-apis-data.d.ts +33 -0
- package/dist/gates/phantom-apis-data.js +398 -0
- package/dist/gates/phantom-apis.js +1 -393
- package/dist/gates/phantom-apis.test.js +52 -0
- package/dist/gates/promise-safety-helpers.d.ts +19 -0
- package/dist/gates/promise-safety-helpers.js +101 -0
- package/dist/gates/promise-safety-rules.d.ts +7 -0
- package/dist/gates/promise-safety-rules.js +19 -0
- package/dist/gates/promise-safety.d.ts +1 -21
- package/dist/gates/promise-safety.js +51 -257
- package/dist/gates/test-quality-lang.d.ts +30 -0
- package/dist/gates/test-quality-lang.js +188 -0
- package/dist/gates/test-quality.d.ts +0 -14
- package/dist/gates/test-quality.js +13 -186
- package/dist/pattern-index/indexer-helpers.d.ts +38 -0
- package/dist/pattern-index/indexer-helpers.js +111 -0
- package/dist/pattern-index/indexer-lang.d.ts +13 -0
- package/dist/pattern-index/indexer-lang.js +244 -0
- package/dist/pattern-index/indexer-ts.d.ts +22 -0
- package/dist/pattern-index/indexer-ts.js +258 -0
- package/dist/pattern-index/indexer.d.ts +4 -106
- package/dist/pattern-index/indexer.js +58 -707
- package/dist/pattern-index/staleness-data.d.ts +6 -0
- package/dist/pattern-index/staleness-data.js +262 -0
- package/dist/pattern-index/staleness.js +1 -258
- package/dist/templates/index.d.ts +12 -16
- package/dist/templates/index.js +11 -527
- package/dist/templates/paradigms.d.ts +2 -0
- package/dist/templates/paradigms.js +46 -0
- package/dist/templates/presets.d.ts +14 -0
- package/dist/templates/presets.js +227 -0
- package/dist/templates/universal-config.d.ts +2 -0
- package/dist/templates/universal-config.js +171 -0
- 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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
case '
|
|
93
|
-
|
|
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]) && !
|
|
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 =
|
|
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
|
|
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 (
|
|
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
|
|
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]) && !
|
|
192
|
-
violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `json.loads() without try/except
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]) && !
|
|
310
|
-
violations.push({ file, line: i + 1, type: 'unsafe-parse', code: lines[i].trim().substring(0, 80), reason: `JSON.parse without begin/rescue
|
|
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])
|
|
320
|
-
|
|
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
|
|
341
|
-
|
|
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
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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 =
|
|
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
|
|
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
|
|
380
|
-
|
|
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
|
-
|
|
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;
|