@lifeaitools/rdc-skills 0.34.1 → 0.35.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/.claude-plugin/plugin.json +284 -1
- package/VALIDATOR-ARCHITECTURE.md +534 -0
- package/commands/analyze-tests.md +11 -0
- package/commands/check-clean-code.md +11 -0
- package/commands/check-packages.md +10 -0
- package/commands/compare-compliance.md +14 -0
- package/commands/full-analysis.md +50 -0
- package/commands/get-refactoring-plan.md +13 -0
- package/commands/quick-check.md +13 -0
- package/commands/recover.md +149 -0
- package/commands/review-arch.md +12 -0
- package/commands/review.md +12 -113
- package/commands/suggest-patterns.md +11 -0
- package/commands/validate-solid.md +11 -0
- package/package.json +14 -2
- package/scripts/architecture-score.mjs +157 -0
- package/scripts/clean-code-score.mjs +177 -0
- package/scripts/duplication-score.mjs +66 -0
- package/scripts/lib/architecture-scoring.mjs +695 -0
- package/scripts/lib/clean-code-scoring.mjs +258 -0
- package/scripts/lib/duplication-scoring.mjs +238 -0
- package/scripts/lib/language-plugin.mjs +82 -0
- package/scripts/lib/package-metrics.mjs +439 -0
- package/scripts/lib/pattern-scoring.mjs +351 -0
- package/scripts/lib/plugins/treesitter.mjs +1182 -0
- package/scripts/lib/plugins/typescript.mjs +672 -0
- package/scripts/lib/refactoring-scoring.mjs +307 -0
- package/scripts/lib/solid-scoring.mjs +101 -0
- package/scripts/lib/test-smell-scoring.mjs +581 -0
- package/scripts/lib/vendor/codeflow-parser/.source-commit +1 -0
- package/scripts/lib/vendor/codeflow-parser/grammars.d.ts +23 -0
- package/scripts/lib/vendor/codeflow-parser/grammars.js +57 -0
- package/scripts/lib/vendor/codeflow-parser/memberFacts.d.ts +274 -0
- package/scripts/lib/vendor/codeflow-parser/memberFacts.js +1117 -0
- package/scripts/lib/vendor/codeflow-parser/nativeParser.d.ts +115 -0
- package/scripts/lib/vendor/codeflow-parser/nativeParser.js +759 -0
- package/scripts/lib/vendor/codeflow-parser/package.json +3 -0
- package/scripts/lib/vendor/codeflow-parser/xmlParser.d.ts +77 -0
- package/scripts/lib/vendor/codeflow-parser/xmlParser.js +400 -0
- package/scripts/package-metrics-cli.mjs +112 -0
- package/scripts/pattern-score.mjs +143 -0
- package/scripts/refactoring-score.mjs +253 -0
- package/scripts/solid-score.mjs +337 -0
- package/skills/architecture-reviewer/SKILL.md +287 -0
- package/skills/clean-code-analyzer/SKILL.md +147 -0
- package/skills/package-design/SKILL.md +118 -0
- package/skills/pattern-advisor/SKILL.md +237 -0
- package/skills/pattern-refactoring-guide/SKILL.md +262 -0
- package/skills/review/SKILL.md +29 -0
- package/skills/solid-validator/SKILL.md +92 -0
- package/skills/testing-strategy/SKILL.md +132 -0
- package/tests/lib/architecture-scoring.test.mjs +335 -0
- package/tests/lib/clean-code-scoring.test.mjs +241 -0
- package/tests/lib/duplication-scoring.test.mjs +144 -0
- package/tests/lib/fixtures.mjs +58 -0
- package/tests/lib/package-metrics.test.mjs +241 -0
- package/tests/lib/pattern-scoring.test.mjs +251 -0
- package/tests/lib/refactoring-scoring.test.mjs +264 -0
- package/tests/lib/solid-scoring.test.mjs +291 -0
- package/tests/lib/test-smell-scoring.test.mjs +281 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
detectFactoryMethod, detectBuilder, detectSingleton, detectDecorator,
|
|
6
|
+
detectAdapter, detectFacade, detectStrategy, detectObserver,
|
|
7
|
+
detectCommand, detectTemplateMethod, patternScore, PATTERN_NAMES,
|
|
8
|
+
} from '../../scripts/lib/pattern-scoring.mjs';
|
|
9
|
+
import { makeMember, makeUnit } from './fixtures.mjs';
|
|
10
|
+
|
|
11
|
+
// ── empty / trivial input ───────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
test('patternScore: zero members fires zero findings across all 10 detectors, never throws', () => {
|
|
14
|
+
const unit = makeUnit({ members: [] });
|
|
15
|
+
const r = patternScore(unit);
|
|
16
|
+
assert.equal(r.totalFindings, 0);
|
|
17
|
+
assert.equal(Object.keys(r.patterns).length, PATTERN_NAMES.length);
|
|
18
|
+
for (const p of Object.values(r.patterns)) assert.deepEqual(p.findings, []);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('patternScore: a single trivial member with no signals fires nothing', () => {
|
|
22
|
+
const r = patternScore(makeUnit({ members: [makeMember()] }));
|
|
23
|
+
assert.equal(r.totalFindings, 0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('PATTERN_NAMES lists all 10 detected patterns', () => {
|
|
27
|
+
assert.equal(PATTERN_NAMES.length, 10);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// ── Factory Method — two independent signals ─────────────────────────────
|
|
31
|
+
|
|
32
|
+
test('Factory Method (a): switch-on-type constructing via `new` fires', () => {
|
|
33
|
+
const unit = makeUnit({ members: [makeMember({ switchStatements: [{ line: 5, hasBehaviorCall: false, hasTypeCreation: true }] })] });
|
|
34
|
+
const r = detectFactoryMethod(unit);
|
|
35
|
+
assert.equal(r.findings.length, 1);
|
|
36
|
+
assert.equal(r.findings[0].confidence, 90);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('Factory Method (b): >5 total news with >3 unique targets fires', () => {
|
|
40
|
+
const unit = makeUnit({ members: [makeMember({ constructorNewCallTargets: ['A', 'B', 'C', 'D', 'A', 'B'] })] }); // 6 total, 4 unique
|
|
41
|
+
const r = detectFactoryMethod(unit);
|
|
42
|
+
assert.equal(r.findings.length, 1);
|
|
43
|
+
assert.equal(r.findings[0].confidence, 75);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('Factory Method: boundary — exactly 5 news does not fire (b)', () => {
|
|
47
|
+
const unit = makeUnit({ members: [makeMember({ constructorNewCallTargets: ['A', 'B', 'C', 'D', 'A'] })] }); // 5 total, 4 unique
|
|
48
|
+
assert.deepEqual(detectFactoryMethod(unit).findings, []);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('Factory Method: boundary — 6 news but only 3 unique targets does not fire (b)', () => {
|
|
52
|
+
const unit = makeUnit({ members: [makeMember({ constructorNewCallTargets: ['A', 'A', 'B', 'B', 'C', 'C'] })] }); // 6 total, 3 unique
|
|
53
|
+
assert.deepEqual(detectFactoryMethod(unit).findings, []);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('Factory Method: no type-switch and few/uniform news does not fire', () => {
|
|
57
|
+
const unit = makeUnit({ members: [makeMember({ switchStatements: [], constructorNewCallTargets: ['A'] })] });
|
|
58
|
+
assert.deepEqual(detectFactoryMethod(unit).findings, []);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ── Builder — telescoping constructor (>4 params) ────────────────────────
|
|
62
|
+
|
|
63
|
+
test('Builder: constructor with 5 params fires', () => {
|
|
64
|
+
const unit = makeUnit({ members: [makeMember({ name: 'constructor', paramCount: 5 })] });
|
|
65
|
+
assert.equal(detectBuilder(unit).findings.length, 1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('Builder: boundary — constructor with exactly 4 params does not fire', () => {
|
|
69
|
+
const unit = makeUnit({ members: [makeMember({ name: 'constructor', paramCount: 4 })] });
|
|
70
|
+
assert.deepEqual(detectBuilder(unit).findings, []);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('Builder: a non-constructor member with 5 params does not fire (constructor-only rule)', () => {
|
|
74
|
+
const unit = makeUnit({ members: [makeMember({ name: 'setup', paramCount: 5 })] });
|
|
75
|
+
assert.deepEqual(detectBuilder(unit).findings, []);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ── Singleton — static instance field OR getInstance ─────────────────────
|
|
79
|
+
|
|
80
|
+
test('Singleton: hasGetInstanceMethod fires', () => {
|
|
81
|
+
const unit = makeUnit({ hasGetInstanceMethod: true });
|
|
82
|
+
assert.equal(detectSingleton(unit).findings.length, 1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('Singleton: a static property named "instance" (name match) fires', () => {
|
|
86
|
+
const unit = makeUnit({ staticPropertyNames: ['_instance'] });
|
|
87
|
+
assert.equal(detectSingleton(unit).findings.length, 1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('Singleton: a getInstance call-site fires', () => {
|
|
91
|
+
const unit = makeUnit({ members: [makeMember({ calleeNames: ['getInstance'] })] });
|
|
92
|
+
assert.equal(detectSingleton(unit).findings.length, 1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('Singleton: none of the three signals present does not fire', () => {
|
|
96
|
+
const unit = makeUnit({ hasGetInstanceMethod: false, staticPropertyNames: ['config'], members: [makeMember({ calleeNames: ['build'] })] });
|
|
97
|
+
assert.deepEqual(detectSingleton(unit).findings, []);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ── Decorator — conditional feature-add call ──────────────────────────────
|
|
101
|
+
|
|
102
|
+
test('Decorator: conditionalFeatureCallLine present fires', () => {
|
|
103
|
+
const unit = makeUnit({ members: [makeMember({ conditionalFeatureCallLine: 12 })] });
|
|
104
|
+
assert.equal(detectDecorator(unit).findings.length, 1);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('Decorator: conditionalFeatureCallLine null does not fire', () => {
|
|
108
|
+
const unit = makeUnit({ members: [makeMember({ conditionalFeatureCallLine: null })] });
|
|
109
|
+
assert.deepEqual(detectDecorator(unit).findings, []);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ── Adapter — name or call names an interface conversion ─────────────────
|
|
113
|
+
|
|
114
|
+
test('Adapter: member name matches convert/transform/adapt fires', () => {
|
|
115
|
+
const unit = makeUnit({ members: [makeMember({ name: 'convertToDto' })] });
|
|
116
|
+
assert.equal(detectAdapter(unit).findings.length, 1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('Adapter: a call to a convert/transform/adapt-named function fires', () => {
|
|
120
|
+
const unit = makeUnit({ members: [makeMember({ name: 'run', calleeNames: ['transformPayload'] })] });
|
|
121
|
+
assert.equal(detectAdapter(unit).findings.length, 1);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('Adapter: no matching name or call does not fire', () => {
|
|
125
|
+
const unit = makeUnit({ members: [makeMember({ name: 'run', calleeNames: ['persist'] })] });
|
|
126
|
+
assert.deepEqual(detectAdapter(unit).findings, []);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ── Facade — >5 deep-chain calls ──────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
test('Facade: deepChainCallCount=6 fires', () => {
|
|
132
|
+
const unit = makeUnit({ members: [makeMember({ deepChainCallCount: 6 })] });
|
|
133
|
+
assert.equal(detectFacade(unit).findings.length, 1);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('Facade: boundary — deepChainCallCount exactly 5 does not fire', () => {
|
|
137
|
+
const unit = makeUnit({ members: [makeMember({ deepChainCallCount: 5 })] });
|
|
138
|
+
assert.deepEqual(detectFacade(unit).findings, []);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ── Strategy — switch dispatching behavior ────────────────────────────────
|
|
142
|
+
|
|
143
|
+
test('Strategy: switchBehaviorCallLine present fires', () => {
|
|
144
|
+
const unit = makeUnit({ members: [makeMember({ switchBehaviorCallLine: 9 })] });
|
|
145
|
+
assert.equal(detectStrategy(unit).findings.length, 1);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('Strategy: switchBehaviorCallLine null does not fire', () => {
|
|
149
|
+
const unit = makeUnit({ members: [makeMember({ switchBehaviorCallLine: null })] });
|
|
150
|
+
assert.deepEqual(detectStrategy(unit).findings, []);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ── Observer — >3 notify/update/inform/broadcast calls ────────────────────
|
|
154
|
+
|
|
155
|
+
test('Observer: 4 matching callee names (aggregated across unit) fires', () => {
|
|
156
|
+
const unit = makeUnit({
|
|
157
|
+
members: [
|
|
158
|
+
makeMember({ name: 'a', calleeNames: ['notifyAll', 'update'] }),
|
|
159
|
+
makeMember({ name: 'b', calleeNames: ['inform', 'broadcast'] }),
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
assert.equal(detectObserver(unit).findings.length, 1);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('Observer: boundary — exactly 3 matching calls does not fire', () => {
|
|
166
|
+
const unit = makeUnit({ members: [makeMember({ calleeNames: ['notify', 'update', 'inform'] })] });
|
|
167
|
+
assert.deepEqual(detectObserver(unit).findings, []);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ── Command — >4 occurrences of undo/redo/history/queue/execute ──────────
|
|
171
|
+
|
|
172
|
+
test('Command: >4 occurrences across name/callees/declaredNames/fieldAccess fires', () => {
|
|
173
|
+
const unit = makeUnit({
|
|
174
|
+
name: 'execute', // 1
|
|
175
|
+
members: [makeMember({
|
|
176
|
+
name: 'undo', // 2
|
|
177
|
+
calleeNames: ['redo'], // 3
|
|
178
|
+
declaredNames: [{ name: 'queue', line: 1 }], // 4
|
|
179
|
+
fieldAccess: ['history'], // 5
|
|
180
|
+
})],
|
|
181
|
+
});
|
|
182
|
+
const r = detectCommand(unit);
|
|
183
|
+
assert.equal(r.findings.length, 1);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('Command: boundary — exactly 4 occurrences does not fire', () => {
|
|
187
|
+
const unit = makeUnit({
|
|
188
|
+
name: 'Widget',
|
|
189
|
+
members: [makeMember({
|
|
190
|
+
name: 'undo', // 1
|
|
191
|
+
calleeNames: ['redo'], // 2
|
|
192
|
+
declaredNames: [{ name: 'queue', line: 1 }], // 3
|
|
193
|
+
fieldAccess: ['history'], // 4
|
|
194
|
+
})],
|
|
195
|
+
});
|
|
196
|
+
assert.deepEqual(detectCommand(unit).findings, []);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('Command: a bare fieldAccess-only reference (this.queue, never called/declared) still counts toward the total', () => {
|
|
200
|
+
const unit = makeUnit({
|
|
201
|
+
name: 'undo', // 1
|
|
202
|
+
members: [makeMember({
|
|
203
|
+
name: 'redo', // 2
|
|
204
|
+
calleeNames: ['execute'], // 3
|
|
205
|
+
fieldAccess: ['queue', 'history'], // 4, 5
|
|
206
|
+
})],
|
|
207
|
+
});
|
|
208
|
+
assert.equal(detectCommand(unit).findings.length, 1);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ── Template Method — >2 members call initialize/process/cleanup ─────────
|
|
212
|
+
|
|
213
|
+
test('Template Method: 3 members each calling an initialize/process/cleanup-named function fires', () => {
|
|
214
|
+
const unit = makeUnit({
|
|
215
|
+
members: [
|
|
216
|
+
makeMember({ name: 'a', calleeNames: ['initializeStep'] }),
|
|
217
|
+
makeMember({ name: 'b', calleeNames: ['processStep'] }),
|
|
218
|
+
makeMember({ name: 'c', calleeNames: ['cleanupStep'] }),
|
|
219
|
+
],
|
|
220
|
+
});
|
|
221
|
+
assert.equal(detectTemplateMethod(unit).findings.length, 1);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('Template Method: boundary — exactly 2 matching members does not fire', () => {
|
|
225
|
+
const unit = makeUnit({
|
|
226
|
+
members: [
|
|
227
|
+
makeMember({ name: 'a', calleeNames: ['initializeStep'] }),
|
|
228
|
+
makeMember({ name: 'b', calleeNames: ['processStep'] }),
|
|
229
|
+
],
|
|
230
|
+
});
|
|
231
|
+
assert.deepEqual(detectTemplateMethod(unit).findings, []);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ── patternScore — full aggregate, deterministic sort ─────────────────────
|
|
235
|
+
|
|
236
|
+
test('patternScore: a unit tripping multiple detectors sums totalFindings correctly and sorts findings by line', () => {
|
|
237
|
+
const unit = makeUnit({
|
|
238
|
+
hasGetInstanceMethod: true, // Singleton
|
|
239
|
+
members: [
|
|
240
|
+
makeMember({ name: 'constructor', paramCount: 5 }), // Builder
|
|
241
|
+
makeMember({ name: 'a', switchStatements: [{ line: 20, hasBehaviorCall: false, hasTypeCreation: true }] }), // Factory Method
|
|
242
|
+
makeMember({ name: 'b', switchBehaviorCallLine: 10 }), // Strategy
|
|
243
|
+
],
|
|
244
|
+
});
|
|
245
|
+
const r = patternScore(unit);
|
|
246
|
+
assert.equal(r.patterns['Singleton'].findings.length, 1);
|
|
247
|
+
assert.equal(r.patterns['Builder'].findings.length, 1);
|
|
248
|
+
assert.equal(r.patterns['Factory Method'].findings.length, 1);
|
|
249
|
+
assert.equal(r.patterns['Strategy'].findings.length, 1);
|
|
250
|
+
assert.equal(r.totalFindings, 4);
|
|
251
|
+
});
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
extractMethodOpportunities, extractClassOpportunities,
|
|
6
|
+
introduceParameterObjectOpportunities, replaceMagicNumberOpportunities,
|
|
7
|
+
consolidateDuplicateCodeOpportunities, decomposeConditionalOpportunities,
|
|
8
|
+
strategyTransformOpportunities, factoryTransformOpportunities,
|
|
9
|
+
nullObjectTransformOpportunities, estimateEffort, refactoringScore,
|
|
10
|
+
} from '../../scripts/lib/refactoring-scoring.mjs';
|
|
11
|
+
import { makeMember, makeUnit } from './fixtures.mjs';
|
|
12
|
+
|
|
13
|
+
// ── empty / trivial input ───────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
test('refactoringScore: zero members fires zero findings for all 9 rules, never throws', () => {
|
|
16
|
+
const unit = makeUnit({ members: [] });
|
|
17
|
+
const r = refactoringScore(unit);
|
|
18
|
+
assert.equal(r.totalFindings, 0);
|
|
19
|
+
assert.equal(Object.keys(r.rules).length, 9);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('refactoringScore: a single trivial member with no smells fires nothing', () => {
|
|
23
|
+
assert.equal(refactoringScore(makeUnit({ members: [makeMember()] })).totalFindings, 0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// ── extract-method (statementCount > 25) ──────────────────────────────────
|
|
27
|
+
|
|
28
|
+
test('extract-method: 26 statements fires', () => {
|
|
29
|
+
const unit = makeUnit({ members: [makeMember({ statementCount: 26 })] });
|
|
30
|
+
assert.equal(extractMethodOpportunities(unit).findings.length, 1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('extract-method: boundary — exactly 25 statements does not fire', () => {
|
|
34
|
+
const unit = makeUnit({ members: [makeMember({ statementCount: 25 })] });
|
|
35
|
+
assert.deepEqual(extractMethodOpportunities(unit).findings, []);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// ── extract-class (class with >15 members) ─────────────────────────────
|
|
39
|
+
|
|
40
|
+
test('extract-class: a class with 16 members fires', () => {
|
|
41
|
+
const members = Array.from({ length: 16 }, (_, i) => makeMember({ name: `m${i}` }));
|
|
42
|
+
const unit = makeUnit({ kind: 'class', members });
|
|
43
|
+
assert.equal(extractClassOpportunities(unit).findings.length, 1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('extract-class: boundary — a class with exactly 15 members does not fire', () => {
|
|
47
|
+
const members = Array.from({ length: 15 }, (_, i) => makeMember({ name: `m${i}` }));
|
|
48
|
+
const unit = makeUnit({ kind: 'class', members });
|
|
49
|
+
assert.deepEqual(extractClassOpportunities(unit).findings, []);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('extract-class: a MODULE with 16 top-level functions does not fire (class-only rule)', () => {
|
|
53
|
+
const members = Array.from({ length: 16 }, (_, i) => makeMember({ name: `m${i}` }));
|
|
54
|
+
const unit = makeUnit({ kind: 'module', members });
|
|
55
|
+
assert.deepEqual(extractClassOpportunities(unit).findings, []);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ── introduce-parameter-object (paramCount > 4) ──────────────────────────
|
|
59
|
+
|
|
60
|
+
test('introduce-parameter-object: 5 params fires', () => {
|
|
61
|
+
const unit = makeUnit({ members: [makeMember({ paramCount: 5 })] });
|
|
62
|
+
assert.equal(introduceParameterObjectOpportunities(unit).findings.length, 1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('introduce-parameter-object: boundary — exactly 4 params does not fire (differs from clean-code F2 at >3)', () => {
|
|
66
|
+
const unit = makeUnit({ members: [makeMember({ paramCount: 4 })] });
|
|
67
|
+
assert.deepEqual(introduceParameterObjectOpportunities(unit).findings, []);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ── replace-magic-number (unit-wide count > 5) ────────────────────────────
|
|
71
|
+
|
|
72
|
+
test('replace-magic-number: unit-wide total of 6 magic numbers fires, one finding per occurrence', () => {
|
|
73
|
+
const unit = makeUnit({
|
|
74
|
+
members: [makeMember({ magicNumbers: Array.from({ length: 6 }, (_, i) => ({ value: i + 10, line: i + 1 })) })],
|
|
75
|
+
});
|
|
76
|
+
const r = replaceMagicNumberOpportunities(unit);
|
|
77
|
+
assert.equal(r.findings.length, 6);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('replace-magic-number: boundary — unit-wide total of exactly 5 does not fire', () => {
|
|
81
|
+
const unit = makeUnit({
|
|
82
|
+
members: [makeMember({ magicNumbers: Array.from({ length: 5 }, (_, i) => ({ value: i + 10, line: i + 1 })) })],
|
|
83
|
+
});
|
|
84
|
+
assert.deepEqual(replaceMagicNumberOpportunities(unit).findings, []);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('replace-magic-number: aggregates ACROSS members, not per-member', () => {
|
|
88
|
+
const unit = makeUnit({
|
|
89
|
+
members: [
|
|
90
|
+
makeMember({ name: 'a', magicNumbers: [{ value: 1, line: 1 }, { value: 2, line: 2 }, { value: 3, line: 3 }] }),
|
|
91
|
+
makeMember({ name: 'b', magicNumbers: [{ value: 4, line: 4 }, { value: 5, line: 5 }, { value: 6, line: 6 }] }),
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
assert.equal(replaceMagicNumberOpportunities(unit).findings.length, 6);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ── consolidate-duplicate-code (>3 groups, each repeated >3x) ────────────
|
|
98
|
+
|
|
99
|
+
function repeatedStatementText(text, count, memberName = 'm') {
|
|
100
|
+
return Array.from({ length: count }, (_, i) => ({ text, line: i + 1 }));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
test('consolidate-duplicate-code: 4 distinct patterns each repeated 4x fires with 4 findings', () => {
|
|
104
|
+
const unit = makeUnit({
|
|
105
|
+
members: [makeMember({
|
|
106
|
+
statementTexts: [
|
|
107
|
+
...repeatedStatementText('const a = doSomethingLong();', 4),
|
|
108
|
+
...repeatedStatementText('const b = doAnotherThingLong();', 4),
|
|
109
|
+
...repeatedStatementText('const c = doThirdThingLong();', 4),
|
|
110
|
+
...repeatedStatementText('const d = doFourthThingLong();', 4),
|
|
111
|
+
],
|
|
112
|
+
})],
|
|
113
|
+
});
|
|
114
|
+
const r = consolidateDuplicateCodeOpportunities(unit);
|
|
115
|
+
assert.equal(r.findings.length, 4);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('consolidate-duplicate-code: boundary — exactly 3 distinct qualifying patterns does not fire', () => {
|
|
119
|
+
const unit = makeUnit({
|
|
120
|
+
members: [makeMember({
|
|
121
|
+
statementTexts: [
|
|
122
|
+
...repeatedStatementText('const a = doSomethingLong();', 4),
|
|
123
|
+
...repeatedStatementText('const b = doAnotherThingLong();', 4),
|
|
124
|
+
...repeatedStatementText('const c = doThirdThingLong();', 4),
|
|
125
|
+
],
|
|
126
|
+
})],
|
|
127
|
+
});
|
|
128
|
+
assert.deepEqual(consolidateDuplicateCodeOpportunities(unit).findings, []);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('consolidate-duplicate-code: boundary — a pattern repeated exactly 3x (not >3) does not count toward a group', () => {
|
|
132
|
+
const unit = makeUnit({
|
|
133
|
+
members: [makeMember({
|
|
134
|
+
statementTexts: [
|
|
135
|
+
...repeatedStatementText('const a = doSomethingLong();', 3),
|
|
136
|
+
...repeatedStatementText('const b = doAnotherThingLong();', 4),
|
|
137
|
+
...repeatedStatementText('const c = doThirdThingLong();', 4),
|
|
138
|
+
...repeatedStatementText('const d = doFourthThingLong();', 4),
|
|
139
|
+
],
|
|
140
|
+
})],
|
|
141
|
+
});
|
|
142
|
+
// only 3 groups actually qualify (>3 repeats) despite 4 distinct texts present
|
|
143
|
+
assert.deepEqual(consolidateDuplicateCodeOpportunities(unit).findings, []);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ── decompose-conditional (>2 complex conditionals per unit) ─────────────
|
|
147
|
+
|
|
148
|
+
test('decompose-conditional: 3 complex conditionals (unit-wide) fires', () => {
|
|
149
|
+
const unit = makeUnit({
|
|
150
|
+
members: [makeMember({ complexConditionals: [{ line: 1, length: 60 }, { line: 2, length: 70 }, { line: 3, length: 55 }] })],
|
|
151
|
+
});
|
|
152
|
+
assert.equal(decomposeConditionalOpportunities(unit).findings.length, 3);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('decompose-conditional: boundary — exactly 2 complex conditionals does not fire', () => {
|
|
156
|
+
const unit = makeUnit({
|
|
157
|
+
members: [makeMember({ complexConditionals: [{ line: 1, length: 60 }, { line: 2, length: 70 }] })],
|
|
158
|
+
});
|
|
159
|
+
assert.deepEqual(decomposeConditionalOpportunities(unit).findings, []);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ── strategy-transform (switch dispatching behavior) ──────────────────────
|
|
163
|
+
|
|
164
|
+
test('strategy-transform: switchStatements.hasBehaviorCall fires', () => {
|
|
165
|
+
const unit = makeUnit({ members: [makeMember({ switchStatements: [{ line: 4, hasBehaviorCall: true, hasTypeCreation: false }] })] });
|
|
166
|
+
assert.equal(strategyTransformOpportunities(unit).findings.length, 1);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('strategy-transform: hasBehaviorCall false does not fire', () => {
|
|
170
|
+
const unit = makeUnit({ members: [makeMember({ switchStatements: [{ line: 4, hasBehaviorCall: false, hasTypeCreation: false }] })] });
|
|
171
|
+
assert.deepEqual(strategyTransformOpportunities(unit).findings, []);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ── factory-transform (switch on type creating objects) ──────────────────
|
|
175
|
+
|
|
176
|
+
test('factory-transform: switchStatements.hasTypeCreation fires', () => {
|
|
177
|
+
const unit = makeUnit({ members: [makeMember({ switchStatements: [{ line: 4, hasBehaviorCall: false, hasTypeCreation: true }] })] });
|
|
178
|
+
assert.equal(factoryTransformOpportunities(unit).findings.length, 1);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('factory-transform: hasTypeCreation false does not fire', () => {
|
|
182
|
+
const unit = makeUnit({ members: [makeMember({ switchStatements: [{ line: 4, hasBehaviorCall: false, hasTypeCreation: false }] })] });
|
|
183
|
+
assert.deepEqual(factoryTransformOpportunities(unit).findings, []);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ── null-object-transform (>5 null checks per unit) ───────────────────────
|
|
187
|
+
|
|
188
|
+
test('null-object-transform: 6 null checks (unit-wide) fires', () => {
|
|
189
|
+
const unit = makeUnit({
|
|
190
|
+
members: [makeMember({ nullChecks: Array.from({ length: 6 }, (_, i) => ({ line: i + 1 })) })],
|
|
191
|
+
});
|
|
192
|
+
assert.equal(nullObjectTransformOpportunities(unit).findings.length, 6);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('null-object-transform: boundary — exactly 5 null checks does not fire', () => {
|
|
196
|
+
const unit = makeUnit({
|
|
197
|
+
members: [makeMember({ nullChecks: Array.from({ length: 5 }, (_, i) => ({ line: i + 1 })) })],
|
|
198
|
+
});
|
|
199
|
+
assert.deepEqual(nullObjectTransformOpportunities(unit).findings, []);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// ── estimateEffort — unmeasured, boundary, package-crossing ──────────────
|
|
203
|
+
|
|
204
|
+
test('estimateEffort: no referenceSites is reported unmeasured, never as zero effort', () => {
|
|
205
|
+
const r = estimateEffort({ unitPackage: 'packages/core', referenceSites: null });
|
|
206
|
+
assert.equal(r.effort, null);
|
|
207
|
+
assert.equal(r.confidence, 'unmeasured');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('estimateEffort: referenceCount -1 is unmeasured', () => {
|
|
211
|
+
const r = estimateEffort({ unitPackage: 'packages/core', referenceSites: { referenceCount: -1, files: [], kind: null } });
|
|
212
|
+
assert.equal(r.effort, null);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('estimateEffort: a reference site outside the unit package is "high" via package-boundary crossing', () => {
|
|
216
|
+
const r = estimateEffort({
|
|
217
|
+
unitPackage: 'packages/core',
|
|
218
|
+
referenceSites: { referenceCount: 2, files: ['/repo/packages/other/consumer.mjs'], kind: 'FunctionDeclaration' },
|
|
219
|
+
});
|
|
220
|
+
assert.equal(r.effort, 'high');
|
|
221
|
+
assert.equal(r.criterion, 'crosses a package boundary');
|
|
222
|
+
assert.equal(r.invariantCheckRequired, true);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('estimateEffort: boundary — 16 call sites, same package, is "high" via the >15 criterion', () => {
|
|
226
|
+
const files = Array.from({ length: 16 }, (_, i) => `/repo/packages/core/consumer${i}.mjs`);
|
|
227
|
+
const r = estimateEffort({ unitPackage: 'packages/core', referenceSites: { referenceCount: 16, files, kind: 'FunctionDeclaration' } });
|
|
228
|
+
assert.equal(r.effort, 'high');
|
|
229
|
+
assert.equal(r.criterion, '>15 call sites');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('estimateEffort: boundary — exactly 15 call sites, same package, is "medium"', () => {
|
|
233
|
+
const files = Array.from({ length: 15 }, (_, i) => `/repo/packages/core/consumer${i}.mjs`);
|
|
234
|
+
const r = estimateEffort({ unitPackage: 'packages/core', referenceSites: { referenceCount: 15, files, kind: 'FunctionDeclaration' } });
|
|
235
|
+
assert.equal(r.effort, 'medium');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('estimateEffort: boundary — exactly 4 call sites is "medium"', () => {
|
|
239
|
+
const files = Array.from({ length: 4 }, (_, i) => `/repo/packages/core/c${i}.mjs`);
|
|
240
|
+
const r = estimateEffort({ unitPackage: 'packages/core', referenceSites: { referenceCount: 4, files, kind: 'FunctionDeclaration' } });
|
|
241
|
+
assert.equal(r.effort, 'medium');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('estimateEffort: boundary — exactly 3 call sites is "low"', () => {
|
|
245
|
+
const files = Array.from({ length: 3 }, (_, i) => `/repo/packages/core/c${i}.mjs`);
|
|
246
|
+
const r = estimateEffort({ unitPackage: 'packages/core', referenceSites: { referenceCount: 3, files, kind: 'FunctionDeclaration' } });
|
|
247
|
+
assert.equal(r.effort, 'low');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ── refactoringScore — full aggregate ──────────────────────────────────
|
|
251
|
+
|
|
252
|
+
test('refactoringScore: a unit tripping several rules sums totalFindings correctly', () => {
|
|
253
|
+
const unit = makeUnit({
|
|
254
|
+
kind: 'class',
|
|
255
|
+
members: [
|
|
256
|
+
makeMember({ name: 'longMethod', statementCount: 30 }),
|
|
257
|
+
makeMember({ name: 'manyParams', paramCount: 6 }),
|
|
258
|
+
],
|
|
259
|
+
});
|
|
260
|
+
const r = refactoringScore(unit);
|
|
261
|
+
assert.equal(r.rules['extract-method'].findings.length, 1);
|
|
262
|
+
assert.equal(r.rules['introduce-parameter-object'].findings.length, 1);
|
|
263
|
+
assert.equal(r.totalFindings, 2);
|
|
264
|
+
});
|