@rigour-labs/core 3.0.4 → 3.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
@@ -0,0 +1,188 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* Go test quality checks.
|
|
7
|
+
* Go tests use func TestXxx(t *testing.T) pattern.
|
|
8
|
+
* Assertions via t.Fatal, t.Error, t.Fatalf, t.Errorf, t.Fail, t.FailNow.
|
|
9
|
+
* Also checks for t.Run subtests and table-driven patterns.
|
|
10
|
+
*/
|
|
11
|
+
export function checkGoTestQuality(content, file, issues, config) {
|
|
12
|
+
const lines = content.split('\n');
|
|
13
|
+
let inTestFunc = false;
|
|
14
|
+
let testStartLine = 0;
|
|
15
|
+
let braceDepth = 0;
|
|
16
|
+
let hasAssertion = false;
|
|
17
|
+
let testContent = '';
|
|
18
|
+
for (let i = 0; i < lines.length; i++) {
|
|
19
|
+
const line = lines[i];
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
// Detect test function: func TestXxx(t *testing.T) {
|
|
22
|
+
const testMatch = trimmed.match(/^func\s+(Test\w+|Benchmark\w+)\s*\(/);
|
|
23
|
+
if (testMatch && !inTestFunc) {
|
|
24
|
+
inTestFunc = true;
|
|
25
|
+
testStartLine = i + 1;
|
|
26
|
+
braceDepth = 0;
|
|
27
|
+
hasAssertion = false;
|
|
28
|
+
testContent = '';
|
|
29
|
+
// Count braces on this line
|
|
30
|
+
for (const ch of line) {
|
|
31
|
+
if (ch === '{')
|
|
32
|
+
braceDepth++;
|
|
33
|
+
if (ch === '}')
|
|
34
|
+
braceDepth--;
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (inTestFunc) {
|
|
39
|
+
testContent += line + '\n';
|
|
40
|
+
for (const ch of line) {
|
|
41
|
+
if (ch === '{')
|
|
42
|
+
braceDepth++;
|
|
43
|
+
if (ch === '}')
|
|
44
|
+
braceDepth--;
|
|
45
|
+
}
|
|
46
|
+
// Go test assertions: t.Fatal, t.Error, t.Fatalf, t.Errorf, t.Fail, t.FailNow
|
|
47
|
+
// Also: assert/require from testify, t.Run for subtests
|
|
48
|
+
if (/\bt\.\s*(?:Fatal|Error|Fatalf|Errorf|Fail|FailNow|Log|Logf|Skip|Skipf|Helper)\s*\(/.test(line) ||
|
|
49
|
+
/\bt\.Run\s*\(/.test(line) ||
|
|
50
|
+
/\bassert\.\w+\s*\(/.test(line) || /\brequire\.\w+\s*\(/.test(line) ||
|
|
51
|
+
/\bif\b.*\bt\./.test(line)) {
|
|
52
|
+
hasAssertion = true;
|
|
53
|
+
}
|
|
54
|
+
// Tautological: if true { t.Fatal... } or assert.True(t, true)
|
|
55
|
+
if (config.check_tautological) {
|
|
56
|
+
if (/assert\.True\s*\(\s*\w+\s*,\s*true\s*\)/.test(line) ||
|
|
57
|
+
/assert\.Equal\s*\(\s*\w+\s*,\s*(\d+|"[^"]*")\s*,\s*\1\s*\)/.test(line)) {
|
|
58
|
+
issues.push({
|
|
59
|
+
file, line: i + 1, pattern: 'tautological-assertion',
|
|
60
|
+
reason: 'Tautological assertion — comparing a constant to itself proves nothing',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// End of function
|
|
65
|
+
if (braceDepth === 0 && testContent.trim()) {
|
|
66
|
+
const meaningful = testContent.split('\n').filter(l => {
|
|
67
|
+
const t = l.trim();
|
|
68
|
+
return t && t !== '{' && t !== '}' && !t.startsWith('//');
|
|
69
|
+
});
|
|
70
|
+
if (config.check_empty_tests && meaningful.length === 0) {
|
|
71
|
+
issues.push({
|
|
72
|
+
file, line: testStartLine, pattern: 'empty-test',
|
|
73
|
+
reason: 'Empty test function — no test logic',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else if (config.check_empty_tests && !hasAssertion && meaningful.length > 0) {
|
|
77
|
+
issues.push({
|
|
78
|
+
file, line: testStartLine, pattern: 'no-assertion',
|
|
79
|
+
reason: 'Test has no assertions (t.Error, t.Fatal, assert.*) — executes code but never verifies',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
inTestFunc = false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Java/Kotlin test quality checks.
|
|
89
|
+
* JUnit 4: @Test + Assert.assertEquals, assertTrue, etc.
|
|
90
|
+
* JUnit 5: @Test + Assertions.assertEquals, assertThrows, etc.
|
|
91
|
+
* Kotlin: @Test + kotlin.test assertEquals, etc.
|
|
92
|
+
*/
|
|
93
|
+
export function checkJavaKotlinTestQuality(content, file, ext, issues, config) {
|
|
94
|
+
const lines = content.split('\n');
|
|
95
|
+
const isKotlin = ext === '.kt';
|
|
96
|
+
let inTestMethod = false;
|
|
97
|
+
let testStartLine = 0;
|
|
98
|
+
let braceDepth = 0;
|
|
99
|
+
let hasAssertion = false;
|
|
100
|
+
let mockCount = 0;
|
|
101
|
+
for (let i = 0; i < lines.length; i++) {
|
|
102
|
+
const line = lines[i];
|
|
103
|
+
const trimmed = line.trim();
|
|
104
|
+
// Detect @Test annotation (next non-empty line is the method)
|
|
105
|
+
if (trimmed === '@Test' || /^@Test\s*(\(|$)/.test(trimmed)) {
|
|
106
|
+
// Look for the method signature on this or next lines
|
|
107
|
+
for (let j = i + 1; j < Math.min(i + 3, lines.length); j++) {
|
|
108
|
+
const methodLine = lines[j].trim();
|
|
109
|
+
const methodMatch = isKotlin
|
|
110
|
+
? methodLine.match(/^(?:fun|suspend\s+fun)\s+(\w+)\s*\(/)
|
|
111
|
+
: methodLine.match(/^(?:public\s+|private\s+|protected\s+)?(?:static\s+)?void\s+(\w+)\s*\(/);
|
|
112
|
+
if (methodMatch) {
|
|
113
|
+
inTestMethod = true;
|
|
114
|
+
testStartLine = j + 1;
|
|
115
|
+
braceDepth = 0;
|
|
116
|
+
hasAssertion = false;
|
|
117
|
+
mockCount = 0;
|
|
118
|
+
// Count braces
|
|
119
|
+
for (const ch of lines[j]) {
|
|
120
|
+
if (ch === '{')
|
|
121
|
+
braceDepth++;
|
|
122
|
+
if (ch === '}')
|
|
123
|
+
braceDepth--;
|
|
124
|
+
}
|
|
125
|
+
i = j;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (inTestMethod) {
|
|
132
|
+
for (const ch of line) {
|
|
133
|
+
if (ch === '{')
|
|
134
|
+
braceDepth++;
|
|
135
|
+
if (ch === '}')
|
|
136
|
+
braceDepth--;
|
|
137
|
+
}
|
|
138
|
+
// JUnit 4/5 assertions
|
|
139
|
+
if (/\b(?:assert(?:Equals|True|False|NotNull|Null|That|Throws|DoesNotThrow|Same|NotSame|ArrayEquals)|assertEquals|assertTrue|assertFalse|assertNotNull|assertNull|assertThrows)\s*\(/.test(line)) {
|
|
140
|
+
hasAssertion = true;
|
|
141
|
+
}
|
|
142
|
+
// Kotlin test assertions
|
|
143
|
+
if (isKotlin && /\b(?:assertEquals|assertTrue|assertFalse|assertNotNull|assertNull|assertFailsWith|assertIs|assertContains|expect)\s*[({]/.test(line)) {
|
|
144
|
+
hasAssertion = true;
|
|
145
|
+
}
|
|
146
|
+
// Hamcrest / AssertJ
|
|
147
|
+
if (/\bassertThat\s*\(/.test(line) || /\.should\w*\(/.test(line)) {
|
|
148
|
+
hasAssertion = true;
|
|
149
|
+
}
|
|
150
|
+
// Verify (Mockito)
|
|
151
|
+
if (/\bverify\s*\(/.test(line)) {
|
|
152
|
+
hasAssertion = true;
|
|
153
|
+
}
|
|
154
|
+
// Mock counting
|
|
155
|
+
if (/\b(?:mock|spy|when|doReturn|doThrow|doNothing|Mockito\.\w+)\s*\(/.test(line) ||
|
|
156
|
+
/@Mock\b/.test(line) || /@InjectMocks\b/.test(line)) {
|
|
157
|
+
mockCount++;
|
|
158
|
+
}
|
|
159
|
+
// Tautological
|
|
160
|
+
if (config.check_tautological) {
|
|
161
|
+
if (/assertEquals\s*\(\s*true\s*,\s*true\s*\)/.test(line) ||
|
|
162
|
+
/assertTrue\s*\(\s*true\s*\)/.test(line) ||
|
|
163
|
+
/assertEquals\s*\(\s*(\d+)\s*,\s*\1\s*\)/.test(line)) {
|
|
164
|
+
issues.push({
|
|
165
|
+
file, line: i + 1, pattern: 'tautological-assertion',
|
|
166
|
+
reason: 'Tautological assertion — comparing a constant to itself proves nothing',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// End of method
|
|
171
|
+
if (braceDepth === 0) {
|
|
172
|
+
if (config.check_empty_tests && !hasAssertion) {
|
|
173
|
+
issues.push({
|
|
174
|
+
file, line: testStartLine, pattern: 'no-assertion',
|
|
175
|
+
reason: 'Test has no assertions — executes code but never verifies results',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (config.check_mock_heavy && mockCount > config.max_mocks_per_test) {
|
|
179
|
+
issues.push({
|
|
180
|
+
file, line: testStartLine, pattern: 'mock-heavy',
|
|
181
|
+
reason: `Test uses ${mockCount} mocks (max: ${config.max_mocks_per_test}) — may be testing mocks, not real behavior`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
inTestMethod = false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -50,18 +50,4 @@ export declare class TestQualityGate extends Gate {
|
|
|
50
50
|
private analyzeJSTestBlock;
|
|
51
51
|
private checkPythonTestQuality;
|
|
52
52
|
private analyzePythonTestBlock;
|
|
53
|
-
/**
|
|
54
|
-
* Go test quality checks.
|
|
55
|
-
* Go tests use func TestXxx(t *testing.T) pattern.
|
|
56
|
-
* Assertions via t.Fatal, t.Error, t.Fatalf, t.Errorf, t.Fail, t.FailNow.
|
|
57
|
-
* Also checks for t.Run subtests and table-driven patterns.
|
|
58
|
-
*/
|
|
59
|
-
private checkGoTestQuality;
|
|
60
|
-
/**
|
|
61
|
-
* Java/Kotlin test quality checks.
|
|
62
|
-
* JUnit 4: @Test + Assert.assertEquals, assertTrue, etc.
|
|
63
|
-
* JUnit 5: @Test + Assertions.assertEquals, assertThrows, etc.
|
|
64
|
-
* Kotlin: @Test + kotlin.test assertEquals, etc.
|
|
65
|
-
*/
|
|
66
|
-
private checkJavaKotlinTestQuality;
|
|
67
53
|
}
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { Gate } from './base.js';
|
|
27
27
|
import { FileScanner } from '../utils/scanner.js';
|
|
28
28
|
import { Logger } from '../utils/logger.js';
|
|
29
|
+
import { checkGoTestQuality, checkJavaKotlinTestQuality } from './test-quality-lang.js';
|
|
29
30
|
import fs from 'fs-extra';
|
|
30
31
|
import path from 'path';
|
|
31
32
|
export class TestQualityGate extends Gate {
|
|
@@ -76,10 +77,20 @@ export class TestQualityGate extends Gate {
|
|
|
76
77
|
this.checkPythonTestQuality(content, file, issues);
|
|
77
78
|
}
|
|
78
79
|
else if (ext === '.go') {
|
|
79
|
-
|
|
80
|
+
checkGoTestQuality(content, file, issues, {
|
|
81
|
+
check_empty_tests: this.config.check_empty_tests,
|
|
82
|
+
check_tautological: this.config.check_tautological,
|
|
83
|
+
check_mock_heavy: this.config.check_mock_heavy,
|
|
84
|
+
max_mocks_per_test: this.config.max_mocks_per_test,
|
|
85
|
+
});
|
|
80
86
|
}
|
|
81
87
|
else if (ext === '.java' || ext === '.kt') {
|
|
82
|
-
|
|
88
|
+
checkJavaKotlinTestQuality(content, file, ext, issues, {
|
|
89
|
+
check_empty_tests: this.config.check_empty_tests,
|
|
90
|
+
check_tautological: this.config.check_tautological,
|
|
91
|
+
check_mock_heavy: this.config.check_mock_heavy,
|
|
92
|
+
max_mocks_per_test: this.config.max_mocks_per_test,
|
|
93
|
+
});
|
|
83
94
|
}
|
|
84
95
|
}
|
|
85
96
|
catch { /* skip */ }
|
|
@@ -325,188 +336,4 @@ export class TestQualityGate extends Gate {
|
|
|
325
336
|
});
|
|
326
337
|
}
|
|
327
338
|
}
|
|
328
|
-
/**
|
|
329
|
-
* Go test quality checks.
|
|
330
|
-
* Go tests use func TestXxx(t *testing.T) pattern.
|
|
331
|
-
* Assertions via t.Fatal, t.Error, t.Fatalf, t.Errorf, t.Fail, t.FailNow.
|
|
332
|
-
* Also checks for t.Run subtests and table-driven patterns.
|
|
333
|
-
*/
|
|
334
|
-
checkGoTestQuality(content, file, issues) {
|
|
335
|
-
const lines = content.split('\n');
|
|
336
|
-
let inTestFunc = false;
|
|
337
|
-
let testStartLine = 0;
|
|
338
|
-
let braceDepth = 0;
|
|
339
|
-
let hasAssertion = false;
|
|
340
|
-
let testContent = '';
|
|
341
|
-
for (let i = 0; i < lines.length; i++) {
|
|
342
|
-
const line = lines[i];
|
|
343
|
-
const trimmed = line.trim();
|
|
344
|
-
// Detect test function: func TestXxx(t *testing.T) {
|
|
345
|
-
const testMatch = trimmed.match(/^func\s+(Test\w+|Benchmark\w+)\s*\(/);
|
|
346
|
-
if (testMatch && !inTestFunc) {
|
|
347
|
-
inTestFunc = true;
|
|
348
|
-
testStartLine = i + 1;
|
|
349
|
-
braceDepth = 0;
|
|
350
|
-
hasAssertion = false;
|
|
351
|
-
testContent = '';
|
|
352
|
-
// Count braces on this line
|
|
353
|
-
for (const ch of line) {
|
|
354
|
-
if (ch === '{')
|
|
355
|
-
braceDepth++;
|
|
356
|
-
if (ch === '}')
|
|
357
|
-
braceDepth--;
|
|
358
|
-
}
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
if (inTestFunc) {
|
|
362
|
-
testContent += line + '\n';
|
|
363
|
-
for (const ch of line) {
|
|
364
|
-
if (ch === '{')
|
|
365
|
-
braceDepth++;
|
|
366
|
-
if (ch === '}')
|
|
367
|
-
braceDepth--;
|
|
368
|
-
}
|
|
369
|
-
// Go test assertions: t.Fatal, t.Error, t.Fatalf, t.Errorf, t.Fail, t.FailNow
|
|
370
|
-
// Also: assert/require from testify, t.Run for subtests
|
|
371
|
-
if (/\bt\.\s*(?:Fatal|Error|Fatalf|Errorf|Fail|FailNow|Log|Logf|Skip|Skipf|Helper)\s*\(/.test(line) ||
|
|
372
|
-
/\bt\.Run\s*\(/.test(line) ||
|
|
373
|
-
/\bassert\.\w+\s*\(/.test(line) || /\brequire\.\w+\s*\(/.test(line) ||
|
|
374
|
-
/\bif\b.*\bt\./.test(line)) {
|
|
375
|
-
hasAssertion = true;
|
|
376
|
-
}
|
|
377
|
-
// Tautological: if true { t.Fatal... } or assert.True(t, true)
|
|
378
|
-
if (this.config.check_tautological) {
|
|
379
|
-
if (/assert\.True\s*\(\s*\w+\s*,\s*true\s*\)/.test(line) ||
|
|
380
|
-
/assert\.Equal\s*\(\s*\w+\s*,\s*(\d+|"[^"]*")\s*,\s*\1\s*\)/.test(line)) {
|
|
381
|
-
issues.push({
|
|
382
|
-
file, line: i + 1, pattern: 'tautological-assertion',
|
|
383
|
-
reason: 'Tautological assertion — comparing a constant to itself proves nothing',
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
// End of function
|
|
388
|
-
if (braceDepth === 0 && testContent.trim()) {
|
|
389
|
-
const meaningful = testContent.split('\n').filter(l => {
|
|
390
|
-
const t = l.trim();
|
|
391
|
-
return t && t !== '{' && t !== '}' && !t.startsWith('//');
|
|
392
|
-
});
|
|
393
|
-
if (this.config.check_empty_tests && meaningful.length === 0) {
|
|
394
|
-
issues.push({
|
|
395
|
-
file, line: testStartLine, pattern: 'empty-test',
|
|
396
|
-
reason: 'Empty test function — no test logic',
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
else if (this.config.check_empty_tests && !hasAssertion && meaningful.length > 0) {
|
|
400
|
-
issues.push({
|
|
401
|
-
file, line: testStartLine, pattern: 'no-assertion',
|
|
402
|
-
reason: 'Test has no assertions (t.Error, t.Fatal, assert.*) — executes code but never verifies',
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
inTestFunc = false;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Java/Kotlin test quality checks.
|
|
412
|
-
* JUnit 4: @Test + Assert.assertEquals, assertTrue, etc.
|
|
413
|
-
* JUnit 5: @Test + Assertions.assertEquals, assertThrows, etc.
|
|
414
|
-
* Kotlin: @Test + kotlin.test assertEquals, etc.
|
|
415
|
-
*/
|
|
416
|
-
checkJavaKotlinTestQuality(content, file, ext, issues) {
|
|
417
|
-
const lines = content.split('\n');
|
|
418
|
-
const isKotlin = ext === '.kt';
|
|
419
|
-
let inTestMethod = false;
|
|
420
|
-
let testStartLine = 0;
|
|
421
|
-
let braceDepth = 0;
|
|
422
|
-
let hasAssertion = false;
|
|
423
|
-
let mockCount = 0;
|
|
424
|
-
for (let i = 0; i < lines.length; i++) {
|
|
425
|
-
const line = lines[i];
|
|
426
|
-
const trimmed = line.trim();
|
|
427
|
-
// Detect @Test annotation (next non-empty line is the method)
|
|
428
|
-
if (trimmed === '@Test' || /^@Test\s*(\(|$)/.test(trimmed)) {
|
|
429
|
-
// Look for the method signature on this or next lines
|
|
430
|
-
for (let j = i + 1; j < Math.min(i + 3, lines.length); j++) {
|
|
431
|
-
const methodLine = lines[j].trim();
|
|
432
|
-
const methodMatch = isKotlin
|
|
433
|
-
? methodLine.match(/^(?:fun|suspend\s+fun)\s+(\w+)\s*\(/)
|
|
434
|
-
: methodLine.match(/^(?:public\s+|private\s+|protected\s+)?(?:static\s+)?void\s+(\w+)\s*\(/);
|
|
435
|
-
if (methodMatch) {
|
|
436
|
-
inTestMethod = true;
|
|
437
|
-
testStartLine = j + 1;
|
|
438
|
-
braceDepth = 0;
|
|
439
|
-
hasAssertion = false;
|
|
440
|
-
mockCount = 0;
|
|
441
|
-
// Count braces
|
|
442
|
-
for (const ch of lines[j]) {
|
|
443
|
-
if (ch === '{')
|
|
444
|
-
braceDepth++;
|
|
445
|
-
if (ch === '}')
|
|
446
|
-
braceDepth--;
|
|
447
|
-
}
|
|
448
|
-
i = j;
|
|
449
|
-
break;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
if (inTestMethod) {
|
|
455
|
-
for (const ch of line) {
|
|
456
|
-
if (ch === '{')
|
|
457
|
-
braceDepth++;
|
|
458
|
-
if (ch === '}')
|
|
459
|
-
braceDepth--;
|
|
460
|
-
}
|
|
461
|
-
// JUnit 4/5 assertions
|
|
462
|
-
if (/\b(?:assert(?:Equals|True|False|NotNull|Null|That|Throws|DoesNotThrow|Same|NotSame|ArrayEquals)|assertEquals|assertTrue|assertFalse|assertNotNull|assertNull|assertThrows)\s*\(/.test(line)) {
|
|
463
|
-
hasAssertion = true;
|
|
464
|
-
}
|
|
465
|
-
// Kotlin test assertions
|
|
466
|
-
if (isKotlin && /\b(?:assertEquals|assertTrue|assertFalse|assertNotNull|assertNull|assertFailsWith|assertIs|assertContains|expect)\s*[({]/.test(line)) {
|
|
467
|
-
hasAssertion = true;
|
|
468
|
-
}
|
|
469
|
-
// Hamcrest / AssertJ
|
|
470
|
-
if (/\bassertThat\s*\(/.test(line) || /\.should\w*\(/.test(line)) {
|
|
471
|
-
hasAssertion = true;
|
|
472
|
-
}
|
|
473
|
-
// Verify (Mockito)
|
|
474
|
-
if (/\bverify\s*\(/.test(line)) {
|
|
475
|
-
hasAssertion = true;
|
|
476
|
-
}
|
|
477
|
-
// Mock counting
|
|
478
|
-
if (/\b(?:mock|spy|when|doReturn|doThrow|doNothing|Mockito\.\w+)\s*\(/.test(line) ||
|
|
479
|
-
/@Mock\b/.test(line) || /@InjectMocks\b/.test(line)) {
|
|
480
|
-
mockCount++;
|
|
481
|
-
}
|
|
482
|
-
// Tautological
|
|
483
|
-
if (this.config.check_tautological) {
|
|
484
|
-
if (/assertEquals\s*\(\s*true\s*,\s*true\s*\)/.test(line) ||
|
|
485
|
-
/assertTrue\s*\(\s*true\s*\)/.test(line) ||
|
|
486
|
-
/assertEquals\s*\(\s*(\d+)\s*,\s*\1\s*\)/.test(line)) {
|
|
487
|
-
issues.push({
|
|
488
|
-
file, line: i + 1, pattern: 'tautological-assertion',
|
|
489
|
-
reason: 'Tautological assertion — comparing a constant to itself proves nothing',
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
// End of method
|
|
494
|
-
if (braceDepth === 0) {
|
|
495
|
-
if (this.config.check_empty_tests && !hasAssertion) {
|
|
496
|
-
issues.push({
|
|
497
|
-
file, line: testStartLine, pattern: 'no-assertion',
|
|
498
|
-
reason: 'Test has no assertions — executes code but never verifies results',
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
if (this.config.check_mock_heavy && mockCount > this.config.max_mocks_per_test) {
|
|
502
|
-
issues.push({
|
|
503
|
-
file, line: testStartLine, pattern: 'mock-heavy',
|
|
504
|
-
reason: `Test uses ${mockCount} mocks (max: ${this.config.max_mocks_per_test}) — may be testing mocks, not real behavior`,
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
inTestMethod = false;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
339
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Indexer — Pure Utility Helpers
|
|
3
|
+
*
|
|
4
|
+
* Standalone pure functions shared across language extractors and the main
|
|
5
|
+
* indexer class. No class state is referenced here.
|
|
6
|
+
*/
|
|
7
|
+
import type { PatternEntry, PatternType } from './types.js';
|
|
8
|
+
/** SHA-256 of `content`, truncated to 16 hex chars. */
|
|
9
|
+
export declare function hashContent(content: string): string;
|
|
10
|
+
export interface PatternEntryParams {
|
|
11
|
+
type: PatternType;
|
|
12
|
+
name: string;
|
|
13
|
+
file: string;
|
|
14
|
+
line: number;
|
|
15
|
+
endLine: number;
|
|
16
|
+
signature: string;
|
|
17
|
+
description: string;
|
|
18
|
+
keywords: string[];
|
|
19
|
+
content: string;
|
|
20
|
+
exported: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Build a complete PatternEntry from constituent parts. */
|
|
23
|
+
export declare function createPatternEntry(params: PatternEntryParams): PatternEntry;
|
|
24
|
+
/** Split camelCase / PascalCase / snake_case names into unique lowercase words. */
|
|
25
|
+
export declare function extractKeywords(name: string): string[];
|
|
26
|
+
/** Walk forward from `startIndex` and return the line index after the closing brace. */
|
|
27
|
+
export declare function findBraceBlockEnd(lines: string[], startIndex: number): number;
|
|
28
|
+
/** Return the source lines for a brace-delimited block starting at `startIndex`. */
|
|
29
|
+
export declare function getBraceBlockContent(lines: string[], startIndex: number): string;
|
|
30
|
+
/**
|
|
31
|
+
* Collect consecutive `//` comments immediately above `startIndex` (Go / Rust style).
|
|
32
|
+
* Walks upward until a non-comment line is encountered.
|
|
33
|
+
*/
|
|
34
|
+
export declare function getCOMLineComments(lines: string[], startIndex: number): string;
|
|
35
|
+
/**
|
|
36
|
+
* Extract the first JavaDoc `/** … *\/` comment block found above `startIndex`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function getJavaDoc(lines: string[], startIndex: number): string;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Indexer — Pure Utility Helpers
|
|
3
|
+
*
|
|
4
|
+
* Standalone pure functions shared across language extractors and the main
|
|
5
|
+
* indexer class. No class state is referenced here.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from 'crypto';
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Hashing / ID generation
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
/** SHA-256 of `content`, truncated to 16 hex chars. */
|
|
12
|
+
export function hashContent(content) {
|
|
13
|
+
return createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
14
|
+
}
|
|
15
|
+
/** Build a complete PatternEntry from constituent parts. */
|
|
16
|
+
export function createPatternEntry(params) {
|
|
17
|
+
const id = hashContent(`${params.file}:${params.name}:${params.line}`);
|
|
18
|
+
const hash = hashContent(params.content);
|
|
19
|
+
return {
|
|
20
|
+
id,
|
|
21
|
+
type: params.type,
|
|
22
|
+
name: params.name,
|
|
23
|
+
file: params.file,
|
|
24
|
+
line: params.line,
|
|
25
|
+
endLine: params.endLine,
|
|
26
|
+
signature: params.signature,
|
|
27
|
+
description: params.description,
|
|
28
|
+
keywords: params.keywords,
|
|
29
|
+
hash,
|
|
30
|
+
exported: params.exported,
|
|
31
|
+
usageCount: 0,
|
|
32
|
+
indexedAt: new Date().toISOString(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Keyword extraction
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
/** Split camelCase / PascalCase / snake_case names into unique lowercase words. */
|
|
39
|
+
export function extractKeywords(name) {
|
|
40
|
+
const words = name
|
|
41
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
42
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
43
|
+
.toLowerCase()
|
|
44
|
+
.split(/[\s_-]+/)
|
|
45
|
+
.filter(w => w.length > 1);
|
|
46
|
+
return [...new Set(words)];
|
|
47
|
+
}
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Brace-based block helpers (Go, Rust, JVM, C-style)
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
/** Walk forward from `startIndex` and return the line index after the closing brace. */
|
|
52
|
+
export function findBraceBlockEnd(lines, startIndex) {
|
|
53
|
+
let braceCount = 0;
|
|
54
|
+
let started = false;
|
|
55
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
56
|
+
const line = lines[i];
|
|
57
|
+
if (line.includes('{')) {
|
|
58
|
+
braceCount += (line.match(/\{/g) || []).length;
|
|
59
|
+
started = true;
|
|
60
|
+
}
|
|
61
|
+
if (line.includes('}')) {
|
|
62
|
+
braceCount -= (line.match(/\}/g) || []).length;
|
|
63
|
+
}
|
|
64
|
+
if (started && braceCount === 0)
|
|
65
|
+
return i + 1;
|
|
66
|
+
}
|
|
67
|
+
return lines.length;
|
|
68
|
+
}
|
|
69
|
+
/** Return the source lines for a brace-delimited block starting at `startIndex`. */
|
|
70
|
+
export function getBraceBlockContent(lines, startIndex) {
|
|
71
|
+
const end = findBraceBlockEnd(lines, startIndex);
|
|
72
|
+
return lines.slice(startIndex, end).join('\n');
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Comment extraction helpers
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
/**
|
|
78
|
+
* Collect consecutive `//` comments immediately above `startIndex` (Go / Rust style).
|
|
79
|
+
* Walks upward until a non-comment line is encountered.
|
|
80
|
+
*/
|
|
81
|
+
export function getCOMLineComments(lines, startIndex) {
|
|
82
|
+
const comments = [];
|
|
83
|
+
for (let i = startIndex; i >= 0; i--) {
|
|
84
|
+
const line = lines[i].trim();
|
|
85
|
+
if (line.startsWith('//')) {
|
|
86
|
+
comments.unshift(line.replace('//', '').trim());
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return comments.join(' ');
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Extract the first JavaDoc `/** … *\/` comment block found above `startIndex`.
|
|
96
|
+
*/
|
|
97
|
+
export function getJavaDoc(lines, startIndex) {
|
|
98
|
+
const comments = [];
|
|
99
|
+
let inDoc = false;
|
|
100
|
+
for (let i = startIndex; i >= 0; i--) {
|
|
101
|
+
const line = lines[i].trim();
|
|
102
|
+
if (line.endsWith('*/'))
|
|
103
|
+
inDoc = true;
|
|
104
|
+
if (inDoc) {
|
|
105
|
+
comments.unshift(line.replace('/**', '').replace('*/', '').replace(/^\*/, '').trim());
|
|
106
|
+
}
|
|
107
|
+
if (line.startsWith('/**'))
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
return comments.join(' ');
|
|
111
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Indexer — Language-Specific Extractors
|
|
3
|
+
*
|
|
4
|
+
* Standalone extraction functions for Go, Rust, JVM (Java/Kotlin/C#),
|
|
5
|
+
* Python, and a generic C-style fallback. Each function is pure and
|
|
6
|
+
* receives all required context as parameters.
|
|
7
|
+
*/
|
|
8
|
+
import type { PatternEntry } from './types.js';
|
|
9
|
+
export declare function extractGoPatterns(filePath: string, content: string, rootDir: string): PatternEntry[];
|
|
10
|
+
export declare function extractRustPatterns(filePath: string, content: string, rootDir: string): PatternEntry[];
|
|
11
|
+
export declare function extractJVMStylePatterns(filePath: string, content: string, rootDir: string): PatternEntry[];
|
|
12
|
+
export declare function extractGenericCPatterns(_filePath: string, _content: string): PatternEntry[];
|
|
13
|
+
export declare function extractPythonPatterns(filePath: string, content: string, rootDir: string, minNameLength: number): PatternEntry[];
|