@boyingliu01/xp-gate 0.8.8 → 0.8.10
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/bin/xp-gate.js +18 -0
- package/hooks/pre-commit +565 -120
- package/lib/arch.js +49 -0
- package/lib/check.js +50 -0
- package/lib/init.js +36 -0
- package/lib/principles.js +48 -0
- package/mock-policy/AGENTS.md +94 -0
- package/mock-policy/__tests__/config.test.ts +292 -0
- package/mock-policy/__tests__/integration.test.ts +600 -0
- package/mock-policy/__tests__/mock-decision-engine.test.ts +231 -0
- package/mock-policy/__tests__/scope-scanner.test.ts +535 -0
- package/mock-policy/config.ts +106 -0
- package/mock-policy/gate-m3.ts +233 -0
- package/mock-policy/mock-decision-engine.ts +155 -0
- package/mock-policy/schema.ts +22 -0
- package/mock-policy/scope-scanner.ts +288 -0
- package/mock-policy/types.ts +65 -0
- package/mutation/AGENTS.md +73 -0
- package/mutation/__tests__/detect-ai-test.test.ts +152 -0
- package/mutation/__tests__/gate-m.test.ts +194 -0
- package/mutation/detect-ai-test.ts +128 -0
- package/mutation/gate-m.ts +619 -0
- package/mutation/init-baseline.ts +192 -0
- package/mutation/stryker-types.ts +14 -0
- package/mutation/types.ts +70 -0
- package/mutation/update-baseline.ts +160 -0
- package/package.json +5 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +2 -2
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +7 -7
- package/plugins/claude-code/skills/delphi-review/INSTALL.md +1 -1
- package/plugins/claude-code/skills/delphi-review/SKILL.md +12 -12
- package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +83 -36
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/force-levels.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/phase-3-review.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/README.md +23 -7
- package/plugins/opencode/index.ts +36 -21
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +7 -7
- package/plugins/opencode/skills/delphi-review/INSTALL.md +1 -1
- package/plugins/opencode/skills/delphi-review/SKILL.md +12 -12
- package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +1 -1
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +83 -36
- package/plugins/opencode/skills/sprint-flow/SKILL.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/force-levels.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/phase-3-review.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +20 -0
- package/plugins/qoder/skills/delphi-review/AGENTS.md +7 -7
- package/plugins/qoder/skills/delphi-review/INSTALL.md +1 -1
- package/plugins/qoder/skills/delphi-review/SKILL.md +12 -12
- package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +1 -1
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +83 -36
- package/plugins/qoder/skills/sprint-flow/SKILL.md +51 -1
- package/plugins/qoder/skills/sprint-flow/references/force-levels.md +1 -1
- package/plugins/qoder/skills/sprint-flow/references/phase-3-review.md +2 -2
- package/plugins/qoder/skills/test-driven-development/SKILL.md +371 -0
- package/plugins/qoder/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +111 -0
- package/principles/__tests__/analyzer.test.ts +343 -0
- package/principles/__tests__/baseline-storage.test.ts +187 -0
- package/principles/__tests__/baseline.test.ts +328 -0
- package/principles/__tests__/boy-scout-integration.test.ts +264 -0
- package/principles/__tests__/boy-scout.test.ts +646 -0
- package/principles/__tests__/config.test.ts +78 -0
- package/principles/__tests__/index.test.ts +81 -0
- package/principles/__tests__/lint-baseline.test.ts +314 -0
- package/principles/__tests__/reporter.test.ts +832 -0
- package/principles/__tests__/types.test.ts +101 -0
- package/principles/adapters/__tests__/base.test.ts +95 -0
- package/principles/adapters/__tests__/cpp.test.ts +290 -0
- package/principles/adapters/__tests__/dart.test.ts +84 -0
- package/principles/adapters/__tests__/go.test.ts +68 -0
- package/principles/adapters/__tests__/java.test.ts +68 -0
- package/principles/adapters/__tests__/kotlin.test.ts +76 -0
- package/principles/adapters/__tests__/objectivec.test.ts +453 -0
- package/principles/adapters/__tests__/python.test.ts +84 -0
- package/principles/adapters/__tests__/swift.test.ts +68 -0
- package/principles/adapters/__tests__/typescript.test.ts +116 -0
- package/principles/adapters/base.ts +124 -0
- package/principles/adapters/cpp.ts +115 -0
- package/principles/adapters/dart.ts +44 -0
- package/principles/adapters/go.ts +55 -0
- package/principles/adapters/java.ts +56 -0
- package/principles/adapters/kotlin.ts +54 -0
- package/principles/adapters/objectivec.ts +82 -0
- package/principles/adapters/python.ts +83 -0
- package/principles/adapters/swift.ts +40 -0
- package/principles/adapters/typescript.ts +66 -0
- package/principles/analyzer.ts +190 -0
- package/principles/baseline.ts +273 -0
- package/principles/boy-scout.ts +416 -0
- package/principles/config.ts +118 -0
- package/principles/index.ts +87 -0
- package/principles/lint-baseline.ts +385 -0
- package/principles/reporter.ts +188 -0
- package/principles/rules/__tests__/clean-code/code-duplication.test.ts +84 -0
- package/principles/rules/__tests__/clean-code/deep-nesting.test.ts +74 -0
- package/principles/rules/__tests__/clean-code/god-class.test.ts +111 -0
- package/principles/rules/__tests__/clean-code/large-file.test.ts +73 -0
- package/principles/rules/__tests__/clean-code/long-function.test.ts +83 -0
- package/principles/rules/__tests__/clean-code/magic-numbers.test.ts +68 -0
- package/principles/rules/__tests__/clean-code/many-exports.test.ts +118 -0
- package/principles/rules/__tests__/clean-code/missing-error-handling.test.ts +173 -0
- package/principles/rules/__tests__/clean-code/too-many-params.test.ts +60 -0
- package/principles/rules/__tests__/clean-code/unused-imports.test.ts +107 -0
- package/principles/rules/__tests__/index.test.ts +49 -0
- package/principles/rules/__tests__/solid/dip.test.ts +121 -0
- package/principles/rules/__tests__/solid/isp.test.ts +60 -0
- package/principles/rules/__tests__/solid/lsp.test.ts +184 -0
- package/principles/rules/__tests__/solid/ocp.test.ts +69 -0
- package/principles/rules/__tests__/solid/srp.test.ts +89 -0
- package/principles/rules/clean-code/code-duplication.ts +31 -0
- package/principles/rules/clean-code/deep-nesting.ts +38 -0
- package/principles/rules/clean-code/god-class.ts +54 -0
- package/principles/rules/clean-code/large-file.ts +34 -0
- package/principles/rules/clean-code/long-function.ts +47 -0
- package/principles/rules/clean-code/magic-numbers.ts +59 -0
- package/principles/rules/clean-code/many-exports.ts +40 -0
- package/principles/rules/clean-code/missing-error-handling.ts +33 -0
- package/principles/rules/clean-code/too-many-params.ts +35 -0
- package/principles/rules/clean-code/unused-imports.ts +33 -0
- package/principles/rules/index.ts +61 -0
- package/principles/rules/solid/dip.ts +47 -0
- package/principles/rules/solid/isp.ts +37 -0
- package/principles/rules/solid/lsp.ts +79 -0
- package/principles/rules/solid/ocp.ts +40 -0
- package/principles/rules/solid/srp.ts +44 -0
- package/principles/types.ts +27 -0
- package/skills/delphi-review/AGENTS.md +7 -7
- package/skills/delphi-review/INSTALL.md +1 -1
- package/skills/delphi-review/SKILL.md +12 -12
- package/skills/delphi-review/references/code-walkthrough.md +1 -1
- package/skills/sprint-flow/AGENTS.md +83 -36
- package/skills/sprint-flow/SKILL.md +1 -1
- package/skills/sprint-flow/references/force-levels.md +1 -1
- package/skills/sprint-flow/references/phase-3-review.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('../../../config', () => ({
|
|
4
|
+
getDefaultConfig: () => ({
|
|
5
|
+
rules: {
|
|
6
|
+
'clean-code': {
|
|
7
|
+
'many-exports': {
|
|
8
|
+
enabled: true,
|
|
9
|
+
// threshold intentionally omitted -> exercises ?? 10 fallback branch
|
|
10
|
+
severity: 'warning',
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import { manyExportsRule } from '../../clean-code/many-exports';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @test REQ-174 Branch coverage for many-exports rule
|
|
21
|
+
* @intent Cover every branch in many-exports.ts (extractExports presence,
|
|
22
|
+
* threshold boundary, empty/undefined exports, first-export line fallback,
|
|
23
|
+
* and try/catch fallback) so branch coverage reaches >=80%.
|
|
24
|
+
* @covers AC-174-01, AC-174-02, AC-174-03, AC-174-04
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const baseAdapter = {
|
|
28
|
+
detectLanguage: () => 'typescript',
|
|
29
|
+
parseAST: () => undefined,
|
|
30
|
+
extractFunctions: () => [],
|
|
31
|
+
extractClasses: () => [],
|
|
32
|
+
countLines: () => 0,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe('manyExportsRule', () => {
|
|
36
|
+
it('has correct rule metadata', () => {
|
|
37
|
+
expect(manyExportsRule.id).toBe('clean-code.many-exports');
|
|
38
|
+
expect(manyExportsRule.name).toBe('Many Exports Rule');
|
|
39
|
+
expect(manyExportsRule.threshold).toBe(10);
|
|
40
|
+
expect(manyExportsRule.severity).toBe('warning');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns no violations when adapter lacks extractExports (falsy branch)', () => {
|
|
44
|
+
// typedAdapter.extractExports is undefined -> exports = []
|
|
45
|
+
const violations = manyExportsRule.check('file.ts', baseAdapter as never);
|
|
46
|
+
expect(violations).toHaveLength(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns no violations when extractExports returns empty array', () => {
|
|
50
|
+
const adapter: never = {
|
|
51
|
+
...baseAdapter,
|
|
52
|
+
extractExports: () => [],
|
|
53
|
+
} as never;
|
|
54
|
+
const violations = manyExportsRule.check('file.ts', adapter);
|
|
55
|
+
expect(violations).toHaveLength(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns no violations when export count equals threshold (boundary)', () => {
|
|
59
|
+
const adapter: never = {
|
|
60
|
+
...baseAdapter,
|
|
61
|
+
extractExports: () =>
|
|
62
|
+
Array.from({ length: 10 }, (_, i) => ({ line: i + 1 })),
|
|
63
|
+
} as never;
|
|
64
|
+
const violations = manyExportsRule.check('file.ts', adapter);
|
|
65
|
+
expect(violations).toHaveLength(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('returns violation when export count exceeds threshold', () => {
|
|
69
|
+
const adapter: never = {
|
|
70
|
+
...baseAdapter,
|
|
71
|
+
extractExports: () =>
|
|
72
|
+
Array.from({ length: 11 }, (_, i) => ({ line: i + 5 })),
|
|
73
|
+
} as never;
|
|
74
|
+
const violations = manyExportsRule.check('file.ts', adapter);
|
|
75
|
+
expect(violations).toHaveLength(1);
|
|
76
|
+
expect(violations[0]).toEqual({
|
|
77
|
+
file: 'file.ts',
|
|
78
|
+
line: 5,
|
|
79
|
+
ruleId: 'clean-code.many-exports',
|
|
80
|
+
message:
|
|
81
|
+
'Module has too many exports: 11 (maximum: 10). Consider splitting into focused sub-modules.',
|
|
82
|
+
severity: 'warning',
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('falls back to line 1 when first export line is missing/zero', () => {
|
|
87
|
+
const adapter: never = {
|
|
88
|
+
...baseAdapter,
|
|
89
|
+
// first export has line 0 -> `exports[0]?.line || 1` selects 1
|
|
90
|
+
extractExports: () =>
|
|
91
|
+
Array.from({ length: 11 }, () => ({ line: 0 })),
|
|
92
|
+
} as never;
|
|
93
|
+
const violations = manyExportsRule.check('file.ts', adapter);
|
|
94
|
+
expect(violations).toHaveLength(1);
|
|
95
|
+
expect(violations[0].line).toBe(1);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('returns empty violations when extractExports returns undefined', () => {
|
|
99
|
+
// covers the `exports && ...` short-circuit false branch
|
|
100
|
+
const adapter: never = {
|
|
101
|
+
...baseAdapter,
|
|
102
|
+
extractExports: () => undefined,
|
|
103
|
+
} as never;
|
|
104
|
+
const violations = manyExportsRule.check('file.ts', adapter);
|
|
105
|
+
expect(violations).toHaveLength(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('returns empty violations when adapter throws (catch branch)', () => {
|
|
109
|
+
const adapter: never = {
|
|
110
|
+
...baseAdapter,
|
|
111
|
+
extractExports: () => {
|
|
112
|
+
throw new Error('boom');
|
|
113
|
+
},
|
|
114
|
+
} as never;
|
|
115
|
+
const violations = manyExportsRule.check('file.ts', adapter);
|
|
116
|
+
expect(violations).toHaveLength(0);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { missingErrorHandlingRule } from '../../clean-code/missing-error-handling';
|
|
3
|
+
|
|
4
|
+
const mockAdapter = {
|
|
5
|
+
detectLanguage: () => 'typescript',
|
|
6
|
+
parseAST: () => undefined,
|
|
7
|
+
extractFunctions: () => [],
|
|
8
|
+
extractClasses: () => [],
|
|
9
|
+
countLines: () => 0
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('missing-error-handling.ts - Missing Error Handling Rule', () => {
|
|
13
|
+
it('should detect async function with fetch without try-catch', () => {
|
|
14
|
+
const mockAdapterWithIO = {
|
|
15
|
+
...mockAdapter,
|
|
16
|
+
extractFunctions: () => [{
|
|
17
|
+
name: 'loadData',
|
|
18
|
+
startLine: 10,
|
|
19
|
+
code: `
|
|
20
|
+
async function loadData(url: string) {
|
|
21
|
+
const response = await fetch(url);
|
|
22
|
+
return response.json();
|
|
23
|
+
}
|
|
24
|
+
`,
|
|
25
|
+
isAsync: true,
|
|
26
|
+
ioOperations: ['fetch']
|
|
27
|
+
}]
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const violations = missingErrorHandlingRule.check('test.ts', mockAdapterWithIO as never);
|
|
31
|
+
|
|
32
|
+
expect(violations.length).toBe(1);
|
|
33
|
+
expect(violations[0].ruleId).toBe('clean-code.missing-error-handling');
|
|
34
|
+
expect(violations[0].message).toContain('fetch');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should detect async function with fs.readFile without try-catch', () => {
|
|
38
|
+
const mockAdapterWithIO = {
|
|
39
|
+
...mockAdapter,
|
|
40
|
+
extractFunctions: () => [{
|
|
41
|
+
name: 'readConfig',
|
|
42
|
+
startLine: 20,
|
|
43
|
+
code: `
|
|
44
|
+
async function readConfig(path: string) {
|
|
45
|
+
const data = await fs.readFile(path, 'utf-8');
|
|
46
|
+
return JSON.parse(data);
|
|
47
|
+
}
|
|
48
|
+
`,
|
|
49
|
+
isAsync: true,
|
|
50
|
+
ioOperations: ['fs.readFile']
|
|
51
|
+
}]
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const violations = missingErrorHandlingRule.check('test.ts', mockAdapterWithIO as never);
|
|
55
|
+
|
|
56
|
+
expect(violations.length).toBe(1);
|
|
57
|
+
expect(violations[0].ruleId).toBe('clean-code.missing-error-handling');
|
|
58
|
+
expect(violations[0].message).toContain('fs.readFile');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should pass for async function with try-catch wrapping IO', () => {
|
|
62
|
+
const mockAdapterWithHandledIO = {
|
|
63
|
+
...mockAdapter,
|
|
64
|
+
extractFunctions: () => [{
|
|
65
|
+
name: 'loadDataSafe',
|
|
66
|
+
startLine: 30,
|
|
67
|
+
code: `
|
|
68
|
+
async function loadDataSafe(url: string) {
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(url);
|
|
71
|
+
return response.json();
|
|
72
|
+
} catch (error) {
|
|
73
|
+
logger.error('Failed to load', error);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
`,
|
|
78
|
+
isAsync: true,
|
|
79
|
+
ioOperations: ['fetch'],
|
|
80
|
+
hasTryCatch: true
|
|
81
|
+
}]
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const violations = missingErrorHandlingRule.check('test.ts', mockAdapterWithHandledIO as never);
|
|
85
|
+
|
|
86
|
+
expect(violations.length).toBe(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should pass for pure function without IO operations', () => {
|
|
90
|
+
const mockAdapterWithPureFunction = {
|
|
91
|
+
...mockAdapter,
|
|
92
|
+
extractFunctions: () => [{
|
|
93
|
+
name: 'calculateTotal',
|
|
94
|
+
startLine: 40,
|
|
95
|
+
code: `
|
|
96
|
+
function calculateTotal(items: Item[]) {
|
|
97
|
+
return items.reduce((sum, item) => sum + item.price, 0);
|
|
98
|
+
}
|
|
99
|
+
`,
|
|
100
|
+
isAsync: false,
|
|
101
|
+
ioOperations: []
|
|
102
|
+
}]
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const violations = missingErrorHandlingRule.check('test.ts', mockAdapterWithPureFunction as never);
|
|
106
|
+
|
|
107
|
+
expect(violations.length).toBe(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should detect axios call without error handling', () => {
|
|
111
|
+
const mockAdapterWithAxios = {
|
|
112
|
+
...mockAdapter,
|
|
113
|
+
extractFunctions: () => [{
|
|
114
|
+
name: 'fetchUser',
|
|
115
|
+
startLine: 50,
|
|
116
|
+
code: `
|
|
117
|
+
async function fetchUser(id: string) {
|
|
118
|
+
const response = await axios.get('/api/users/' + id);
|
|
119
|
+
return response.data;
|
|
120
|
+
}
|
|
121
|
+
`,
|
|
122
|
+
isAsync: true,
|
|
123
|
+
ioOperations: ['axios']
|
|
124
|
+
}]
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const violations = missingErrorHandlingRule.check('test.ts', mockAdapterWithAxios as never);
|
|
128
|
+
|
|
129
|
+
expect(violations.length).toBe(1);
|
|
130
|
+
expect(violations[0].ruleId).toBe('clean-code.missing-error-handling');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should use severity from config', () => {
|
|
134
|
+
expect(missingErrorHandlingRule.severity).toBe('warning');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should return empty array when adapter.extractFunctions throws', () => {
|
|
138
|
+
const mockAdapterThrowing = {
|
|
139
|
+
...mockAdapter,
|
|
140
|
+
extractFunctions: () => { throw new Error('Parse error'); }
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const violations = missingErrorHandlingRule.check('test.ts', mockAdapterThrowing as never);
|
|
144
|
+
|
|
145
|
+
expect(violations).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should detect multiple IO operations in one function', () => {
|
|
149
|
+
const mockAdapterWithMultipleIO = {
|
|
150
|
+
...mockAdapter,
|
|
151
|
+
extractFunctions: () => [{
|
|
152
|
+
name: 'processData',
|
|
153
|
+
startLine: 60,
|
|
154
|
+
code: `
|
|
155
|
+
async function processData() {
|
|
156
|
+
const response = await fetch('/api/data');
|
|
157
|
+
const data = await response.json();
|
|
158
|
+
await fs.writeFile('output.json', JSON.stringify(data));
|
|
159
|
+
return data;
|
|
160
|
+
}
|
|
161
|
+
`,
|
|
162
|
+
isAsync: true,
|
|
163
|
+
ioOperations: ['fetch', 'fs.writeFile']
|
|
164
|
+
}]
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const violations = missingErrorHandlingRule.check('test.ts', mockAdapterWithMultipleIO as never);
|
|
168
|
+
|
|
169
|
+
// Should detect at least one violation (function needs try-catch)
|
|
170
|
+
expect(violations.length).toBeGreaterThanOrEqual(1);
|
|
171
|
+
expect(violations[0].ruleId).toBe('clean-code.missing-error-handling');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { tooManyParamsRule } from '../../clean-code/too-many-params';
|
|
3
|
+
|
|
4
|
+
const mockAdapter = {
|
|
5
|
+
detectLanguage: () => 'typescript',
|
|
6
|
+
parseAST: () => undefined,
|
|
7
|
+
extractFunctions: () => [],
|
|
8
|
+
extractClasses: () => [],
|
|
9
|
+
countLines: () => 0
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('too-many-params.ts - Too Many Parameters Rule', () => {
|
|
13
|
+
it('should detect function with more than 7 parameters', () => {
|
|
14
|
+
const mockAdapterWithManyParams = {
|
|
15
|
+
...mockAdapter,
|
|
16
|
+
extractFunctions: () => [{
|
|
17
|
+
name: 'lotsOfParams',
|
|
18
|
+
startLine: 1,
|
|
19
|
+
params: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'],
|
|
20
|
+
paramCount: 8
|
|
21
|
+
}]
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const violations = tooManyParamsRule.check('test.ts', mockAdapterWithManyParams as never);
|
|
25
|
+
|
|
26
|
+
expect(violations.length).toBe(1);
|
|
27
|
+
expect(violations[0].ruleId).toBe('clean-code.too-many-params');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should pass for function with acceptable parameters', () => {
|
|
31
|
+
const mockAdapterWithNormalParams = {
|
|
32
|
+
...mockAdapter,
|
|
33
|
+
extractFunctions: () => [{
|
|
34
|
+
name: 'normalParams',
|
|
35
|
+
startLine: 1,
|
|
36
|
+
paramCount: 5
|
|
37
|
+
}]
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const violations = tooManyParamsRule.check('test.ts', mockAdapterWithNormalParams as never);
|
|
41
|
+
|
|
42
|
+
expect(violations.length).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should use threshold from config', () => {
|
|
46
|
+
expect(tooManyParamsRule.threshold).toBe(7);
|
|
47
|
+
expect(tooManyParamsRule.severity).toBe('info');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should return empty violations when adapter throws error', () => {
|
|
51
|
+
const mockAdapterThatThrows = {
|
|
52
|
+
...mockAdapter,
|
|
53
|
+
extractFunctions: () => { throw new Error('Adapter failed'); }
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const violations = tooManyParamsRule.check('test.ts', mockAdapterThatThrows as never);
|
|
57
|
+
|
|
58
|
+
expect(violations.length).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { unusedImportsRule } from '../../clean-code/unused-imports';
|
|
3
|
+
|
|
4
|
+
const mockAdapter = {
|
|
5
|
+
detectLanguage: () => 'typescript',
|
|
6
|
+
parseAST: () => undefined,
|
|
7
|
+
extractFunctions: () => [],
|
|
8
|
+
extractClasses: () => [],
|
|
9
|
+
countLines: () => 0
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('unused-imports.ts - Unused Imports Rule', () => {
|
|
13
|
+
it('should detect unused value import', () => {
|
|
14
|
+
const mockAdapterWithUnusedImport = {
|
|
15
|
+
...mockAdapter,
|
|
16
|
+
imports: [
|
|
17
|
+
{ name: 'lodash', line: 1, type: 'value', used: false },
|
|
18
|
+
{ name: 'fs', line: 2, type: 'value', used: true }
|
|
19
|
+
]
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const violations = unusedImportsRule.check('test.ts', mockAdapterWithUnusedImport as never);
|
|
23
|
+
|
|
24
|
+
expect(violations.length).toBe(1);
|
|
25
|
+
expect(violations[0].ruleId).toBe('clean-code.unused-imports');
|
|
26
|
+
expect(violations[0].message).toContain('lodash');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should pass for used imports', () => {
|
|
30
|
+
const mockAdapterWithUsedImports = {
|
|
31
|
+
...mockAdapter,
|
|
32
|
+
imports: [
|
|
33
|
+
{ name: 'fs', line: 1, type: 'value', used: true },
|
|
34
|
+
{ name: 'path', line: 2, type: 'value', used: true }
|
|
35
|
+
]
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const violations = unusedImportsRule.check('test.ts', mockAdapterWithUsedImports as never);
|
|
39
|
+
|
|
40
|
+
expect(violations.length).toBe(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should skip type-only imports (TypeScript)', () => {
|
|
44
|
+
const mockAdapterWithTypeImports = {
|
|
45
|
+
...mockAdapter,
|
|
46
|
+
imports: [
|
|
47
|
+
{ name: 'User', line: 1, type: 'type', used: false },
|
|
48
|
+
{ name: 'fs', line: 2, type: 'value', used: true }
|
|
49
|
+
]
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const violations = unusedImportsRule.check('test.ts', mockAdapterWithTypeImports as never);
|
|
53
|
+
|
|
54
|
+
expect(violations.length).toBe(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should detect multiple unused imports', () => {
|
|
58
|
+
const mockAdapterWithMultipleUnused = {
|
|
59
|
+
...mockAdapter,
|
|
60
|
+
imports: [
|
|
61
|
+
{ name: 'lodash', line: 1, type: 'value', used: false },
|
|
62
|
+
{ name: 'axios', line: 2, type: 'value', used: false },
|
|
63
|
+
{ name: 'fs', line: 3, type: 'value', used: true }
|
|
64
|
+
]
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const violations = unusedImportsRule.check('test.ts', mockAdapterWithMultipleUnused as never);
|
|
68
|
+
|
|
69
|
+
expect(violations.length).toBe(2);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should use severity from config', () => {
|
|
73
|
+
expect(unusedImportsRule.severity).toBe('info');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should return empty array when adapter.imports is undefined', () => {
|
|
77
|
+
const violations = unusedImportsRule.check('test.ts', mockAdapter as never);
|
|
78
|
+
|
|
79
|
+
expect(violations).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should return empty array when adapter throws', () => {
|
|
83
|
+
const mockAdapterThrowing = {
|
|
84
|
+
...mockAdapter,
|
|
85
|
+
imports: () => { throw new Error('Parse error'); }
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const violations = unusedImportsRule.check('test.ts', mockAdapterThrowing as never);
|
|
89
|
+
|
|
90
|
+
expect(violations).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should handle namespace imports', () => {
|
|
94
|
+
const mockAdapterWithNamespaceImport = {
|
|
95
|
+
...mockAdapter,
|
|
96
|
+
imports: [
|
|
97
|
+
{ name: '* as fs', line: 1, type: 'namespace', used: true },
|
|
98
|
+
{ name: '* as lodash', line: 2, type: 'namespace', used: false }
|
|
99
|
+
]
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const violations = unusedImportsRule.check('test.ts', mockAdapterWithNamespaceImport as never);
|
|
103
|
+
|
|
104
|
+
expect(violations.length).toBe(1);
|
|
105
|
+
expect(violations[0].message).toContain('lodash');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as rulesIndex from '../index';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @test REQ-COV-002 Coverage: src/principles/rules/index.ts
|
|
6
|
+
* @intent Verify all 15 rule re-exports are accessible
|
|
7
|
+
* @covers AC-COV-002
|
|
8
|
+
*/
|
|
9
|
+
describe('principles/rules/index', () => {
|
|
10
|
+
const expectedCleanCodeRules = [
|
|
11
|
+
'longFunctionRule',
|
|
12
|
+
'largeFileRule',
|
|
13
|
+
'magicNumbersRule',
|
|
14
|
+
'godClassRule',
|
|
15
|
+
'deepNestingRule',
|
|
16
|
+
'tooManyParamsRule',
|
|
17
|
+
'missingErrorHandlingRule',
|
|
18
|
+
'unusedImportsRule',
|
|
19
|
+
'codeDuplicationRule',
|
|
20
|
+
'manyExportsRule',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const expectedSolidRules = ['srpRule', 'ocpRule', 'lspRule', 'ispRule', 'dipRule'];
|
|
24
|
+
|
|
25
|
+
it('exports all 10 clean-code rules', () => {
|
|
26
|
+
for (const name of expectedCleanCodeRules) {
|
|
27
|
+
expect(rulesIndex).toHaveProperty(name);
|
|
28
|
+
expect(typeof (rulesIndex as Record<string, unknown>)[name]).toBe('object');
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('exports all 5 SOLID rules', () => {
|
|
33
|
+
for (const name of expectedSolidRules) {
|
|
34
|
+
expect(rulesIndex).toHaveProperty(name);
|
|
35
|
+
expect(typeof (rulesIndex as Record<string, unknown>)[name]).toBe('object');
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('each exported rule has an id and check function', () => {
|
|
40
|
+
const allRules = [...expectedCleanCodeRules, ...expectedSolidRules];
|
|
41
|
+
for (const name of allRules) {
|
|
42
|
+
const rule = (rulesIndex as Record<string, { id?: string; check?: unknown }>)[name];
|
|
43
|
+
expect(rule.id).toBeDefined();
|
|
44
|
+
expect(typeof rule.id).toBe('string');
|
|
45
|
+
expect(rule.check).toBeDefined();
|
|
46
|
+
expect(typeof rule.check).toBe('function');
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { dipRule } from '../../solid/dip';
|
|
3
|
+
|
|
4
|
+
const mockAdapter = {
|
|
5
|
+
detectLanguage: () => 'typescript',
|
|
6
|
+
parseAST: () => undefined,
|
|
7
|
+
extractFunctions: () => [],
|
|
8
|
+
extractClasses: () => [],
|
|
9
|
+
countLines: () => 0
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('dip.ts - Dependency Inversion Principle Rule', () => {
|
|
13
|
+
it('should detect direct instantiation in business logic', () => {
|
|
14
|
+
const mockAdapterWithDirectInstantiation = {
|
|
15
|
+
...mockAdapter,
|
|
16
|
+
extractClasses: () => [{
|
|
17
|
+
name: 'UserService',
|
|
18
|
+
line: 1,
|
|
19
|
+
code: `
|
|
20
|
+
class UserService {
|
|
21
|
+
private repo = new UserRepository();
|
|
22
|
+
|
|
23
|
+
getUser(id: string) {
|
|
24
|
+
return this.repo.findById(id);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
`
|
|
28
|
+
}]
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const violations = dipRule.check('test.ts', mockAdapterWithDirectInstantiation as never);
|
|
32
|
+
|
|
33
|
+
expect(violations.length).toBeGreaterThan(0);
|
|
34
|
+
expect(violations[0].ruleId).toBe('solid.dip');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should pass for dependency injection pattern', () => {
|
|
38
|
+
const mockAdapterWithDI = {
|
|
39
|
+
...mockAdapter,
|
|
40
|
+
extractClasses: () => [{
|
|
41
|
+
name: 'UserService',
|
|
42
|
+
line: 1,
|
|
43
|
+
code: `
|
|
44
|
+
class UserService {
|
|
45
|
+
constructor(private repo: IRepository) {}
|
|
46
|
+
|
|
47
|
+
getUser(id: string) {
|
|
48
|
+
return this.repo.findById(id);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
`
|
|
52
|
+
}]
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const violations = dipRule.check('test.ts', mockAdapterWithDI as never);
|
|
56
|
+
|
|
57
|
+
expect(violations.length).toBe(0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should exclude value objects (Date, Map, Set, etc)', () => {
|
|
61
|
+
const mockAdapterWithValueObjects = {
|
|
62
|
+
...mockAdapter,
|
|
63
|
+
extractClasses: () => [{
|
|
64
|
+
name: 'DateService',
|
|
65
|
+
line: 1,
|
|
66
|
+
code: `
|
|
67
|
+
class DateService {
|
|
68
|
+
formatDate(date: Date) {
|
|
69
|
+
return new Intl.DateTimeFormat().format(date);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
`
|
|
73
|
+
}]
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const violations = dipRule.check('test.ts', mockAdapterWithValueObjects as never);
|
|
77
|
+
|
|
78
|
+
expect(violations.length).toBe(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should use threshold from config', () => {
|
|
82
|
+
expect(dipRule.severity).toBe('warning');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should return empty violations when adapter throws error', () => {
|
|
86
|
+
const mockAdapterThatThrows = {
|
|
87
|
+
...mockAdapter,
|
|
88
|
+
extractClasses: () => { throw new Error('Adapter failed'); }
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const violations = dipRule.check('test.ts', mockAdapterThatThrows as never);
|
|
92
|
+
|
|
93
|
+
expect(violations.length).toBe(0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should exclude built-in types (Date, Map, Set, etc.)', () => {
|
|
97
|
+
const mockAdapterWithBuiltins = {
|
|
98
|
+
...mockAdapter,
|
|
99
|
+
extractClasses: () => [{
|
|
100
|
+
name: 'Service',
|
|
101
|
+
line: 1,
|
|
102
|
+
code: `class Service { run() { return new Date(); } }`
|
|
103
|
+
}]
|
|
104
|
+
};
|
|
105
|
+
const violations = dipRule.check('test.ts', mockAdapterWithBuiltins as never);
|
|
106
|
+
expect(violations.length).toBe(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should exclude Factory and Builder patterns', () => {
|
|
110
|
+
const mockAdapterWithFactory = {
|
|
111
|
+
...mockAdapter,
|
|
112
|
+
extractClasses: () => [{
|
|
113
|
+
name: 'Client',
|
|
114
|
+
line: 1,
|
|
115
|
+
code: `class Client { create() { return new UserFactory(); } }`
|
|
116
|
+
}]
|
|
117
|
+
};
|
|
118
|
+
const violations = dipRule.check('test.ts', mockAdapterWithFactory as never);
|
|
119
|
+
expect(violations.length).toBe(0);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ispRule } from '../../solid/isp';
|
|
3
|
+
|
|
4
|
+
const mockAdapter = {
|
|
5
|
+
detectLanguage: () => 'typescript',
|
|
6
|
+
parseAST: () => undefined,
|
|
7
|
+
extractFunctions: () => [],
|
|
8
|
+
extractClasses: () => [],
|
|
9
|
+
countLines: () => 0
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('isp.ts - Interface Segregation Principle Rule', () => {
|
|
13
|
+
it('should detect interface with too many methods (>10)', () => {
|
|
14
|
+
const mockAdapterWithFatInterface = {
|
|
15
|
+
...mockAdapter,
|
|
16
|
+
extractInterfaces: () => [{
|
|
17
|
+
name: 'FatInterface',
|
|
18
|
+
line: 1,
|
|
19
|
+
methodCount: 12
|
|
20
|
+
}]
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const violations = ispRule.check('test.ts', mockAdapterWithFatInterface as never);
|
|
24
|
+
|
|
25
|
+
expect(violations.length).toBeGreaterThan(0);
|
|
26
|
+
expect(violations[0].ruleId).toBe('solid.isp');
|
|
27
|
+
expect(violations[0].message).toContain('12');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should pass for focused interface', () => {
|
|
31
|
+
const mockAdapterWithFocusedInterface = {
|
|
32
|
+
...mockAdapter,
|
|
33
|
+
extractInterfaces: () => [{
|
|
34
|
+
name: 'ReaderInterface',
|
|
35
|
+
line: 1,
|
|
36
|
+
methodCount: 5
|
|
37
|
+
}]
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const violations = ispRule.check('test.ts', mockAdapterWithFocusedInterface as never);
|
|
41
|
+
|
|
42
|
+
expect(violations.length).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should use threshold from config', () => {
|
|
46
|
+
expect(ispRule.threshold).toBe(10);
|
|
47
|
+
expect(ispRule.severity).toBe('info');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should return empty violations when adapter throws error', () => {
|
|
51
|
+
const mockAdapterThatThrows = {
|
|
52
|
+
...mockAdapter,
|
|
53
|
+
extractInterfaces: () => { throw new Error('Adapter failed'); }
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const violations = ispRule.check('test.ts', mockAdapterThatThrows as never);
|
|
57
|
+
|
|
58
|
+
expect(violations.length).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
});
|