@rigour-labs/core 6.2.4 → 6.3.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 +2 -0
- package/dist/deep/code-context.d.ts +28 -0
- package/dist/deep/code-context.js +93 -0
- package/dist/deep/code-context.test.d.ts +1 -0
- package/dist/deep/code-context.test.js +48 -0
- package/dist/deep/code-pass.d.ts +15 -0
- package/dist/deep/code-pass.js +44 -0
- package/dist/deep/code-review-prompt.d.ts +17 -0
- package/dist/deep/code-review-prompt.js +30 -0
- package/dist/deep/code-review-prompt.test.d.ts +1 -0
- package/dist/deep/code-review-prompt.test.js +20 -0
- package/dist/deep/code-verifier.d.ts +19 -0
- package/dist/deep/code-verifier.js +48 -0
- package/dist/deep/code-verifier.test.d.ts +1 -0
- package/dist/deep/code-verifier.test.js +46 -0
- package/dist/deep/fact-extractor.d.ts +7 -1
- package/dist/deep/fact-extractor.js +16 -8
- package/dist/deep/fact-extractor.test.js +34 -1
- package/dist/deep/facts-pass.d.ts +24 -0
- package/dist/deep/facts-pass.js +69 -0
- package/dist/deep/index.d.ts +6 -1
- package/dist/deep/index.js +5 -1
- package/dist/deep/parse-findings.d.ts +12 -0
- package/dist/deep/parse-findings.js +73 -0
- package/dist/deep/parse-findings.test.d.ts +1 -0
- package/dist/deep/parse-findings.test.js +25 -0
- package/dist/deep/prompts.d.ts +1 -1
- package/dist/deep/prompts.js +7 -8
- package/dist/deep/prompts.test.js +15 -0
- package/dist/discovery-signals.d.ts +27 -0
- package/dist/discovery-signals.js +96 -0
- package/dist/discovery.d.ts +1 -2
- package/dist/discovery.js +36 -49
- package/dist/discovery.test.js +84 -71
- package/dist/gates/context.js +7 -0
- package/dist/gates/deep-analysis.d.ts +33 -8
- package/dist/gates/deep-analysis.js +169 -306
- package/dist/gates/deep-analysis.test.d.ts +1 -0
- package/dist/gates/deep-analysis.test.js +86 -0
- package/dist/gates/deep-intent.d.ts +3 -0
- package/dist/gates/deep-intent.js +57 -0
- package/dist/gates/deep-intent.test.d.ts +1 -0
- package/dist/gates/deep-intent.test.js +51 -0
- package/dist/gates/deep-runner.d.ts +15 -0
- package/dist/gates/deep-runner.js +66 -0
- package/dist/gates/dependency.js +4 -0
- package/dist/gates/deprecated-apis-ast.d.ts +8 -0
- package/dist/gates/deprecated-apis-ast.js +25 -0
- package/dist/gates/deprecated-apis-rules-node.js +2 -8
- package/dist/gates/deprecated-apis.js +5 -1
- package/dist/gates/deprecated-apis.test.js +30 -0
- package/dist/gates/frontend-secret-exposure.js +5 -1
- package/dist/gates/frontend-secret-exposure.test.js +20 -0
- package/dist/gates/hallucinated-imports/framework-modules.d.ts +14 -0
- package/dist/gates/hallucinated-imports/framework-modules.js +75 -0
- package/dist/gates/hallucinated-imports/index.js +14 -6
- package/dist/gates/hallucinated-imports/js-resolver.d.ts +16 -12
- package/dist/gates/hallucinated-imports/js-resolver.js +65 -153
- package/dist/gates/hallucinated-imports/ts-path-target.js +16 -0
- package/dist/gates/hallucinated-imports/tsconfig-paths.d.ts +16 -0
- package/dist/gates/hallucinated-imports/tsconfig-paths.js +120 -0
- package/dist/gates/hallucinated-imports-sveltekit.test.d.ts +1 -0
- package/dist/gates/hallucinated-imports-sveltekit.test.js +108 -0
- package/dist/gates/logic-drift.js +7 -1
- package/dist/gates/runner.js +10 -40
- package/dist/gates/runner.test.js +37 -25
- package/dist/gates/scoped-gates.test.d.ts +1 -0
- package/dist/gates/scoped-gates.test.js +52 -0
- package/dist/gates/semantic-bugs.d.ts +23 -0
- package/dist/gates/semantic-bugs.js +48 -0
- package/dist/gates/semantic-bugs.test.d.ts +1 -0
- package/dist/gates/semantic-bugs.test.js +70 -0
- package/dist/gates/side-effect-analysis/index.js +11 -1
- package/dist/gates/side-effect-analysis.test.d.ts +1 -0
- package/dist/gates/side-effect-analysis.test.js +162 -0
- package/dist/gates/side-effect-helpers/pattern-detection.js +35 -7
- package/dist/gates/side-effect-helpers/resource-tracking.js +17 -10
- package/dist/gates/side-effect-helpers/scope-analysis.d.ts +6 -0
- package/dist/gates/side-effect-helpers/scope-analysis.js +20 -0
- package/dist/gates/style-drift.js +3 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.js +10 -2
- package/dist/inference/http-download.d.ts +22 -0
- package/dist/inference/http-download.js +132 -0
- package/dist/inference/http-download.test.d.ts +1 -0
- package/dist/inference/http-download.test.js +109 -0
- package/dist/inference/index.d.ts +5 -1
- package/dist/inference/index.js +2 -1
- package/dist/inference/llama-engine.d.ts +27 -0
- package/dist/inference/llama-engine.js +152 -0
- package/dist/inference/llama-engine.test.d.ts +1 -0
- package/dist/inference/llama-engine.test.js +51 -0
- package/dist/inference/llama-process.d.ts +72 -0
- package/dist/inference/llama-process.js +126 -0
- package/dist/inference/llama-process.test.d.ts +1 -0
- package/dist/inference/llama-process.test.js +61 -0
- package/dist/inference/model-download.test.d.ts +1 -0
- package/dist/inference/model-download.test.js +73 -0
- package/dist/inference/model-manager.d.ts +14 -1
- package/dist/inference/model-manager.js +65 -80
- package/dist/inference/sidecar-provider.d.ts +19 -10
- package/dist/inference/sidecar-provider.js +125 -342
- package/dist/inference/types.d.ts +2 -0
- package/dist/semantic/ast.d.ts +29 -0
- package/dist/semantic/ast.js +122 -0
- package/dist/semantic/benchmark.d.ts +16 -0
- package/dist/semantic/benchmark.js +31 -0
- package/dist/semantic/benchmark.test.d.ts +1 -0
- package/dist/semantic/benchmark.test.js +18 -0
- package/dist/semantic/engine.d.ts +8 -0
- package/dist/semantic/engine.js +45 -0
- package/dist/semantic/intent/benchmark.d.ts +31 -0
- package/dist/semantic/intent/benchmark.js +74 -0
- package/dist/semantic/intent/intent-check.d.ts +43 -0
- package/dist/semantic/intent/intent-check.js +72 -0
- package/dist/semantic/intent/intent.test.d.ts +1 -0
- package/dist/semantic/intent/intent.test.js +46 -0
- package/dist/semantic/intent/parallel-reads.d.ts +23 -0
- package/dist/semantic/intent/parallel-reads.js +59 -0
- package/dist/semantic/learn/benchmark.d.ts +20 -0
- package/dist/semantic/learn/benchmark.js +71 -0
- package/dist/semantic/learn/compile.d.ts +4 -0
- package/dist/semantic/learn/compile.js +56 -0
- package/dist/semantic/learn/conditions.d.ts +22 -0
- package/dist/semantic/learn/conditions.js +93 -0
- package/dist/semantic/learn/extract.d.ts +30 -0
- package/dist/semantic/learn/extract.js +122 -0
- package/dist/semantic/learn/git-trees.d.ts +8 -0
- package/dist/semantic/learn/git-trees.js +32 -0
- package/dist/semantic/learn/identity.d.ts +18 -0
- package/dist/semantic/learn/identity.js +97 -0
- package/dist/semantic/learn/learn.d.ts +25 -0
- package/dist/semantic/learn/learn.js +109 -0
- package/dist/semantic/learn/learn.test.d.ts +1 -0
- package/dist/semantic/learn/learn.test.js +67 -0
- package/dist/semantic/learn/store.d.ts +4 -0
- package/dist/semantic/learn/store.js +39 -0
- package/dist/semantic/learn/types.d.ts +468 -0
- package/dist/semantic/learn/types.js +55 -0
- package/dist/semantic/origins.d.ts +42 -0
- package/dist/semantic/origins.js +149 -0
- package/dist/semantic/origins.test.d.ts +1 -0
- package/dist/semantic/origins.test.js +77 -0
- package/dist/semantic/program.d.ts +13 -0
- package/dist/semantic/program.js +62 -0
- package/dist/semantic/rules/credential-redirect.d.ts +2 -0
- package/dist/semantic/rules/credential-redirect.js +209 -0
- package/dist/semantic/rules/degraded-response-cached.d.ts +2 -0
- package/dist/semantic/rules/degraded-response-cached.js +271 -0
- package/dist/semantic/rules/in-memory-aggregation.d.ts +2 -0
- package/dist/semantic/rules/in-memory-aggregation.js +179 -0
- package/dist/semantic/summaries.d.ts +18 -0
- package/dist/semantic/summaries.js +33 -0
- package/dist/semantic/types.d.ts +32 -0
- package/dist/semantic/types.js +1 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/team-diagnostics.d.ts +22 -0
- package/dist/storage/team-diagnostics.js +58 -0
- package/dist/storage/team-diagnostics.test.d.ts +1 -0
- package/dist/storage/team-diagnostics.test.js +51 -0
- package/dist/storage/team-schema.d.ts +6 -0
- package/dist/storage/team-schema.js +82 -0
- package/dist/storage/team-store.js +25 -72
- package/dist/templates/universal-config.js +4 -2
- package/dist/types/index.d.ts +114 -14
- package/dist/types/index.js +16 -2
- package/dist/utils/deep-status.d.ts +7 -0
- package/dist/utils/deep-status.js +11 -0
- package/dist/utils/diff.d.ts +3 -0
- package/dist/utils/diff.js +52 -0
- package/dist/utils/diff.test.d.ts +1 -0
- package/dist/utils/diff.test.js +38 -0
- package/dist/utils/scanner.js +13 -2
- package/dist/utils/scanner.test.js +22 -8
- package/dist/utils/scope.d.ts +15 -0
- package/dist/utils/scope.js +51 -0
- package/dist/utils/scope.test.d.ts +1 -0
- package/dist/utils/scope.test.js +38 -0
- package/package.json +6 -6
package/dist/gates/runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SEVERITY_WEIGHTS } from '../types/index.js';
|
|
2
|
-
import {
|
|
2
|
+
import { runDeepAnalysis } from './deep-runner.js';
|
|
3
3
|
import { persistAndReinforce } from '../storage/local-memory.js';
|
|
4
4
|
import { recordGateRun } from '../services/adaptive-thresholds.js';
|
|
5
5
|
import { FileGate } from './file.js';
|
|
@@ -27,6 +27,7 @@ import { DeprecatedApisGate } from './deprecated-apis.js';
|
|
|
27
27
|
import { TestQualityGate } from './test-quality.js';
|
|
28
28
|
import { SideEffectAnalysisGate } from './side-effect-analysis/index.js';
|
|
29
29
|
import { StyleDriftGate } from './style-drift.js';
|
|
30
|
+
import { SemanticBugsGate } from './semantic-bugs.js';
|
|
30
31
|
import { LogicDriftGate } from './logic-drift.js';
|
|
31
32
|
import { execa } from 'execa';
|
|
32
33
|
import { Logger } from '../utils/logger.js';
|
|
@@ -68,6 +69,10 @@ export class GateRunner {
|
|
|
68
69
|
if (this.config.gates.checkpoint?.enabled) {
|
|
69
70
|
this.gates.push(new CheckpointGate(this.config.gates.checkpoint));
|
|
70
71
|
}
|
|
72
|
+
// Semantic Bugs Gate (type-aware, proven findings) — opt-in until field-validated
|
|
73
|
+
if (this.config.gates.semantic_bugs?.enabled) {
|
|
74
|
+
this.gates.push(new SemanticBugsGate(this.config.gates.semantic_bugs));
|
|
75
|
+
}
|
|
71
76
|
// Security Patterns Gate (code-level vulnerability detection) — enabled by default since v2.15
|
|
72
77
|
if (this.config.gates.security?.enabled !== false) {
|
|
73
78
|
this.gates.push(new SecurityPatternsGate(this.config.gates.security));
|
|
@@ -200,45 +205,10 @@ export class GateRunner {
|
|
|
200
205
|
// 3. Run Deep Analysis (if enabled)
|
|
201
206
|
let deepStats = undefined;
|
|
202
207
|
if (deepOptions?.enabled) {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
threads: this.config.gates.deep?.threads,
|
|
208
|
-
maxTokens: this.config.gates.deep?.max_tokens,
|
|
209
|
-
temperature: this.config.gates.deep?.temperature,
|
|
210
|
-
timeoutMs: this.config.gates.deep?.timeout_ms,
|
|
211
|
-
onProgress: deepOptions.onProgress,
|
|
212
|
-
});
|
|
213
|
-
try {
|
|
214
|
-
const deepFailures = await deepGate.run({ cwd, ignore, patterns });
|
|
215
|
-
if (deepFailures.length > 0) {
|
|
216
|
-
failures.push(...deepFailures);
|
|
217
|
-
summary['deep-analysis'] = 'FAIL';
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
summary['deep-analysis'] = 'PASS';
|
|
221
|
-
}
|
|
222
|
-
const isLocalDeepExecution = !deepOptions.apiKey || (deepOptions.provider || '').toLowerCase() === 'local';
|
|
223
|
-
const deepTier = isLocalDeepExecution
|
|
224
|
-
? (deepOptions.pro ? 'deep' : 'lite')
|
|
225
|
-
: 'cloud';
|
|
226
|
-
deepStats = {
|
|
227
|
-
enabled: true,
|
|
228
|
-
tier: deepTier,
|
|
229
|
-
model: isLocalDeepExecution
|
|
230
|
-
? (deepOptions.pro ? 'Qwen2.5-Coder-1.5B' : 'Qwen2.5-Coder-0.5B')
|
|
231
|
-
: (deepOptions.modelName || deepOptions.provider || 'cloud'),
|
|
232
|
-
total_ms: Date.now() - deepSetupStart,
|
|
233
|
-
findings_count: deepFailures.length,
|
|
234
|
-
findings_verified: deepFailures.filter((f) => f.verified).length,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
catch (error) {
|
|
238
|
-
Logger.error(`Deep analysis failed: ${error.message}`);
|
|
239
|
-
summary['deep-analysis'] = 'ERROR';
|
|
240
|
-
deepStats = { enabled: true };
|
|
241
|
-
}
|
|
208
|
+
const deep = await runDeepAnalysis(this.config, { cwd, ignore, patterns }, deepOptions);
|
|
209
|
+
failures.push(...deep.failures);
|
|
210
|
+
summary['deep-analysis'] = deep.summary;
|
|
211
|
+
deepStats = deep.stats;
|
|
242
212
|
}
|
|
243
213
|
const status = failures.length > 0 ? 'FAIL' : 'PASS';
|
|
244
214
|
// Severity-weighted scoring with per-gate cap and deduplication
|
|
@@ -4,6 +4,12 @@ import path from 'path';
|
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { GateRunner } from './runner.js';
|
|
6
6
|
import { DeepAnalysisGate } from './deep-analysis.js';
|
|
7
|
+
function mockDeep(outcome, failures = []) {
|
|
8
|
+
vi.spyOn(DeepAnalysisGate.prototype, 'run').mockResolvedValue(failures);
|
|
9
|
+
vi.spyOn(DeepAnalysisGate.prototype, 'getOutcome').mockReturnValue({
|
|
10
|
+
status: 'ok', mode: 'facts', filesAnalyzed: 1, chunksTotal: 1, chunksFailed: 0, ...outcome,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
7
13
|
describe('GateRunner deep stats execution mode', () => {
|
|
8
14
|
let testDir;
|
|
9
15
|
beforeEach(async () => {
|
|
@@ -25,41 +31,47 @@ describe('GateRunner deep stats execution mode', () => {
|
|
|
25
31
|
},
|
|
26
32
|
});
|
|
27
33
|
}
|
|
28
|
-
it('reports local lite tier
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
enabled: true,
|
|
33
|
-
apiKey: 'sk-test',
|
|
34
|
-
provider: 'local',
|
|
35
|
-
pro: false,
|
|
34
|
+
it('reports local lite tier and the model the provider actually loaded', async () => {
|
|
35
|
+
mockDeep({ model: 'Qwen2.5-Coder-0.5B-Instruct (stock)', modelFallback: true });
|
|
36
|
+
const report = await createRunner().run(testDir, undefined, {
|
|
37
|
+
enabled: true, apiKey: 'sk-test', provider: 'local', pro: false,
|
|
36
38
|
});
|
|
37
39
|
expect(report.stats.deep?.tier).toBe('lite');
|
|
38
|
-
expect(report.stats.deep?.model).toBe('Qwen2.5-Coder-0.5B');
|
|
40
|
+
expect(report.stats.deep?.model).toBe('Qwen2.5-Coder-0.5B-Instruct (stock)');
|
|
41
|
+
expect(report.stats.deep?.model_fallback).toBe(true);
|
|
42
|
+
expect(report.stats.deep?.status).toBe('ok');
|
|
43
|
+
expect(report.summary['deep-analysis']).toBe('PASS');
|
|
39
44
|
});
|
|
40
45
|
it('reports local deep tier when provider=local and pro=true', async () => {
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
enabled: true,
|
|
45
|
-
apiKey: 'sk-test',
|
|
46
|
-
provider: 'local',
|
|
47
|
-
pro: true,
|
|
46
|
+
mockDeep({ model: 'Rigour-Deep-v5.0.0 (Qwen2.5-Coder-1.5B fine-tuned)', modelFallback: false });
|
|
47
|
+
const report = await createRunner().run(testDir, undefined, {
|
|
48
|
+
enabled: true, apiKey: 'sk-test', provider: 'local', pro: true,
|
|
48
49
|
});
|
|
49
50
|
expect(report.stats.deep?.tier).toBe('deep');
|
|
50
|
-
expect(report.stats.deep?.model).toBe('Qwen2.5-Coder-1.5B');
|
|
51
|
+
expect(report.stats.deep?.model).toBe('Rigour-Deep-v5.0.0 (Qwen2.5-Coder-1.5B fine-tuned)');
|
|
51
52
|
});
|
|
52
53
|
it('reports cloud tier/model for cloud providers', async () => {
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
enabled: true,
|
|
57
|
-
apiKey: 'sk-test',
|
|
58
|
-
provider: 'openai',
|
|
59
|
-
modelName: 'gpt-4.1-mini',
|
|
60
|
-
pro: false,
|
|
54
|
+
mockDeep({ model: 'gpt-4.1-mini' });
|
|
55
|
+
const report = await createRunner().run(testDir, undefined, {
|
|
56
|
+
enabled: true, apiKey: 'sk-test', provider: 'openai', modelName: 'gpt-4.1-mini', pro: false,
|
|
61
57
|
});
|
|
62
58
|
expect(report.stats.deep?.tier).toBe('cloud');
|
|
63
59
|
expect(report.stats.deep?.model).toBe('gpt-4.1-mini');
|
|
64
60
|
});
|
|
61
|
+
it('reports a deep run that could not analyze anything as ERROR with a visible failure', async () => {
|
|
62
|
+
mockDeep({ status: 'error', chunksTotal: 3, chunksFailed: 3, error: 'All 3 inference call(s) failed.' });
|
|
63
|
+
const report = await createRunner().run(testDir, undefined, { enabled: true, pro: false });
|
|
64
|
+
expect(report.summary['deep-analysis']).toBe('ERROR');
|
|
65
|
+
expect(report.status).toBe('FAIL');
|
|
66
|
+
expect(report.stats.deep).toMatchObject({ status: 'error', chunks_total: 3, chunks_failed: 3, error: 'All 3 inference call(s) failed.' });
|
|
67
|
+
const failure = report.failures.find(f => f.id === 'deep-analysis');
|
|
68
|
+
expect(failure?.title).toBe('Deep analysis did not run');
|
|
69
|
+
expect(failure?.details).toContain('All 3 inference call(s) failed.');
|
|
70
|
+
});
|
|
71
|
+
it('keeps a partial run as PASS/FAIL by findings and records failed chunks', async () => {
|
|
72
|
+
mockDeep({ status: 'partial', chunksTotal: 4, chunksFailed: 1 });
|
|
73
|
+
const report = await createRunner().run(testDir, undefined, { enabled: true, pro: false });
|
|
74
|
+
expect(report.summary['deep-analysis']).toBe('PASS');
|
|
75
|
+
expect(report.stats.deep).toMatchObject({ status: 'partial', chunks_total: 4, chunks_failed: 1 });
|
|
76
|
+
});
|
|
65
77
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { ContextGate } from './context.js';
|
|
6
|
+
import { DependencyGate } from './dependency.js';
|
|
7
|
+
import { LogicDriftGate } from './logic-drift.js';
|
|
8
|
+
import { StyleDriftGate } from './style-drift.js';
|
|
9
|
+
describe('gates on a scoped run', () => {
|
|
10
|
+
let cwd;
|
|
11
|
+
beforeEach(async () => { cwd = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-scoped-')); });
|
|
12
|
+
afterEach(async () => { await fs.remove(cwd); });
|
|
13
|
+
it('context-drift reports only findings that cite a file in scope', async () => {
|
|
14
|
+
await fs.outputFile(path.join(cwd, 'src/a.ts'), 'export const a = process.env.API_URL_PROD;\n');
|
|
15
|
+
await fs.outputFile(path.join(cwd, 'src/b.ts'), 'export const b = process.env.API_URL_STAGING;\n');
|
|
16
|
+
const gate = new ContextGate({ context: { enabled: true } });
|
|
17
|
+
const record = { anchors: [{ type: 'env', id: 'API_URL', confidence: 1 }] };
|
|
18
|
+
const unscoped = await gate.run({ cwd, record });
|
|
19
|
+
const scoped = await gate.run({ cwd, record, patterns: ['src/a.ts'] });
|
|
20
|
+
expect(unscoped.flatMap(f => f.files)).toEqual(expect.arrayContaining(['src/a.ts', 'src/b.ts']));
|
|
21
|
+
expect(scoped.length).toBeGreaterThan(0);
|
|
22
|
+
expect(scoped.every(f => f.files?.includes('src/a.ts'))).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
it('dependency-guardian checks the manifest only when package.json is in scope', async () => {
|
|
25
|
+
await fs.outputJson(path.join(cwd, 'package.json'), { dependencies: { 'left-pad': '1.0.0' } });
|
|
26
|
+
await fs.outputFile(path.join(cwd, 'src/a.ts'), "import pad from 'left-pad';\n");
|
|
27
|
+
const gate = new DependencyGate({ gates: { dependencies: { forbid: ['left-pad'] } } });
|
|
28
|
+
expect(await gate.run({ cwd, patterns: ['src/a.ts'] })).toEqual([]);
|
|
29
|
+
const withManifest = await gate.run({ cwd, patterns: ['package.json'] });
|
|
30
|
+
expect(withManifest.some(f => f.title === 'Forbidden Dependency')).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
it('logic-drift never creates or rewrites its baseline from a scoped run', async () => {
|
|
33
|
+
const baseline = path.join(cwd, '.rigour/logic-baseline.json');
|
|
34
|
+
await fs.outputFile(path.join(cwd, 'src/rules.ts'), 'export function eligible(score: number) { return score >= 10; }\n');
|
|
35
|
+
await fs.outputFile(path.join(cwd, 'src/other.ts'), 'export function other(n: number) { return n > 1; }\n');
|
|
36
|
+
const gate = new LogicDriftGate();
|
|
37
|
+
expect(await gate.run({ cwd, patterns: ['src/rules.ts'] })).toEqual([]);
|
|
38
|
+
expect(await fs.pathExists(baseline)).toBe(false);
|
|
39
|
+
await gate.run({ cwd });
|
|
40
|
+
const before = await fs.readFile(baseline, 'utf-8');
|
|
41
|
+
await fs.outputFile(path.join(cwd, 'src/rules.ts'), 'export function eligible(score: number) { return score > 10; }\n');
|
|
42
|
+
const scoped = await gate.run({ cwd, patterns: ['src/rules.ts'] });
|
|
43
|
+
expect(scoped.some(f => f.details.includes("'>=' to '>'"))).toBe(true);
|
|
44
|
+
expect(await fs.readFile(baseline, 'utf-8')).toBe(before);
|
|
45
|
+
});
|
|
46
|
+
it('style-drift does not create its baseline from a scoped run', async () => {
|
|
47
|
+
await fs.outputFile(path.join(cwd, 'src/a.ts'), 'export function doThing() { return 1; }\n');
|
|
48
|
+
const gate = new StyleDriftGate();
|
|
49
|
+
expect(await gate.run({ cwd, patterns: ['src/a.ts'] })).toEqual([]);
|
|
50
|
+
expect(await fs.pathExists(path.join(cwd, '.rigour/style-baseline.json'))).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic Bugs Gate: type-aware rules that prove a defect before reporting it.
|
|
3
|
+
*
|
|
4
|
+
* Each rule traces a value from where it enters to where it does harm, across
|
|
5
|
+
* files when needed (see src/semantic/). A finding names both ends; anything
|
|
6
|
+
* the engine cannot resolve produces no finding.
|
|
7
|
+
*/
|
|
8
|
+
import { Gate, GateContext } from './base.js';
|
|
9
|
+
import { Failure, Provenance } from '../types/index.js';
|
|
10
|
+
export interface SemanticBugsConfig {
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
/** Rule ids to run (`learned/<id>` for a learned rule); all rules when omitted. */
|
|
13
|
+
rules?: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare class SemanticBugsGate extends Gate {
|
|
16
|
+
private readonly config;
|
|
17
|
+
constructor(config?: SemanticBugsConfig);
|
|
18
|
+
protected get provenance(): Provenance;
|
|
19
|
+
run(context: GateContext): Promise<Failure[]>;
|
|
20
|
+
/** Built-in rules plus the repository's learned rules (`.rigour/rules/`), filtered by config. */
|
|
21
|
+
private selectRules;
|
|
22
|
+
private toFailure;
|
|
23
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic Bugs Gate: type-aware rules that prove a defect before reporting it.
|
|
3
|
+
*
|
|
4
|
+
* Each rule traces a value from where it enters to where it does harm, across
|
|
5
|
+
* files when needed (see src/semantic/). A finding names both ends; anything
|
|
6
|
+
* the engine cannot resolve produces no finding.
|
|
7
|
+
*/
|
|
8
|
+
import { Gate } from './base.js';
|
|
9
|
+
import { FileScanner } from '../utils/scanner.js';
|
|
10
|
+
import { Logger } from '../utils/logger.js';
|
|
11
|
+
import { analyzeFiles, BUILT_IN_RULES } from '../semantic/engine.js';
|
|
12
|
+
import { compileLearnedRule, LEARNED_PREFIX } from '../semantic/learn/compile.js';
|
|
13
|
+
import { loadLearnedRules } from '../semantic/learn/store.js';
|
|
14
|
+
const SOURCE_PATTERNS = ['**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}'];
|
|
15
|
+
const TEST_FILE = /(?:^|\/)(?:__tests__|__mocks__|tests?)\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
16
|
+
export class SemanticBugsGate extends Gate {
|
|
17
|
+
config;
|
|
18
|
+
constructor(config = {}) {
|
|
19
|
+
super('semantic-bugs', 'Semantic Bug Detection');
|
|
20
|
+
this.config = config;
|
|
21
|
+
}
|
|
22
|
+
get provenance() { return 'traditional'; }
|
|
23
|
+
async run(context) {
|
|
24
|
+
const files = (await FileScanner.findFiles({
|
|
25
|
+
cwd: context.cwd,
|
|
26
|
+
patterns: context.patterns || SOURCE_PATTERNS,
|
|
27
|
+
ignore: context.ignore,
|
|
28
|
+
})).filter(file => /\.(?:[cm]?[jt]s|[jt]sx)$/i.test(file) && !/\.d\.[cm]?ts$/i.test(file) && !TEST_FILE.test(file));
|
|
29
|
+
if (files.length === 0)
|
|
30
|
+
return [];
|
|
31
|
+
const rules = this.selectRules(context.cwd);
|
|
32
|
+
if (rules.length === 0)
|
|
33
|
+
return [];
|
|
34
|
+
Logger.info(`Semantic Bugs: analyzing ${files.length} files with ${rules.length} rule(s)`);
|
|
35
|
+
return analyzeFiles(context.cwd, files, { rules }).map(finding => this.toFailure(finding));
|
|
36
|
+
}
|
|
37
|
+
/** Built-in rules plus the repository's learned rules (`.rigour/rules/`), filtered by config. */
|
|
38
|
+
selectRules(cwd) {
|
|
39
|
+
const all = [...BUILT_IN_RULES, ...loadLearnedRules(cwd).map(compileLearnedRule)];
|
|
40
|
+
const wanted = this.config.rules;
|
|
41
|
+
return wanted?.length ? all.filter(rule => wanted.includes(rule.id)) : all;
|
|
42
|
+
}
|
|
43
|
+
toFailure(finding) {
|
|
44
|
+
const failure = this.createFailure(`${finding.message}\n${finding.evidence.map(e => ` at ${e}`).join('\n')}`, [finding.file], finding.hint, `[${finding.rule}] ${finding.message.split('. ')[0]}`.slice(0, 120), finding.line, undefined, finding.severity);
|
|
45
|
+
const category = finding.rule.startsWith(LEARNED_PREFIX) ? 'learned-rule' : finding.rule;
|
|
46
|
+
return { ...failure, provenance: finding.provenance, source: 'ast', category, verified: true, confidence: 1 };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { SemanticBugsGate } from './semantic-bugs.js';
|
|
6
|
+
import { GateRunner } from './runner.js';
|
|
7
|
+
const LEAKY = [
|
|
8
|
+
'export async function notify(endpoint: string, signature: string) {',
|
|
9
|
+
" return fetch(endpoint, { method: 'POST', headers: { 'x-hook-signature': signature } });",
|
|
10
|
+
'}',
|
|
11
|
+
].join('\n');
|
|
12
|
+
describe('SemanticBugsGate', () => {
|
|
13
|
+
let cwd;
|
|
14
|
+
beforeEach(() => { cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'semantic-gate-')); });
|
|
15
|
+
afterEach(() => { fs.rmSync(cwd, { recursive: true, force: true }); });
|
|
16
|
+
function write(rel, body) {
|
|
17
|
+
fs.mkdirSync(path.dirname(path.join(cwd, rel)), { recursive: true });
|
|
18
|
+
fs.writeFileSync(path.join(cwd, rel), body);
|
|
19
|
+
}
|
|
20
|
+
it('reports a proven finding with its evidence, rule and verified metadata', async () => {
|
|
21
|
+
write('src/notify.ts', LEAKY);
|
|
22
|
+
const [failure, ...rest] = await new SemanticBugsGate().run({ cwd });
|
|
23
|
+
expect(rest).toHaveLength(0);
|
|
24
|
+
expect(failure).toMatchObject({
|
|
25
|
+
id: 'semantic-bugs', files: ['src/notify.ts'], line: 2, severity: 'high',
|
|
26
|
+
provenance: 'security', source: 'ast', category: 'credential-redirect', verified: true, confidence: 1,
|
|
27
|
+
});
|
|
28
|
+
expect(failure.title).toMatch(/^\[credential-redirect\]/);
|
|
29
|
+
expect(failure.details).toContain('at src/notify.ts:2');
|
|
30
|
+
});
|
|
31
|
+
it('skips test files and declaration files', async () => {
|
|
32
|
+
write('src/notify.test.ts', LEAKY);
|
|
33
|
+
write('src/__tests__/notify.ts', LEAKY);
|
|
34
|
+
write('types/notify.d.ts', 'export declare function notify(): void;\n');
|
|
35
|
+
expect(await new SemanticBugsGate().run({ cwd })).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
it('honours a scoped run', async () => {
|
|
38
|
+
write('src/notify.ts', LEAKY);
|
|
39
|
+
write('src/other.ts', LEAKY);
|
|
40
|
+
const failures = await new SemanticBugsGate().run({ cwd, patterns: ['src/other.ts'] });
|
|
41
|
+
expect(failures.map(f => f.files?.[0])).toEqual(['src/other.ts']);
|
|
42
|
+
});
|
|
43
|
+
it('runs only the configured rules', async () => {
|
|
44
|
+
write('src/notify.ts', LEAKY);
|
|
45
|
+
expect(await new SemanticBugsGate({ rules: ['in-memory-aggregation'] }).run({ cwd })).toEqual([]);
|
|
46
|
+
});
|
|
47
|
+
it('runs learned rules from .rigour/rules next to the built-in ones', async () => {
|
|
48
|
+
write('src/load.ts', "export const load = (url: string) => fetch(url, { method: 'GET' });\n");
|
|
49
|
+
write('.rigour/rules/require-option-redirect-abcd1234.json', JSON.stringify({
|
|
50
|
+
id: 'require-option-redirect-abcd1234', version: 1,
|
|
51
|
+
pattern: { template: 'require-option', callee: { level: 'name', key: 'fetch' }, argIndex: 1, property: 'redirect' },
|
|
52
|
+
message: 'fetch(...) does not set `redirect`.', severity: 'medium',
|
|
53
|
+
source: { commit: 'abcdef0123', file: 'src/http.ts', function: 'post' },
|
|
54
|
+
validation: { firesBefore: 1, firesAfter: 0, maxHits: 3, repoHits: [] },
|
|
55
|
+
}));
|
|
56
|
+
const [failure, ...rest] = await new SemanticBugsGate().run({ cwd });
|
|
57
|
+
expect(rest).toHaveLength(0);
|
|
58
|
+
expect(failure).toMatchObject({ files: ['src/load.ts'], line: 1, category: 'learned-rule', severity: 'medium' });
|
|
59
|
+
expect(failure.title).toContain('[learned/require-option-redirect-abcd1234]');
|
|
60
|
+
expect(failure.hint).toContain('fix abcdef01 in src/http.ts (post)');
|
|
61
|
+
});
|
|
62
|
+
it('is off unless enabled in rigour.yml', async () => {
|
|
63
|
+
write('src/notify.ts', LEAKY);
|
|
64
|
+
const base = { version: 1, commands: {}, gates: { max_file_lines: 500 } };
|
|
65
|
+
const off = await new GateRunner(base).run(cwd);
|
|
66
|
+
expect(off.failures.some(f => f.id === 'semantic-bugs')).toBe(false);
|
|
67
|
+
const on = await new GateRunner({ ...base, gates: { ...base.gates, semantic_bugs: { enabled: true } } }).run(cwd);
|
|
68
|
+
expect(on.failures.some(f => f.id === 'semantic-bugs')).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -162,7 +162,8 @@ export class SideEffectAnalysisGate extends Gate {
|
|
|
162
162
|
}
|
|
163
163
|
if (varName) {
|
|
164
164
|
// Variable is stored — check for cleanup using that specific variable
|
|
165
|
-
const
|
|
165
|
+
const cleanupScope = timerCleanupScope(lines, i, scope);
|
|
166
|
+
const hasPairedCleanup = hasCleanupForVariable(lines, varName, cleanupScope.start, cleanupScope.end, cleanupPats, lang);
|
|
166
167
|
if (hasPairedCleanup)
|
|
167
168
|
continue; // Properly paired
|
|
168
169
|
}
|
|
@@ -545,3 +546,12 @@ export class SideEffectAnalysisGate extends Gate {
|
|
|
545
546
|
}
|
|
546
547
|
}
|
|
547
548
|
}
|
|
549
|
+
/**
|
|
550
|
+
* Where a stored timer's cleanup may live. A local declaration (`const t = …`)
|
|
551
|
+
* must be cleared in its own function; a timer kept on a field or an outer
|
|
552
|
+
* variable is usually cleared by another method, so the whole file counts.
|
|
553
|
+
*/
|
|
554
|
+
function timerCleanupScope(lines, lineIdx, scope) {
|
|
555
|
+
const isLocal = /^\s*(?:const|let|var)\s/.test(lines[lineIdx]);
|
|
556
|
+
return isLocal ? scope : { start: 0, end: lines.length };
|
|
557
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { SideEffectAnalysisGate } from './side-effect-analysis/index.js';
|
|
6
|
+
describe('SideEffectAnalysisGate', () => {
|
|
7
|
+
let cwd;
|
|
8
|
+
beforeEach(() => { cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'side-effect-')); });
|
|
9
|
+
afterEach(() => { fs.rmSync(cwd, { recursive: true, force: true }); });
|
|
10
|
+
async function titlesFor(file, source) {
|
|
11
|
+
fs.mkdirSync(path.dirname(path.join(cwd, file)), { recursive: true });
|
|
12
|
+
fs.writeFileSync(path.join(cwd, file), source);
|
|
13
|
+
const failures = await new SideEffectAnalysisGate().run({ cwd });
|
|
14
|
+
return failures.map(f => f.title);
|
|
15
|
+
}
|
|
16
|
+
describe('recursion', () => {
|
|
17
|
+
it('ends an expression-bodied arrow at its expression, not at the next block', async () => {
|
|
18
|
+
// Shape of scripts/measure-intent.mjs: a one-line helper used inside a later loop that logs.
|
|
19
|
+
const titles = await titlesFor('scripts/measure.mjs', [
|
|
20
|
+
'const pct = (n, d) => (d === 0 ? \'n/a\' : `${Math.round((100 * n) / d)}%`);',
|
|
21
|
+
'for (const run of [1, 2]) {',
|
|
22
|
+
' process.stderr.write(`run ${run}: ${pct(run, 2)}\\n`);',
|
|
23
|
+
'}',
|
|
24
|
+
].join('\n'));
|
|
25
|
+
expect(titles).not.toContain('Side-Effect: Unbounded Recursion');
|
|
26
|
+
});
|
|
27
|
+
it('still flags a block-bodied arrow that recurses with I/O and no base case', async () => {
|
|
28
|
+
const titles = await titlesFor('src/walk.ts', [
|
|
29
|
+
"import fs from 'fs';",
|
|
30
|
+
'export const walk = (dir) => {',
|
|
31
|
+
' for (const entry of fs.readdirSync(dir)) walk(`${dir}/${entry}`);',
|
|
32
|
+
'};',
|
|
33
|
+
].join('\n'));
|
|
34
|
+
expect(titles).toContain('Side-Effect: Unbounded Recursion');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('process spawns', () => {
|
|
38
|
+
it('does not treat RegExp.prototype.exec as a process spawn', async () => {
|
|
39
|
+
// Verbatim shape of the SvelteKit repo's scripts/lint-fonts.mjs.
|
|
40
|
+
const titles = await titlesFor('scripts/lint-fonts.mjs', [
|
|
41
|
+
'function checkFontFamilyDeclarations(file, source) {',
|
|
42
|
+
'\tconst findings = [];',
|
|
43
|
+
'\tconst re = /font-family\\s*:/gi;',
|
|
44
|
+
'\tlet match;',
|
|
45
|
+
'\twhile ((match = re.exec(source)) !== null) {',
|
|
46
|
+
'\t\tconst valueStart = match.index + match[0].length;',
|
|
47
|
+
'\t\tfindings.push({ file, valueStart });',
|
|
48
|
+
'\t}',
|
|
49
|
+
'\treturn findings;',
|
|
50
|
+
'}',
|
|
51
|
+
].join('\n'));
|
|
52
|
+
expect(titles.some(t => /Orphan Process/.test(t))).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
it('does not treat a database exec method as a process spawn', async () => {
|
|
55
|
+
const titles = await titlesFor('src/db.ts', "export function migrate(db: any) { db.exec('create table t (id int)'); }\n");
|
|
56
|
+
expect(titles.some(t => /Orphan Process/.test(t))).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
it('does not flag a synchronous execFileSync call', async () => {
|
|
59
|
+
const titles = await titlesFor('scripts/eval/run.ts', [
|
|
60
|
+
"import { execFileSync } from 'node:child_process';",
|
|
61
|
+
"export const revision = () => execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();",
|
|
62
|
+
].join('\n'));
|
|
63
|
+
expect(titles.some(t => /Orphan Process/.test(t))).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
it('accepts once("exit") as exit handling for a spawned process', async () => {
|
|
66
|
+
const titles = await titlesFor('scripts/upload.ts', [
|
|
67
|
+
"import { spawn } from 'node:child_process';",
|
|
68
|
+
'export async function upload(args: string[]) {',
|
|
69
|
+
' const code = await new Promise<number>((resolve, reject) => {',
|
|
70
|
+
" const child = spawn('npx', args, { stdio: 'inherit' });",
|
|
71
|
+
" child.once('error', reject);",
|
|
72
|
+
" child.once('exit', (c) => resolve(c ?? 1));",
|
|
73
|
+
' });',
|
|
74
|
+
' return code;',
|
|
75
|
+
'}',
|
|
76
|
+
].join('\n'));
|
|
77
|
+
expect(titles.some(t => /Orphan Process/.test(t))).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
it('does not read prose in a comment as a fork call', async () => {
|
|
80
|
+
const titles = await titlesFor('src/lib/seo/json-ld.ts', [
|
|
81
|
+
"// Canonical profiles, ported as data from the fork (verified against production).",
|
|
82
|
+
'export const profiles = [];',
|
|
83
|
+
].join('\n'));
|
|
84
|
+
expect(titles.some(t => /Orphan Process/.test(t))).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
it('still flags a child_process exec whose process is never handled', async () => {
|
|
87
|
+
const titles = await titlesFor('src/run.ts', [
|
|
88
|
+
"import { exec } from 'child_process';",
|
|
89
|
+
'export function run() {',
|
|
90
|
+
" exec('ls -la');",
|
|
91
|
+
'}',
|
|
92
|
+
].join('\n'));
|
|
93
|
+
expect(titles.some(t => /Orphan Process/.test(t))).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
it('still flags a spawn through a child_process namespace', async () => {
|
|
96
|
+
const titles = await titlesFor('src/run.ts', [
|
|
97
|
+
"import * as cp from 'node:child_process';",
|
|
98
|
+
'export function run() {',
|
|
99
|
+
" cp.spawn('node', ['worker.js']);",
|
|
100
|
+
'}',
|
|
101
|
+
].join('\n'));
|
|
102
|
+
expect(titles.some(t => /Orphan Process/.test(t))).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe('timers', () => {
|
|
106
|
+
it('does not flag a one-shot awaited setTimeout delay', async () => {
|
|
107
|
+
const titles = await titlesFor('scripts/deploy.ts', [
|
|
108
|
+
'export async function retry(attempt: number) {',
|
|
109
|
+
' await new Promise((resolve) => setTimeout(resolve, attempt * 250));',
|
|
110
|
+
'}',
|
|
111
|
+
].join('\n'));
|
|
112
|
+
expect(titles.some(t => /Unbounded Timer/.test(t))).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
it('pairs a timer kept on a private field with a clearInterval in another method', async () => {
|
|
115
|
+
const titles = await titlesFor('src/lib/review-state.svelte.ts', [
|
|
116
|
+
'export class ReviewState {',
|
|
117
|
+
' #progressTimer: ReturnType<typeof setInterval> | undefined;',
|
|
118
|
+
' start() {',
|
|
119
|
+
' this.#progressTimer = setInterval(() => this.tick(), 500);',
|
|
120
|
+
' }',
|
|
121
|
+
' stop() {',
|
|
122
|
+
' clearInterval(this.#progressTimer);',
|
|
123
|
+
' }',
|
|
124
|
+
' tick() {}',
|
|
125
|
+
'}',
|
|
126
|
+
].join('\n'));
|
|
127
|
+
expect(titles.some(t => /Unbounded Timer/.test(t))).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
it('pairs a timer kept in an outer variable with a clearInterval in another function', async () => {
|
|
130
|
+
const titles = await titlesFor('src/lib/poller.ts', [
|
|
131
|
+
'let pollTimer: ReturnType<typeof setInterval> | undefined;',
|
|
132
|
+
'export function startPolling(refresh: () => void) {',
|
|
133
|
+
' pollTimer = setInterval(refresh, 5000);',
|
|
134
|
+
'}',
|
|
135
|
+
'export function stopPolling() {',
|
|
136
|
+
' clearInterval(pollTimer);',
|
|
137
|
+
'}',
|
|
138
|
+
].join('\n'));
|
|
139
|
+
expect(titles.some(t => /Unbounded Timer/.test(t))).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
it('still flags a local interval whose own function never clears it', async () => {
|
|
142
|
+
const titles = await titlesFor('src/lib/leak.ts', [
|
|
143
|
+
'export function start(refresh: () => void) {',
|
|
144
|
+
' const timer = setInterval(refresh, 5000);',
|
|
145
|
+
' return 1;',
|
|
146
|
+
'}',
|
|
147
|
+
'export function stop(other: ReturnType<typeof setInterval>) {',
|
|
148
|
+
' clearInterval(other);',
|
|
149
|
+
'}',
|
|
150
|
+
].join('\n'));
|
|
151
|
+
expect(titles.some(t => /Unbounded Timer/.test(t))).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
it('still flags a setInterval that is never cleared', async () => {
|
|
154
|
+
const titles = await titlesFor('src/poll.ts', [
|
|
155
|
+
'export function poll(check: () => void) {',
|
|
156
|
+
' setInterval(check, 1000);',
|
|
157
|
+
'}',
|
|
158
|
+
].join('\n'));
|
|
159
|
+
expect(titles.some(t => /Unbounded Timer/.test(t))).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|