@rigour-labs/core 5.0.1 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/gates/agent-team.d.ts +0 -1
- package/dist/gates/agent-team.js +0 -1
- package/dist/gates/checkpoint.d.ts +0 -2
- package/dist/gates/checkpoint.js +0 -2
- package/dist/gates/context-window-artifacts.d.ts +6 -2
- package/dist/gates/context-window-artifacts.js +107 -31
- package/dist/gates/deep-analysis.d.ts +2 -0
- package/dist/gates/deep-analysis.js +41 -11
- package/dist/gates/dependency.d.ts +0 -2
- package/dist/gates/dependency.js +23 -5
- package/dist/gates/deprecated-apis.d.ts +0 -2
- package/dist/gates/deprecated-apis.js +33 -20
- package/dist/gates/duplication-drift/index.d.ts +61 -0
- package/dist/gates/duplication-drift/index.js +240 -0
- package/dist/gates/duplication-drift/similarity.d.ts +68 -0
- package/dist/gates/duplication-drift/similarity.js +177 -0
- package/dist/gates/duplication-drift/tokenizer.d.ts +55 -0
- package/dist/gates/duplication-drift/tokenizer.js +195 -0
- package/dist/gates/frontend-secret-exposure.d.ts +0 -3
- package/dist/gates/frontend-secret-exposure.js +1 -114
- package/dist/gates/frontend-secret-patterns.d.ts +33 -0
- package/dist/gates/frontend-secret-patterns.js +119 -0
- package/dist/gates/{hallucinated-imports.d.ts → hallucinated-imports/index.d.ts} +2 -29
- package/dist/gates/hallucinated-imports/index.js +174 -0
- package/dist/gates/hallucinated-imports/js-resolver.d.ts +45 -0
- package/dist/gates/hallucinated-imports/js-resolver.js +320 -0
- package/dist/gates/hallucinated-imports/manifest-discovery.d.ts +28 -0
- package/dist/gates/hallucinated-imports/manifest-discovery.js +114 -0
- package/dist/gates/hallucinated-imports/python-resolver.d.ts +24 -0
- package/dist/gates/hallucinated-imports/python-resolver.js +306 -0
- package/dist/gates/hallucinated-imports-lang.d.ts +2 -2
- package/dist/gates/hallucinated-imports-lang.js +269 -34
- package/dist/gates/hallucinated-imports.test.js +1 -2
- package/dist/gates/inconsistent-error-handling.d.ts +0 -5
- package/dist/gates/inconsistent-error-handling.js +15 -144
- package/dist/gates/language-adapters/csharp-adapter.d.ts +16 -0
- package/dist/gates/language-adapters/csharp-adapter.js +211 -0
- package/dist/gates/language-adapters/go-adapter.d.ts +26 -0
- package/dist/gates/language-adapters/go-adapter.js +195 -0
- package/dist/gates/language-adapters/index.d.ts +15 -0
- package/dist/gates/language-adapters/index.js +16 -0
- package/dist/gates/language-adapters/java-adapter.d.ts +16 -0
- package/dist/gates/language-adapters/java-adapter.js +237 -0
- package/dist/gates/language-adapters/js-adapter.d.ts +26 -0
- package/dist/gates/language-adapters/js-adapter.js +279 -0
- package/dist/gates/language-adapters/python-adapter.d.ts +25 -0
- package/dist/gates/language-adapters/python-adapter.js +183 -0
- package/dist/gates/language-adapters/registry.d.ts +26 -0
- package/dist/gates/language-adapters/registry.js +65 -0
- package/dist/gates/language-adapters/ruby-adapter.d.ts +25 -0
- package/dist/gates/language-adapters/ruby-adapter.js +217 -0
- package/dist/gates/language-adapters/rust-adapter.d.ts +27 -0
- package/dist/gates/language-adapters/rust-adapter.js +235 -0
- package/dist/gates/language-adapters/types.d.ts +60 -0
- package/dist/gates/language-adapters/types.js +22 -0
- package/dist/gates/logic-drift-extractors.d.ts +15 -0
- package/dist/gates/logic-drift-extractors.js +34 -0
- package/dist/gates/logic-drift.d.ts +0 -30
- package/dist/gates/logic-drift.js +39 -129
- package/dist/gates/phantom-apis.d.ts +0 -2
- package/dist/gates/phantom-apis.js +49 -20
- package/dist/gates/promise-safety.d.ts +0 -1
- package/dist/gates/promise-safety.js +14 -2
- package/dist/gates/runner.js +51 -22
- package/dist/gates/security-patterns-data.d.ts +14 -0
- package/dist/gates/security-patterns-data.js +235 -0
- package/dist/gates/security-patterns.d.ts +17 -3
- package/dist/gates/security-patterns.js +80 -211
- package/dist/gates/side-effect-analysis/categorizer.d.ts +32 -0
- package/dist/gates/side-effect-analysis/categorizer.js +83 -0
- package/dist/gates/{side-effect-analysis.d.ts → side-effect-analysis/index.d.ts} +3 -5
- package/dist/gates/{side-effect-analysis.js → side-effect-analysis/index.js} +33 -45
- package/dist/gates/side-effect-analysis/scope-tracker.d.ts +37 -0
- package/dist/gates/side-effect-analysis/scope-tracker.js +40 -0
- package/dist/gates/side-effect-helpers/index.d.ts +4 -0
- package/dist/gates/side-effect-helpers/index.js +4 -0
- package/dist/gates/side-effect-helpers/pattern-detection.d.ts +123 -0
- package/dist/gates/{side-effect-helpers.js → side-effect-helpers/pattern-detection.js} +22 -468
- package/dist/gates/side-effect-helpers/resource-tracking.d.ts +80 -0
- package/dist/gates/side-effect-helpers/resource-tracking.js +281 -0
- package/dist/gates/side-effect-helpers/scope-analysis.d.ts +21 -0
- package/dist/gates/side-effect-helpers/scope-analysis.js +146 -0
- package/dist/gates/side-effect-helpers/types.d.ts +38 -0
- package/dist/gates/side-effect-helpers/types.js +41 -0
- package/dist/gates/side-effect-rules.d.ts +0 -1
- package/dist/gates/side-effect-rules.js +0 -1
- package/dist/gates/style-drift-rules.d.ts +86 -0
- package/dist/gates/style-drift-rules.js +103 -0
- package/dist/gates/style-drift.d.ts +7 -16
- package/dist/gates/style-drift.js +101 -119
- package/dist/gates/test-quality-matchers.d.ts +53 -0
- package/dist/gates/test-quality-matchers.js +86 -0
- package/dist/gates/test-quality.d.ts +0 -3
- package/dist/gates/test-quality.js +47 -44
- package/dist/hooks/checker.d.ts +0 -1
- package/dist/hooks/checker.js +0 -2
- package/dist/hooks/dlp-templates.d.ts +0 -1
- package/dist/hooks/dlp-templates.js +0 -4
- package/dist/hooks/index.d.ts +0 -2
- package/dist/hooks/index.js +0 -2
- package/dist/hooks/input-validator.d.ts +0 -1
- package/dist/hooks/input-validator.js +0 -1
- package/dist/hooks/input-validator.test.js +0 -1
- package/dist/hooks/standalone-checker.d.ts +0 -1
- package/dist/hooks/standalone-checker.js +0 -1
- package/dist/hooks/standalone-dlp-checker.d.ts +0 -1
- package/dist/hooks/standalone-dlp-checker.js +0 -1
- package/dist/hooks/templates.d.ts +0 -1
- package/dist/hooks/templates.js +0 -1
- package/dist/hooks/types.d.ts +0 -1
- package/dist/hooks/types.js +0 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/services/adaptive-thresholds.d.ts +0 -2
- package/dist/services/adaptive-thresholds.js +0 -2
- package/dist/services/filesystem-cache.d.ts +0 -1
- package/dist/services/filesystem-cache.js +0 -1
- package/dist/services/score-history.d.ts +0 -1
- package/dist/services/score-history.js +0 -1
- package/dist/services/temporal-drift.d.ts +1 -2
- package/dist/services/temporal-drift.js +7 -8
- package/dist/storage/db.d.ts +23 -7
- package/dist/storage/db.js +116 -55
- package/dist/storage/findings.d.ts +4 -3
- package/dist/storage/findings.js +13 -20
- package/dist/storage/local-memory.d.ts +4 -4
- package/dist/storage/local-memory.js +20 -22
- package/dist/storage/patterns.d.ts +5 -5
- package/dist/storage/patterns.js +20 -26
- package/dist/storage/scans.d.ts +6 -6
- package/dist/storage/scans.js +12 -21
- package/dist/types/index.d.ts +1 -0
- package/dist/utils/scanner.js +1 -1
- package/package.json +7 -8
- package/dist/gates/duplication-drift.d.ts +0 -128
- package/dist/gates/duplication-drift.js +0 -585
- package/dist/gates/hallucinated-imports.js +0 -641
- package/dist/gates/side-effect-helpers.d.ts +0 -260
|
@@ -1,456 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Side-Effect Analysis Helpers
|
|
3
|
-
*
|
|
4
|
-
* Context-aware utilities for smart side-effect detection.
|
|
5
|
-
* Follows the same architectural patterns as promise-safety-helpers.ts:
|
|
6
|
-
* - Scope-aware analysis (brace/indent tracking)
|
|
7
|
-
* - Variable binding tracking (pair resource creation with cleanup)
|
|
8
|
-
* - Framework detection (React useEffect, Go defer, Python with, etc.)
|
|
9
|
-
* - Path overlap analysis (circular file watcher detection)
|
|
10
|
-
*
|
|
11
|
-
* These helpers make side-effect detection SMART — instead of asking
|
|
12
|
-
* "does clearInterval exist anywhere in the file?", we ask
|
|
13
|
-
* "is the specific timer variable cleaned up in the right scope?"
|
|
14
|
-
*
|
|
15
|
-
* @since v4.3.0
|
|
16
|
-
*/
|
|
17
|
-
// ── Language detection ──
|
|
18
|
-
export const LANG_MAP = {
|
|
19
|
-
'.ts': 'ts', '.tsx': 'ts', '.mts': 'ts',
|
|
20
|
-
'.js': 'js', '.jsx': 'js', '.mjs': 'js', '.cjs': 'js',
|
|
21
|
-
'.py': 'py',
|
|
22
|
-
'.go': 'go',
|
|
23
|
-
'.rs': 'rs',
|
|
24
|
-
'.cs': 'cs',
|
|
25
|
-
'.java': 'java',
|
|
26
|
-
'.rb': 'rb',
|
|
27
|
-
};
|
|
28
|
-
export const FILE_GLOBS = [
|
|
29
|
-
'**/*.{ts,tsx,mts,js,jsx,mjs,cjs}',
|
|
30
|
-
'**/*.py',
|
|
31
|
-
'**/*.go',
|
|
32
|
-
'**/*.rs',
|
|
33
|
-
'**/*.cs',
|
|
34
|
-
'**/*.java',
|
|
35
|
-
'**/*.rb',
|
|
36
|
-
];
|
|
37
|
-
// ── Strip string contents to avoid false positives in regex matching ──
|
|
38
|
-
export function stripStrings(line) {
|
|
39
|
-
return line
|
|
40
|
-
.replace(/`[^`]*`/g, '""')
|
|
41
|
-
.replace(/"(?:[^"\\]|\\.)*"/g, '""')
|
|
42
|
-
.replace(/'(?:[^'\\]|\\.)*'/g, '""');
|
|
43
|
-
}
|
|
44
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
45
|
-
// SCOPE ANALYSIS — Find function/block boundaries
|
|
46
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
47
|
-
/**
|
|
48
|
-
* Find the enclosing function scope for a given line.
|
|
49
|
-
* Returns { start, end } of the function body.
|
|
50
|
-
* For module-level code, returns { start: 0, end: lines.length }.
|
|
51
|
-
*
|
|
52
|
-
* Follows promise-safety's approach of backward scanning with brace tracking.
|
|
53
|
-
*/
|
|
54
|
-
export function findEnclosingFunction(lines, lineIdx, lang) {
|
|
55
|
-
if (lang === 'py')
|
|
56
|
-
return findEnclosingFunctionPython(lines, lineIdx);
|
|
57
|
-
if (lang === 'rb')
|
|
58
|
-
return findEnclosingFunctionRuby(lines, lineIdx);
|
|
59
|
-
return findEnclosingFunctionBrace(lines, lineIdx, lang);
|
|
60
|
-
}
|
|
61
|
-
function findEnclosingFunctionBrace(lines, lineIdx, lang) {
|
|
62
|
-
// Walk backwards tracking brace depth to find function definition
|
|
63
|
-
let braceDepth = 0;
|
|
64
|
-
const funcPatterns = getFunctionPatterns(lang);
|
|
65
|
-
for (let j = lineIdx; j >= Math.max(0, lineIdx - 200); j--) {
|
|
66
|
-
const stripped = stripStrings(lines[j]);
|
|
67
|
-
// Count braces (reverse direction: } increases, { decreases)
|
|
68
|
-
for (const ch of stripped) {
|
|
69
|
-
if (ch === '}')
|
|
70
|
-
braceDepth++;
|
|
71
|
-
if (ch === '{')
|
|
72
|
-
braceDepth--;
|
|
73
|
-
}
|
|
74
|
-
// If braceDepth < 0, we've exited the enclosing block going backwards
|
|
75
|
-
if (braceDepth < 0) {
|
|
76
|
-
// Check if this line is a function definition
|
|
77
|
-
for (const pat of funcPatterns) {
|
|
78
|
-
if (pat.test(stripped)) {
|
|
79
|
-
const end = findBlockEndBrace(lines, j);
|
|
80
|
-
return { start: j, end };
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
// It's some other block (if/for/etc), keep looking
|
|
84
|
-
braceDepth = 0;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
// Module level
|
|
88
|
-
return { start: 0, end: lines.length };
|
|
89
|
-
}
|
|
90
|
-
function findEnclosingFunctionPython(lines, lineIdx) {
|
|
91
|
-
const lineIndent = lines[lineIdx].length - lines[lineIdx].trimStart().length;
|
|
92
|
-
for (let j = lineIdx - 1; j >= 0; j--) {
|
|
93
|
-
const trimmed = lines[j].trim();
|
|
94
|
-
if (trimmed === '' || trimmed.startsWith('#'))
|
|
95
|
-
continue;
|
|
96
|
-
const indent = lines[j].length - lines[j].trimStart().length;
|
|
97
|
-
if (indent < lineIndent && /^\s*(?:async\s+)?def\s+\w+/.test(lines[j])) {
|
|
98
|
-
const end = findBlockEndIndent(lines, j);
|
|
99
|
-
return { start: j, end };
|
|
100
|
-
}
|
|
101
|
-
if (indent === 0 && /^\s*(?:class|def|async\s+def)\s/.test(lines[j])) {
|
|
102
|
-
const end = findBlockEndIndent(lines, j);
|
|
103
|
-
return { start: j, end };
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return { start: 0, end: lines.length };
|
|
107
|
-
}
|
|
108
|
-
function findEnclosingFunctionRuby(lines, lineIdx) {
|
|
109
|
-
for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - 100); j--) {
|
|
110
|
-
const trimmed = lines[j].trim();
|
|
111
|
-
if (/^def\s+\w+/.test(trimmed)) {
|
|
112
|
-
const end = findBlockEndRuby(lines, j);
|
|
113
|
-
return { start: j, end };
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return { start: 0, end: lines.length };
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Find the end of a brace-delimited block starting at `start`.
|
|
120
|
-
*/
|
|
121
|
-
export function findBlockEndBrace(lines, start) {
|
|
122
|
-
let braces = 0;
|
|
123
|
-
let started = false;
|
|
124
|
-
const maxScan = Math.min(lines.length, start + 300);
|
|
125
|
-
for (let j = start; j < maxScan; j++) {
|
|
126
|
-
const stripped = stripStrings(lines[j]);
|
|
127
|
-
for (const ch of stripped) {
|
|
128
|
-
if (ch === '{') {
|
|
129
|
-
braces++;
|
|
130
|
-
started = true;
|
|
131
|
-
}
|
|
132
|
-
if (ch === '}')
|
|
133
|
-
braces--;
|
|
134
|
-
}
|
|
135
|
-
if (started && braces <= 0)
|
|
136
|
-
return j + 1;
|
|
137
|
-
}
|
|
138
|
-
return maxScan;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Find the end of an indentation-delimited block (Python).
|
|
142
|
-
*/
|
|
143
|
-
export function findBlockEndIndent(lines, start) {
|
|
144
|
-
const baseIndent = lines[start].length - lines[start].trimStart().length;
|
|
145
|
-
const maxScan = Math.min(lines.length, start + 300);
|
|
146
|
-
for (let j = start + 1; j < maxScan; j++) {
|
|
147
|
-
const trimmed = lines[j].trim();
|
|
148
|
-
if (trimmed === '' || trimmed.startsWith('#'))
|
|
149
|
-
continue;
|
|
150
|
-
const indent = lines[j].length - lines[j].trimStart().length;
|
|
151
|
-
if (indent <= baseIndent)
|
|
152
|
-
return j;
|
|
153
|
-
}
|
|
154
|
-
return maxScan;
|
|
155
|
-
}
|
|
156
|
-
function findBlockEndRuby(lines, start) {
|
|
157
|
-
let depth = 0;
|
|
158
|
-
const maxScan = Math.min(lines.length, start + 300);
|
|
159
|
-
const openers = /\b(?:def|do|class|module|if|unless|while|until|for|begin|case)\b/;
|
|
160
|
-
for (let j = start; j < maxScan; j++) {
|
|
161
|
-
const trimmed = lines[j].trim();
|
|
162
|
-
if (openers.test(trimmed))
|
|
163
|
-
depth++;
|
|
164
|
-
if (/^\s*end\b/.test(trimmed)) {
|
|
165
|
-
depth--;
|
|
166
|
-
if (depth <= 0)
|
|
167
|
-
return j + 1;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return maxScan;
|
|
171
|
-
}
|
|
172
|
-
function getFunctionPatterns(lang) {
|
|
173
|
-
switch (lang) {
|
|
174
|
-
case 'go':
|
|
175
|
-
return [/\bfunc\s+/];
|
|
176
|
-
case 'rs':
|
|
177
|
-
return [/\bfn\s+\w+/];
|
|
178
|
-
case 'java':
|
|
179
|
-
case 'cs':
|
|
180
|
-
return [/(?:public|private|protected|static|async|void|int|string|Task|var)\s+\w+\s*\(/];
|
|
181
|
-
default: // js, ts
|
|
182
|
-
return [
|
|
183
|
-
/(?:export\s+)?(?:async\s+)?function\s+\w+/,
|
|
184
|
-
/(?:const|let|var)\s+\w+\s*=\s*(?:async\s+)?(?:\([^)]*\)|[^=])\s*=>/,
|
|
185
|
-
/\w+\s*\([^)]*\)\s*\{/, // method shorthand
|
|
186
|
-
];
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
190
|
-
// VARIABLE BINDING — Track resource creation → cleanup pairs
|
|
191
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
192
|
-
/**
|
|
193
|
-
* Extract the variable name from an assignment.
|
|
194
|
-
* "const timer = setInterval(...)" → "timer"
|
|
195
|
-
* "let fd = fs.open(...)" → "fd"
|
|
196
|
-
* "self.watcher = chokidar.watch()" → "self.watcher"
|
|
197
|
-
* "timer := time.NewTicker(...)" → "timer" (Go)
|
|
198
|
-
*
|
|
199
|
-
* Returns null if the call result is NOT stored in a variable.
|
|
200
|
-
*/
|
|
201
|
-
export function extractVariableBinding(line, lang) {
|
|
202
|
-
const stripped = stripStrings(line).trim();
|
|
203
|
-
if (lang === 'go') {
|
|
204
|
-
// Go: `ticker := time.NewTicker(...)` or `ticker, _ := ...`
|
|
205
|
-
const goMatch = stripped.match(/^(\w+)(?:\s*,\s*\w+)*\s*:?=\s*/);
|
|
206
|
-
if (goMatch)
|
|
207
|
-
return goMatch[1];
|
|
208
|
-
return null;
|
|
209
|
-
}
|
|
210
|
-
if (lang === 'py') {
|
|
211
|
-
// Python: `timer = threading.Timer(...)` or `self.timer = ...`
|
|
212
|
-
const pyMatch = stripped.match(/^((?:self\.)?[\w.]+)\s*=\s*(?!==)/);
|
|
213
|
-
if (pyMatch)
|
|
214
|
-
return pyMatch[1];
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
if (lang === 'rb') {
|
|
218
|
-
// Ruby: `@watcher = Listen.to(...)` or `watcher = ...`
|
|
219
|
-
const rbMatch = stripped.match(/^(@?\w+)\s*=\s*/);
|
|
220
|
-
if (rbMatch)
|
|
221
|
-
return rbMatch[1];
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
// JS/TS/Java/C#/Rust: `const x = ...`, `let x = ...`, `var x = ...`, `auto x = ...`
|
|
225
|
-
const jsMatch = stripped.match(/^(?:const|let|var|final|auto|val)\s+(\w+)\s*=\s*/);
|
|
226
|
-
if (jsMatch)
|
|
227
|
-
return jsMatch[1];
|
|
228
|
-
// Member assignment: `this.timer = ...`, `self.timer = ...`
|
|
229
|
-
const memberMatch = stripped.match(/^(?:this|self)\.([\w]+)\s*=\s*/);
|
|
230
|
-
if (memberMatch)
|
|
231
|
-
return memberMatch[1];
|
|
232
|
-
// Simple assignment: `timer = ...`
|
|
233
|
-
const simpleMatch = stripped.match(/^(\w+)\s*=\s*(?!==)/);
|
|
234
|
-
if (simpleMatch) {
|
|
235
|
-
// Exclude control flow keywords
|
|
236
|
-
const name = simpleMatch[1];
|
|
237
|
-
if (['if', 'for', 'while', 'switch', 'return', 'throw'].includes(name))
|
|
238
|
-
return null;
|
|
239
|
-
return name;
|
|
240
|
-
}
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Check if a specific variable is used in a cleanup call within a scope.
|
|
245
|
-
*
|
|
246
|
-
* Unlike the naive "does clearInterval exist in the file?", this checks:
|
|
247
|
-
* 1. The cleanup function references the specific variable
|
|
248
|
-
* 2. The cleanup is within the correct scope (same function or cleanup callback)
|
|
249
|
-
*
|
|
250
|
-
* Example: for variable "timer" and cleanup patterns [/clearInterval/],
|
|
251
|
-
* matches: `clearInterval(timer)`, `clearInterval(this.timer)`, `timer.close()`
|
|
252
|
-
*/
|
|
253
|
-
export function hasCleanupForVariable(lines, varName, scopeStart, scopeEnd, cleanupPatterns, lang) {
|
|
254
|
-
const scope = lines.slice(scopeStart, scopeEnd);
|
|
255
|
-
for (let i = 0; i < scope.length; i++) {
|
|
256
|
-
const stripped = stripStrings(scope[i]);
|
|
257
|
-
// Check cleanup patterns that reference the specific variable
|
|
258
|
-
for (const pat of cleanupPatterns) {
|
|
259
|
-
if (!pat.test(stripped))
|
|
260
|
-
continue;
|
|
261
|
-
// The cleanup call should reference our variable
|
|
262
|
-
const escapedVar = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
263
|
-
const varRef = new RegExp(`\\b${escapedVar}\\b`);
|
|
264
|
-
if (varRef.test(stripped))
|
|
265
|
-
return true;
|
|
266
|
-
// Also check method calls on the variable: timer.close(), timer.stop()
|
|
267
|
-
// The pattern might match a generic .close() — check if it's on our var
|
|
268
|
-
}
|
|
269
|
-
// Direct method cleanup on the variable: varName.close(), varName.Stop()
|
|
270
|
-
const escapedVar = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
271
|
-
const methodCleanup = new RegExp(`\\b${escapedVar}\\.(?:close|stop|destroy|kill|terminate|dispose|cancel|shutdown|unsubscribe|disconnect|end|release|Clear|Stop|Dispose|Close|Cancel)\\s*\\(`);
|
|
272
|
-
if (methodCleanup.test(stripped))
|
|
273
|
-
return true;
|
|
274
|
-
}
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Check if a line is inside a cleanup/teardown context.
|
|
279
|
-
*
|
|
280
|
-
* Cleanup contexts where resource cleanup is expected:
|
|
281
|
-
* - JS/TS: useEffect return function, componentWillUnmount, beforeDestroy, ngOnDestroy, dispose()
|
|
282
|
-
* - Python: __del__, __exit__, close(), cleanup(), teardown
|
|
283
|
-
* - Go: defer statement
|
|
284
|
-
* - Java: finally block, close() method, @PreDestroy
|
|
285
|
-
* - C#: Dispose(), using block, finalizer
|
|
286
|
-
* - Ruby: ensure block, at_exit
|
|
287
|
-
*/
|
|
288
|
-
export function isInsideCleanupContext(lines, lineIdx, lang) {
|
|
289
|
-
// Scan backwards up to 30 lines for cleanup context markers
|
|
290
|
-
for (let j = lineIdx; j >= Math.max(0, lineIdx - 30); j--) {
|
|
291
|
-
const trimmed = lines[j].trim();
|
|
292
|
-
switch (lang) {
|
|
293
|
-
case 'js':
|
|
294
|
-
case 'ts':
|
|
295
|
-
// React useEffect cleanup: `return () => { cleanup }`
|
|
296
|
-
if (/\breturn\s+(?:\(\)\s*=>|function\s*\()/.test(trimmed))
|
|
297
|
-
return true;
|
|
298
|
-
// Lifecycle: componentWillUnmount, ngOnDestroy, beforeDestroy
|
|
299
|
-
if (/\b(?:componentWillUnmount|ngOnDestroy|beforeDestroy|dispose)\s*\(/.test(trimmed))
|
|
300
|
-
return true;
|
|
301
|
-
// Event: 'beforeunload', 'unload'
|
|
302
|
-
if (/['"](?:beforeunload|unload)['"]\s*,/.test(trimmed))
|
|
303
|
-
return true;
|
|
304
|
-
break;
|
|
305
|
-
case 'py':
|
|
306
|
-
if (/\bdef\s+(?:__del__|__exit__|close|cleanup|teardown|dispose)\s*\(/.test(trimmed))
|
|
307
|
-
return true;
|
|
308
|
-
if (/\bfinally\s*:/.test(trimmed))
|
|
309
|
-
return true;
|
|
310
|
-
break;
|
|
311
|
-
case 'go':
|
|
312
|
-
if (/\bdefer\b/.test(trimmed))
|
|
313
|
-
return true;
|
|
314
|
-
break;
|
|
315
|
-
case 'java':
|
|
316
|
-
if (/\bfinally\s*\{/.test(trimmed))
|
|
317
|
-
return true;
|
|
318
|
-
if (/\b(?:close|destroy|cleanup|dispose)\s*\(/.test(trimmed))
|
|
319
|
-
return true;
|
|
320
|
-
if (/@PreDestroy/.test(trimmed))
|
|
321
|
-
return true;
|
|
322
|
-
break;
|
|
323
|
-
case 'cs':
|
|
324
|
-
if (/\bDispose\s*\(/.test(trimmed))
|
|
325
|
-
return true;
|
|
326
|
-
if (/\busing\s*\(/.test(trimmed))
|
|
327
|
-
return true;
|
|
328
|
-
if (/~\w+\s*\(/.test(trimmed))
|
|
329
|
-
return true; // finalizer
|
|
330
|
-
break;
|
|
331
|
-
case 'rb':
|
|
332
|
-
if (/\bensure\b/.test(trimmed))
|
|
333
|
-
return true;
|
|
334
|
-
break;
|
|
335
|
-
case 'rs':
|
|
336
|
-
if (/\bimpl\s+Drop\b/.test(trimmed))
|
|
337
|
-
return true;
|
|
338
|
-
break;
|
|
339
|
-
}
|
|
340
|
-
// Stop at function boundaries
|
|
341
|
-
if (isFunctionBoundary(trimmed, lang))
|
|
342
|
-
break;
|
|
343
|
-
}
|
|
344
|
-
return false;
|
|
345
|
-
}
|
|
346
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
347
|
-
// FRAMEWORK-AWARE PATTERNS — Detect safe idioms per ecosystem
|
|
348
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
349
|
-
/**
|
|
350
|
-
* Check if a timer/resource creation is inside a React useEffect
|
|
351
|
-
* that returns a cleanup function.
|
|
352
|
-
*
|
|
353
|
-
* Pattern:
|
|
354
|
-
* useEffect(() => {
|
|
355
|
-
* const timer = setInterval(...) ← creation
|
|
356
|
-
* return () => clearInterval(timer) ← cleanup
|
|
357
|
-
* }, [deps])
|
|
358
|
-
*/
|
|
359
|
-
export function isInUseEffectWithCleanup(lines, lineIdx) {
|
|
360
|
-
// Walk backwards to find useEffect
|
|
361
|
-
let braceDepth = 0;
|
|
362
|
-
for (let j = lineIdx; j >= Math.max(0, lineIdx - 30); j--) {
|
|
363
|
-
const stripped = stripStrings(lines[j]);
|
|
364
|
-
for (const ch of stripped) {
|
|
365
|
-
if (ch === '}')
|
|
366
|
-
braceDepth++;
|
|
367
|
-
if (ch === '{')
|
|
368
|
-
braceDepth--;
|
|
369
|
-
}
|
|
370
|
-
if (/\buseEffect\s*\(/.test(stripped) && braceDepth <= 0) {
|
|
371
|
-
// Found enclosing useEffect — now check if it has a return () => ...
|
|
372
|
-
const effectEnd = findBlockEndBrace(lines, j);
|
|
373
|
-
const effectBody = lines.slice(j, effectEnd).join('\n');
|
|
374
|
-
// Look for cleanup return: `return () =>` or `return function`
|
|
375
|
-
if (/\breturn\s+(?:\(\)\s*=>|function\s*\()/.test(effectBody)) {
|
|
376
|
-
return true;
|
|
377
|
-
}
|
|
378
|
-
return false;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
return false;
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* Check if a Go resource open is immediately followed by defer close.
|
|
385
|
-
*
|
|
386
|
-
* Idiomatic Go:
|
|
387
|
-
* f, err := os.Open(path)
|
|
388
|
-
* if err != nil { return err }
|
|
389
|
-
* defer f.Close()
|
|
390
|
-
*/
|
|
391
|
-
export function hasGoDefer(lines, openLine, varName) {
|
|
392
|
-
// Check lines between open and open+5 for defer using the variable
|
|
393
|
-
const escaped = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
394
|
-
const deferPat = new RegExp(`\\bdefer\\s+${escaped}\\.`);
|
|
395
|
-
for (let j = openLine + 1; j < Math.min(lines.length, openLine + 6); j++) {
|
|
396
|
-
if (deferPat.test(lines[j]))
|
|
397
|
-
return true;
|
|
398
|
-
// Also match: defer func() { varName.Close() }()
|
|
399
|
-
if (/\bdefer\s+func\s*\(\)/.test(lines[j])) {
|
|
400
|
-
const endDefer = findBlockEndBrace(lines, j);
|
|
401
|
-
const body = lines.slice(j, endDefer).join('\n');
|
|
402
|
-
if (new RegExp(`\\b${escaped}\\.`).test(body))
|
|
403
|
-
return true;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
return false;
|
|
407
|
-
}
|
|
408
|
-
/**
|
|
409
|
-
* Check if a Python open() is inside a `with` statement (context manager).
|
|
410
|
-
*/
|
|
411
|
-
export function isPythonWithStatement(line) {
|
|
412
|
-
return /\bwith\s+/.test(stripStrings(line));
|
|
413
|
-
}
|
|
414
|
-
/**
|
|
415
|
-
* Check if a Java resource open is inside try-with-resources.
|
|
416
|
-
* Pattern: try (var x = new FileStream(...)) { ... }
|
|
417
|
-
*/
|
|
418
|
-
export function isJavaTryWithResources(lines, lineIdx) {
|
|
419
|
-
for (let j = lineIdx; j >= Math.max(0, lineIdx - 3); j--) {
|
|
420
|
-
if (/\btry\s*\(/.test(stripStrings(lines[j])))
|
|
421
|
-
return true;
|
|
422
|
-
}
|
|
423
|
-
return false;
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Check if a C# resource is inside a using statement/declaration.
|
|
427
|
-
* Patterns: `using (var x = ...)` or `using var x = ...` (C# 8+)
|
|
428
|
-
*/
|
|
429
|
-
export function isCSharpUsing(line) {
|
|
430
|
-
const stripped = stripStrings(line);
|
|
431
|
-
return /\busing\s*\(/.test(stripped) || /\busing\s+(?:var|await)\b/.test(stripped);
|
|
432
|
-
}
|
|
433
|
-
/**
|
|
434
|
-
* Check if a Ruby File.open uses block form (auto-closes).
|
|
435
|
-
* Pattern: File.open(path) do |f| ... end
|
|
436
|
-
* File.open(path) { |f| ... }
|
|
437
|
-
*/
|
|
438
|
-
export function isRubyBlockForm(line) {
|
|
439
|
-
return /\bdo\s*\|/.test(line) || /\{\s*\|/.test(line);
|
|
440
|
-
}
|
|
441
|
-
/**
|
|
442
|
-
* Check if a Rust resource is automatically dropped (RAII).
|
|
443
|
-
* In Rust, all resources are dropped when they go out of scope,
|
|
444
|
-
* so we only flag resources in unsafe blocks or static/global context.
|
|
445
|
-
*/
|
|
446
|
-
export function isRustAutoDropped(lines, lineIdx) {
|
|
447
|
-
// Check if inside unsafe block (manual memory management)
|
|
448
|
-
for (let j = lineIdx; j >= Math.max(0, lineIdx - 20); j--) {
|
|
449
|
-
if (/\bunsafe\s*\{/.test(lines[j]))
|
|
450
|
-
return false; // Not auto-dropped in unsafe
|
|
451
|
-
}
|
|
452
|
-
return true; // Normal Rust = RAII applies
|
|
453
|
-
}
|
|
1
|
+
import { stripStrings } from './types.js';
|
|
454
2
|
// ═══════════════════════════════════════════════════════════════════
|
|
455
3
|
// CIRCULAR TRIGGER DETECTION — Path overlap analysis
|
|
456
4
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -523,6 +71,7 @@ export function pathsOverlap(watchPath, writePath) {
|
|
|
523
71
|
// ═══════════════════════════════════════════════════════════════════
|
|
524
72
|
// LOOP & RECURSION ANALYSIS — Context-aware body extraction
|
|
525
73
|
// ═══════════════════════════════════════════════════════════════════
|
|
74
|
+
import { findBlockEndBrace, findBlockEndIndent } from './scope-analysis.js';
|
|
526
75
|
/**
|
|
527
76
|
* Extract loop body with correct scope tracking.
|
|
528
77
|
* Uses brace/indent matching (not just "next N lines").
|
|
@@ -536,6 +85,22 @@ export function extractLoopBody(lines, loopLine, lang) {
|
|
|
536
85
|
const body = lines.slice(loopLine, end).join('\n');
|
|
537
86
|
return { body, start: loopLine, end };
|
|
538
87
|
}
|
|
88
|
+
function findBlockEndRuby(lines, start) {
|
|
89
|
+
let depth = 0;
|
|
90
|
+
const maxScan = Math.min(lines.length, start + 300);
|
|
91
|
+
const openers = /\b(?:def|do|class|module|if|unless|while|until|for|begin|case)\b/;
|
|
92
|
+
for (let j = start; j < maxScan; j++) {
|
|
93
|
+
const trimmed = lines[j].trim();
|
|
94
|
+
if (openers.test(trimmed))
|
|
95
|
+
depth++;
|
|
96
|
+
if (/^\s*end\b/.test(trimmed)) {
|
|
97
|
+
depth--;
|
|
98
|
+
if (depth <= 0)
|
|
99
|
+
return j + 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return maxScan;
|
|
103
|
+
}
|
|
539
104
|
/**
|
|
540
105
|
* Extract all function definitions with their bodies.
|
|
541
106
|
* Used for recursion detection — need to check if function calls itself
|
|
@@ -730,22 +295,8 @@ export function hasCatchWithContinue(body, lang) {
|
|
|
730
295
|
return /\bcatch\b[\s\S]*?\bcontinue\b/.test(body);
|
|
731
296
|
}
|
|
732
297
|
// ═══════════════════════════════════════════════════════════════════
|
|
733
|
-
//
|
|
298
|
+
// PROCESS & TIMER OPERATIONS
|
|
734
299
|
// ═══════════════════════════════════════════════════════════════════
|
|
735
|
-
function isFunctionBoundary(trimmed, lang) {
|
|
736
|
-
switch (lang) {
|
|
737
|
-
case 'py':
|
|
738
|
-
return /^(?:def|class|async\s+def)\s/.test(trimmed);
|
|
739
|
-
case 'go':
|
|
740
|
-
return /^func\s/.test(trimmed);
|
|
741
|
-
case 'rb':
|
|
742
|
-
return /^(?:def|class|module)\s/.test(trimmed);
|
|
743
|
-
case 'rs':
|
|
744
|
-
return /^(?:fn|impl|pub\s+fn|pub\s+async\s+fn)\s/.test(trimmed);
|
|
745
|
-
default:
|
|
746
|
-
return /^(?:export\s+)?(?:async\s+)?(?:function|class)\s/.test(trimmed);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
300
|
/**
|
|
750
301
|
* Check if a line contains a process spawn call.
|
|
751
302
|
*/
|
|
@@ -879,6 +430,9 @@ export function getProcessCleanupPatterns(lang) {
|
|
|
879
430
|
return [];
|
|
880
431
|
}
|
|
881
432
|
}
|
|
433
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
434
|
+
// LOOP PATTERNS & RESOURCE DETECTION
|
|
435
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
882
436
|
/**
|
|
883
437
|
* Check if a line contains an unbounded loop construct.
|
|
884
438
|
*/
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { SideEffectLang } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Extract the variable name from an assignment.
|
|
4
|
+
* "const timer = setInterval(...)" → "timer"
|
|
5
|
+
* "let fd = fs.open(...)" → "fd"
|
|
6
|
+
* "self.watcher = chokidar.watch()" → "self.watcher"
|
|
7
|
+
* "timer := time.NewTicker(...)" → "timer" (Go)
|
|
8
|
+
*
|
|
9
|
+
* Returns null if the call result is NOT stored in a variable.
|
|
10
|
+
*/
|
|
11
|
+
export declare function extractVariableBinding(line: string, lang: SideEffectLang): string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Check if a specific variable is used in a cleanup call within a scope.
|
|
14
|
+
*
|
|
15
|
+
* Unlike the naive "does clearInterval exist in the file?", this checks:
|
|
16
|
+
* 1. The cleanup function references the specific variable
|
|
17
|
+
* 2. The cleanup is within the correct scope (same function or cleanup callback)
|
|
18
|
+
*
|
|
19
|
+
* Example: for variable "timer" and cleanup patterns [/clearInterval/],
|
|
20
|
+
* matches: `clearInterval(timer)`, `clearInterval(this.timer)`, `timer.close()`
|
|
21
|
+
*/
|
|
22
|
+
export declare function hasCleanupForVariable(lines: string[], varName: string, scopeStart: number, scopeEnd: number, cleanupPatterns: RegExp[], lang: SideEffectLang): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Check if a line is inside a cleanup/teardown context.
|
|
25
|
+
*
|
|
26
|
+
* Cleanup contexts where resource cleanup is expected:
|
|
27
|
+
* - JS/TS: useEffect return function, componentWillUnmount, beforeDestroy, ngOnDestroy, dispose()
|
|
28
|
+
* - Python: __del__, __exit__, close(), cleanup(), teardown
|
|
29
|
+
* - Go: defer statement
|
|
30
|
+
* - Java: finally block, close() method, @PreDestroy
|
|
31
|
+
* - C#: Dispose(), using block, finalizer
|
|
32
|
+
* - Ruby: ensure block, at_exit
|
|
33
|
+
*/
|
|
34
|
+
export declare function isInsideCleanupContext(lines: string[], lineIdx: number, lang: SideEffectLang): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Check if a timer/resource creation is inside a React useEffect
|
|
37
|
+
* that returns a cleanup function.
|
|
38
|
+
*
|
|
39
|
+
* Pattern:
|
|
40
|
+
* useEffect(() => {
|
|
41
|
+
* const timer = setInterval(...) ← creation
|
|
42
|
+
* return () => clearInterval(timer) ← cleanup
|
|
43
|
+
* }, [deps])
|
|
44
|
+
*/
|
|
45
|
+
export declare function isInUseEffectWithCleanup(lines: string[], lineIdx: number): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Check if a Go resource open is immediately followed by defer close.
|
|
48
|
+
*
|
|
49
|
+
* Idiomatic Go:
|
|
50
|
+
* f, err := os.Open(path)
|
|
51
|
+
* if err != nil { return err }
|
|
52
|
+
* defer f.Close()
|
|
53
|
+
*/
|
|
54
|
+
export declare function hasGoDefer(lines: string[], openLine: number, varName: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Check if a Python open() is inside a `with` statement (context manager).
|
|
57
|
+
*/
|
|
58
|
+
export declare function isPythonWithStatement(line: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Check if a Java resource open is inside try-with-resources.
|
|
61
|
+
* Pattern: try (var x = new FileStream(...)) { ... }
|
|
62
|
+
*/
|
|
63
|
+
export declare function isJavaTryWithResources(lines: string[], lineIdx: number): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Check if a C# resource is inside a using statement/declaration.
|
|
66
|
+
* Patterns: `using (var x = ...)` or `using var x = ...` (C# 8+)
|
|
67
|
+
*/
|
|
68
|
+
export declare function isCSharpUsing(line: string): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Check if a Ruby File.open uses block form (auto-closes).
|
|
71
|
+
* Pattern: File.open(path) do |f| ... end
|
|
72
|
+
* File.open(path) { |f| ... }
|
|
73
|
+
*/
|
|
74
|
+
export declare function isRubyBlockForm(line: string): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Check if a Rust resource is automatically dropped (RAII).
|
|
77
|
+
* In Rust, all resources are dropped when they go out of scope,
|
|
78
|
+
* so we only flag resources in unsafe blocks or static/global context.
|
|
79
|
+
*/
|
|
80
|
+
export declare function isRustAutoDropped(lines: string[], lineIdx: number): boolean;
|