@archest/vitest 1.0.0 → 1.0.1
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/index.js +13 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +776 -489
- package/dist/index.mjs.map +1 -1
- package/dist/src/core/classes/classCheckExtendClass.d.ts +3 -0
- package/dist/src/core/classes/classCheckHaveMaxCyclomaticComplexity.d.ts +3 -0
- package/dist/src/core/classes/classCheckHaveModifier.d.ts +3 -0
- package/dist/src/core/classes/classCheckHaveNameMatchingFileName.d.ts +3 -0
- package/dist/src/core/classes/classCheckImplementInterface.d.ts +3 -0
- package/dist/src/core/classes/classCheckMatchNamePattern.d.ts +3 -0
- package/dist/src/core/classes/classCheckResideInFolder.d.ts +3 -0
- package/dist/src/core/classes/locateClasses.d.ts +3 -0
- package/dist/src/core/classes/types.d.ts +14 -0
- package/dist/src/core/files/checkDependOnFilesInFolder.d.ts +3 -0
- package/dist/src/core/files/fileCheckBeFreeOfCycles.d.ts +3 -0
- package/dist/src/core/files/fileCheckHaveMaxCyclomaticComplexity.d.ts +3 -0
- package/dist/src/core/files/fileCheckHaveMaxExportedFunctions.d.ts +3 -0
- package/dist/src/core/files/fileCheckHaveMinMaintainabilityIndex.d.ts +3 -0
- package/dist/src/core/files/fileCheckMatchNamePattern.d.ts +3 -0
- package/dist/src/core/files/getFileDependencies.d.ts +2 -0
- package/dist/src/core/files/locateFiles.d.ts +3 -0
- package/dist/src/core/files/types.d.ts +10 -0
- package/dist/src/core/functions/functionCheckHaveExplicitReturnType.d.ts +3 -0
- package/dist/src/core/functions/functionCheckHaveMaxCyclomaticComplexity.d.ts +3 -0
- package/dist/src/core/functions/functionCheckHaveMinMaintainabilityIndex.d.ts +3 -0
- package/dist/src/core/functions/functionCheckHaveModifier.d.ts +3 -0
- package/dist/src/core/functions/functionCheckHaveNameMatchingFileName.d.ts +3 -0
- package/dist/src/core/functions/functionCheckMatchNamePattern.d.ts +3 -0
- package/dist/src/core/functions/locateFunctions.d.ts +3 -0
- package/dist/src/core/functions/types.d.ts +11 -0
- package/dist/src/core/layers/checkLayeredArchitecture.d.ts +3 -0
- package/dist/src/core/layers/createLayeredArchitecture.d.ts +3 -0
- package/dist/src/core/layers/getLayerDependencies.d.ts +2 -0
- package/dist/src/core/layers/layer.d.ts +2 -0
- package/dist/src/core/layers/layerShouldNotBeAccessedByAnyLayer.d.ts +2 -0
- package/dist/src/core/layers/layerShouldOnlyBeAccessedBy.d.ts +2 -0
- package/dist/src/core/layers/types.d.ts +8 -0
- package/dist/src/core/metrics/calculateCyclomaticComplexity.d.ts +2 -0
- package/dist/src/core/metrics/calculateMaintainabilityIndex.d.ts +2 -0
- package/dist/src/core/project/parseProject.d.ts +22 -0
- package/dist/src/core/properties/locateProperties.d.ts +3 -0
- package/dist/src/core/properties/propertyCheckBeReadonly.d.ts +3 -0
- package/dist/src/core/properties/types.d.ts +10 -0
- package/dist/src/core/shared/sharedCheckHaveMaxCyclomaticComplexity.d.ts +1 -0
- package/dist/src/core/shared/sharedCheckHaveMinMaintainabilityIndex.d.ts +1 -0
- package/dist/src/core/shared/sharedCheckHaveModifier.d.ts +2 -0
- package/dist/src/core/shared/sharedCheckHaveNameMatchingFileName.d.ts +2 -0
- package/dist/src/core/shared/sharedCheckMatchNamePattern.d.ts +1 -0
- package/dist/src/core/slices/locateSlices.d.ts +3 -0
- package/dist/src/core/slices/sliceCheckBeFreeOfCycles.d.ts +3 -0
- package/dist/src/core/slices/sliceCheckHaveMaxDistanceFromMainSequence.d.ts +3 -0
- package/dist/src/core/slices/types.d.ts +8 -0
- package/dist/src/core/types.d.ts +7 -0
- package/dist/src/core/utils/ruleBuilder.d.ts +6 -0
- package/dist/src/index.d.ts +34 -8
- package/dist/src/matchers/index.d.ts +5 -0
- package/package.json +1 -1
- package/src/core/classes/classCheckExtendClass.test.ts +30 -0
- package/src/core/classes/classCheckExtendClass.ts +41 -0
- package/src/core/classes/classCheckHaveMaxCyclomaticComplexity.test.ts +8 -0
- package/src/core/classes/classCheckHaveMaxCyclomaticComplexity.ts +35 -0
- package/src/core/classes/classCheckHaveModifier.test.ts +30 -0
- package/src/core/classes/classCheckHaveModifier.ts +17 -0
- package/src/core/classes/classCheckHaveNameMatchingFileName.test.ts +30 -0
- package/src/core/classes/classCheckHaveNameMatchingFileName.ts +15 -0
- package/src/core/classes/classCheckImplementInterface.test.ts +30 -0
- package/src/core/classes/classCheckImplementInterface.ts +43 -0
- package/src/core/classes/classCheckMatchNamePattern.test.ts +8 -0
- package/src/core/classes/classCheckMatchNamePattern.ts +17 -0
- package/src/core/classes/classCheckResideInFolder.test.ts +8 -0
- package/src/core/classes/classCheckResideInFolder.ts +31 -0
- package/src/core/classes/locateClasses.test.ts +13 -0
- package/src/core/classes/locateClasses.ts +127 -0
- package/src/core/classes/types.ts +16 -0
- package/src/core/files/checkDependOnFilesInFolder.test.ts +7 -0
- package/src/core/files/checkDependOnFilesInFolder.ts +36 -0
- package/src/core/files/fileCheckBeFreeOfCycles.test.ts +7 -0
- package/src/core/files/fileCheckBeFreeOfCycles.ts +72 -0
- package/src/core/files/fileCheckHaveMaxCyclomaticComplexity.test.ts +8 -0
- package/src/core/files/fileCheckHaveMaxCyclomaticComplexity.ts +35 -0
- package/src/core/files/fileCheckHaveMaxExportedFunctions.test.ts +32 -0
- package/src/core/files/fileCheckHaveMaxExportedFunctions.ts +45 -0
- package/src/core/files/fileCheckHaveMinMaintainabilityIndex.test.ts +8 -0
- package/src/core/files/fileCheckHaveMinMaintainabilityIndex.ts +19 -0
- package/src/core/files/fileCheckMatchNamePattern.test.ts +30 -0
- package/src/core/files/fileCheckMatchNamePattern.ts +17 -0
- package/src/core/files/getFileDependencies.ts +43 -0
- package/src/core/files/locateFiles.test.ts +30 -0
- package/src/core/files/locateFiles.ts +31 -0
- package/src/core/files/types.ts +12 -0
- package/src/core/functions/functionCheckHaveExplicitReturnType.test.ts +30 -0
- package/src/core/functions/functionCheckHaveExplicitReturnType.ts +29 -0
- package/src/core/functions/functionCheckHaveMaxCyclomaticComplexity.test.ts +8 -0
- package/src/core/functions/functionCheckHaveMaxCyclomaticComplexity.ts +20 -0
- package/src/core/functions/functionCheckHaveMinMaintainabilityIndex.test.ts +8 -0
- package/src/core/functions/functionCheckHaveMinMaintainabilityIndex.ts +20 -0
- package/src/core/functions/functionCheckHaveModifier.test.ts +30 -0
- package/src/core/functions/functionCheckHaveModifier.ts +18 -0
- package/src/core/functions/functionCheckHaveNameMatchingFileName.test.ts +30 -0
- package/src/core/functions/functionCheckHaveNameMatchingFileName.ts +16 -0
- package/src/core/functions/functionCheckMatchNamePattern.test.ts +30 -0
- package/src/core/functions/functionCheckMatchNamePattern.ts +18 -0
- package/src/core/functions/locateFunctions.test.ts +16 -0
- package/src/core/functions/locateFunctions.ts +47 -0
- package/src/core/functions/types.ts +13 -0
- package/src/core/layers/checkLayeredArchitecture.test.ts +23 -0
- package/src/core/layers/checkLayeredArchitecture.ts +15 -0
- package/src/core/layers/createLayeredArchitecture.test.ts +13 -0
- package/src/core/layers/createLayeredArchitecture.ts +15 -0
- package/src/core/layers/getLayerDependencies.ts +45 -0
- package/src/core/layers/layer.test.ts +12 -0
- package/src/core/layers/layer.ts +10 -0
- package/src/core/layers/layerShouldNotBeAccessedByAnyLayer.test.ts +14 -0
- package/src/core/layers/layerShouldNotBeAccessedByAnyLayer.ts +40 -0
- package/src/core/layers/layerShouldOnlyBeAccessedBy.test.ts +15 -0
- package/src/core/layers/layerShouldOnlyBeAccessedBy.ts +48 -0
- package/src/core/layers/types.ts +9 -0
- package/src/core/metrics/calculateCyclomaticComplexity.test.ts +23 -0
- package/src/core/metrics/calculateCyclomaticComplexity.ts +36 -0
- package/src/core/metrics/calculateMaintainabilityIndex.test.ts +38 -0
- package/src/core/metrics/calculateMaintainabilityIndex.ts +47 -0
- package/src/core/project/parseProject.test.ts +19 -0
- package/src/core/project/parseProject.ts +216 -0
- package/src/core/properties/locateProperties.test.ts +19 -0
- package/src/core/properties/locateProperties.ts +41 -0
- package/src/core/properties/propertyCheckBeReadonly.test.ts +32 -0
- package/src/core/properties/propertyCheckBeReadonly.ts +27 -0
- package/src/core/properties/types.ts +12 -0
- package/src/core/shared/sharedCheckHaveMaxCyclomaticComplexity.test.ts +30 -0
- package/src/core/shared/sharedCheckHaveMaxCyclomaticComplexity.ts +23 -0
- package/src/core/shared/sharedCheckHaveMinMaintainabilityIndex.test.ts +30 -0
- package/src/core/shared/sharedCheckHaveMinMaintainabilityIndex.ts +23 -0
- package/src/core/shared/sharedCheckHaveModifier.test.ts +31 -0
- package/src/core/shared/sharedCheckHaveModifier.ts +51 -0
- package/src/core/shared/sharedCheckHaveNameMatchingFileName.test.ts +17 -0
- package/src/core/shared/sharedCheckHaveNameMatchingFileName.ts +36 -0
- package/src/core/shared/sharedCheckMatchNamePattern.test.ts +29 -0
- package/src/core/shared/sharedCheckMatchNamePattern.ts +22 -0
- package/src/core/slices/locateSlices.test.ts +22 -0
- package/src/core/slices/locateSlices.ts +35 -0
- package/src/core/slices/sliceCheckBeFreeOfCycles.test.ts +26 -0
- package/src/core/slices/sliceCheckBeFreeOfCycles.ts +91 -0
- package/src/core/slices/sliceCheckHaveMaxDistanceFromMainSequence.test.ts +27 -0
- package/src/core/slices/sliceCheckHaveMaxDistanceFromMainSequence.ts +110 -0
- package/src/core/slices/types.ts +9 -0
- package/src/core/testUtils.ts +61 -0
- package/src/core/types.ts +15 -0
- package/src/core/utils/ruleBuilder.test.ts +24 -0
- package/src/core/utils/ruleBuilder.ts +28 -0
- package/src/index.ts +34 -8
- package/src/matchers/index.ts +269 -51
- package/test/architecture.test.ts +109 -16
- package/tsconfig.json +2 -1
- package/vite.config.ts +2 -2
- package/dist/src/core/ClassLocator.d.ts +0 -20
- package/dist/src/core/FileLocator.d.ts +0 -15
- package/dist/src/core/FunctionLocator.d.ts +0 -15
- package/dist/src/core/LayerLocator.d.ts +0 -23
- package/dist/src/core/Project.d.ts +0 -20
- package/dist/src/core/PropertyLocator.d.ts +0 -12
- package/dist/src/core/SliceLocator.d.ts +0 -10
- package/src/core/ClassLocator.ts +0 -226
- package/src/core/FileLocator.ts +0 -168
- package/src/core/FunctionLocator.ts +0 -105
- package/src/core/LayerLocator.ts +0 -119
- package/src/core/Project.ts +0 -171
- package/src/core/PropertyLocator.ts +0 -50
- package/src/core/SliceLocator.ts +0 -92
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import * as ts from 'typescript';
|
|
3
|
+
import { locateClasses } from '../classes/locateClasses';
|
|
4
|
+
import type { ClassQueryOptions } from '../classes/types';
|
|
5
|
+
import { locateFiles } from '../files/locateFiles';
|
|
6
|
+
import type { FileQueryOptions } from '../files/types';
|
|
7
|
+
import { locateFunctions } from '../functions/locateFunctions';
|
|
8
|
+
import type { FunctionQueryOptions } from '../functions/types';
|
|
9
|
+
import { checkLayeredArchitecture } from '../layers/checkLayeredArchitecture';
|
|
10
|
+
import { createLayeredArchitecture } from '../layers/createLayeredArchitecture';
|
|
11
|
+
import { layer } from '../layers/layer';
|
|
12
|
+
import { layerShouldNotBeAccessedByAnyLayer } from '../layers/layerShouldNotBeAccessedByAnyLayer';
|
|
13
|
+
import { layerShouldOnlyBeAccessedBy } from '../layers/layerShouldOnlyBeAccessedBy';
|
|
14
|
+
import { locateProperties } from '../properties/locateProperties';
|
|
15
|
+
import type { PropertyQueryOptions } from '../properties/types';
|
|
16
|
+
import { locateSlices } from '../slices/locateSlices';
|
|
17
|
+
|
|
18
|
+
export function parseProject(options: { tsConfigFilePath?: string } = {}) {
|
|
19
|
+
let compilerOptions: ts.CompilerOptions = {};
|
|
20
|
+
const configPath =
|
|
21
|
+
options.tsConfigFilePath ||
|
|
22
|
+
ts.findConfigFile(process.cwd(), ts.sys.fileExists, 'tsconfig.json');
|
|
23
|
+
|
|
24
|
+
let program: ts.Program;
|
|
25
|
+
|
|
26
|
+
if (configPath) {
|
|
27
|
+
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
28
|
+
const parsedCommandLine = ts.parseJsonConfigFileContent(
|
|
29
|
+
configFile.config,
|
|
30
|
+
ts.sys,
|
|
31
|
+
dirname(configPath),
|
|
32
|
+
undefined,
|
|
33
|
+
configPath,
|
|
34
|
+
undefined,
|
|
35
|
+
[
|
|
36
|
+
{
|
|
37
|
+
extension: '.vue',
|
|
38
|
+
isMixedContent: true,
|
|
39
|
+
scriptKind: ts.ScriptKind.TS,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
extension: '.svelte',
|
|
43
|
+
isMixedContent: true,
|
|
44
|
+
scriptKind: ts.ScriptKind.TS,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
);
|
|
48
|
+
compilerOptions = parsedCommandLine.options;
|
|
49
|
+
|
|
50
|
+
const host = ts.createCompilerHost(compilerOptions);
|
|
51
|
+
const originalGetSourceFile = host.getSourceFile;
|
|
52
|
+
|
|
53
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic require
|
|
54
|
+
let vueCompiler: any;
|
|
55
|
+
try {
|
|
56
|
+
vueCompiler = require('@vue/compiler-sfc');
|
|
57
|
+
} catch (_e) {
|
|
58
|
+
// Not a Vue project
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
host.getSourceFile = (
|
|
62
|
+
fileName,
|
|
63
|
+
languageVersion,
|
|
64
|
+
onError,
|
|
65
|
+
shouldCreateNewSourceFile,
|
|
66
|
+
) => {
|
|
67
|
+
if (fileName.endsWith('.vue')) {
|
|
68
|
+
const content = ts.sys.readFile(fileName);
|
|
69
|
+
if (content) {
|
|
70
|
+
let scriptContent = '';
|
|
71
|
+
if (vueCompiler) {
|
|
72
|
+
const parsed = vueCompiler.parse(content);
|
|
73
|
+
const script = parsed.descriptor.script;
|
|
74
|
+
const scriptSetup = parsed.descriptor.scriptSetup;
|
|
75
|
+
if (script) scriptContent += `${script.content}\n`;
|
|
76
|
+
if (scriptSetup) scriptContent += `${scriptSetup.content}\n`;
|
|
77
|
+
} else {
|
|
78
|
+
const match = content.match(/<script[^>]*>(.*?)<\/script>/s);
|
|
79
|
+
if (match) scriptContent = match[1];
|
|
80
|
+
}
|
|
81
|
+
return ts.createSourceFile(
|
|
82
|
+
fileName,
|
|
83
|
+
scriptContent,
|
|
84
|
+
languageVersion,
|
|
85
|
+
true,
|
|
86
|
+
ts.ScriptKind.TS,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (fileName.endsWith('.svelte')) {
|
|
92
|
+
const content = ts.sys.readFile(fileName);
|
|
93
|
+
if (content) {
|
|
94
|
+
let scriptContent = '';
|
|
95
|
+
const match = content.match(/<script[^>]*>(.*?)<\/script>/s);
|
|
96
|
+
if (match) scriptContent = match[1];
|
|
97
|
+
return ts.createSourceFile(
|
|
98
|
+
fileName,
|
|
99
|
+
scriptContent,
|
|
100
|
+
languageVersion,
|
|
101
|
+
true,
|
|
102
|
+
ts.ScriptKind.TS,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return originalGetSourceFile(
|
|
108
|
+
fileName,
|
|
109
|
+
languageVersion,
|
|
110
|
+
onError,
|
|
111
|
+
shouldCreateNewSourceFile,
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
program = ts.createProgram({
|
|
116
|
+
rootNames: parsedCommandLine.fileNames,
|
|
117
|
+
options: compilerOptions,
|
|
118
|
+
host: host,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Force the TypeScript compiler to bind the AST and populate .parent pointers
|
|
122
|
+
program.getTypeChecker();
|
|
123
|
+
} else {
|
|
124
|
+
throw new Error('Could not find tsconfig.json');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const getSourceFiles = () =>
|
|
128
|
+
program
|
|
129
|
+
.getSourceFiles()
|
|
130
|
+
.filter(
|
|
131
|
+
(sf) => !sf.isDeclarationFile && !sf.fileName.includes('node_modules'),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
getFiles: (queryOptions?: FileQueryOptions) => {
|
|
136
|
+
return locateFiles(getSourceFiles(), program, queryOptions);
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
getClasses: (queryOptions?: ClassQueryOptions) => {
|
|
140
|
+
const allClasses: ts.ClassDeclaration[] = [];
|
|
141
|
+
for (const sf of getSourceFiles()) {
|
|
142
|
+
const visit = (node: ts.Node) => {
|
|
143
|
+
if (ts.isClassDeclaration(node)) {
|
|
144
|
+
allClasses.push(node);
|
|
145
|
+
}
|
|
146
|
+
ts.forEachChild(node, visit);
|
|
147
|
+
};
|
|
148
|
+
visit(sf);
|
|
149
|
+
}
|
|
150
|
+
return locateClasses(allClasses, program, queryOptions);
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
layeredArchitecture: () => {
|
|
154
|
+
let data = createLayeredArchitecture(getSourceFiles(), program);
|
|
155
|
+
const api = {
|
|
156
|
+
layer: (name: string, folderPattern: string) => {
|
|
157
|
+
data = layer(data, name, folderPattern);
|
|
158
|
+
return api;
|
|
159
|
+
},
|
|
160
|
+
whereLayer: (name: string) => {
|
|
161
|
+
return {
|
|
162
|
+
shouldNotBeAccessedByAnyLayer: () => {
|
|
163
|
+
data = layerShouldNotBeAccessedByAnyLayer(data, name);
|
|
164
|
+
return api;
|
|
165
|
+
},
|
|
166
|
+
shouldOnlyBeAccessedBy: (...allowedLayers: string[]) => {
|
|
167
|
+
data = layerShouldOnlyBeAccessedBy(data, name, allowedLayers);
|
|
168
|
+
return api;
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
check: () => checkLayeredArchitecture(data),
|
|
173
|
+
get data() {
|
|
174
|
+
return data;
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
return api;
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
getFunctions: (queryOptions?: FunctionQueryOptions) => {
|
|
181
|
+
const allFunctions: ts.FunctionLikeDeclaration[] = [];
|
|
182
|
+
for (const sf of getSourceFiles()) {
|
|
183
|
+
const visit = (node: ts.Node) => {
|
|
184
|
+
if (
|
|
185
|
+
ts.isFunctionDeclaration(node) ||
|
|
186
|
+
ts.isMethodDeclaration(node) ||
|
|
187
|
+
ts.isArrowFunction(node)
|
|
188
|
+
) {
|
|
189
|
+
allFunctions.push(node);
|
|
190
|
+
}
|
|
191
|
+
ts.forEachChild(node, visit);
|
|
192
|
+
};
|
|
193
|
+
visit(sf);
|
|
194
|
+
}
|
|
195
|
+
return locateFunctions(allFunctions, program, queryOptions);
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
getProperties: (queryOptions?: PropertyQueryOptions) => {
|
|
199
|
+
const allProperties: ts.PropertyDeclaration[] = [];
|
|
200
|
+
for (const sf of getSourceFiles()) {
|
|
201
|
+
const visit = (node: ts.Node) => {
|
|
202
|
+
if (ts.isPropertyDeclaration(node)) {
|
|
203
|
+
allProperties.push(node);
|
|
204
|
+
}
|
|
205
|
+
ts.forEachChild(node, visit);
|
|
206
|
+
};
|
|
207
|
+
visit(sf);
|
|
208
|
+
}
|
|
209
|
+
return locateProperties(allProperties, program, queryOptions);
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
getSlices: (pattern: string) => {
|
|
213
|
+
return locateSlices(getSourceFiles(), program, pattern);
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { createSourceFile } from '../testUtils';
|
|
4
|
+
import { locateProperties } from './locateProperties';
|
|
5
|
+
|
|
6
|
+
describe('locateProperties', () => {
|
|
7
|
+
it('should return all properties if no query options', () => {
|
|
8
|
+
const file1 = createSourceFile('class A { prop: string; }');
|
|
9
|
+
const properties: ts.PropertyDeclaration[] = [];
|
|
10
|
+
const visit = (node: ts.Node) => {
|
|
11
|
+
if (ts.isPropertyDeclaration(node)) properties.push(node);
|
|
12
|
+
ts.forEachChild(node, visit);
|
|
13
|
+
};
|
|
14
|
+
visit(file1);
|
|
15
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
16
|
+
const result = locateProperties(properties, {} as any);
|
|
17
|
+
expect(result.properties.length).toBe(1);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import type { PropertyLocatorData, PropertyQueryOptions } from './types';
|
|
3
|
+
|
|
4
|
+
export function locateProperties(
|
|
5
|
+
properties: ts.PropertyDeclaration[],
|
|
6
|
+
program: ts.Program,
|
|
7
|
+
options?: PropertyQueryOptions,
|
|
8
|
+
): PropertyLocatorData {
|
|
9
|
+
let filtered = properties;
|
|
10
|
+
|
|
11
|
+
if (options?.inFolder) {
|
|
12
|
+
filtered = filtered.filter((p) => {
|
|
13
|
+
const sourceFile = p.getSourceFile
|
|
14
|
+
? p.getSourceFile()
|
|
15
|
+
: p.parent
|
|
16
|
+
? ts.getSourceFileOfNode(p)
|
|
17
|
+
: undefined;
|
|
18
|
+
if (!sourceFile) return false;
|
|
19
|
+
return (
|
|
20
|
+
sourceFile.fileName.includes(`/${options.inFolder}/`) ||
|
|
21
|
+
sourceFile.fileName.includes(`\\${options.inFolder}\\`)
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (options?.matchNamePattern) {
|
|
27
|
+
const regex =
|
|
28
|
+
typeof options.matchNamePattern === 'string'
|
|
29
|
+
? new RegExp(options.matchNamePattern)
|
|
30
|
+
: options.matchNamePattern;
|
|
31
|
+
filtered = filtered.filter((p) => {
|
|
32
|
+
return ts.isIdentifier(p.name) && regex.test(p.name.text);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
type: 'PropertyLocator',
|
|
38
|
+
properties: filtered,
|
|
39
|
+
program,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createSourceFile, getProperties } from '../testUtils';
|
|
3
|
+
import { propertyCheckBeReadonly } from './propertyCheckBeReadonly';
|
|
4
|
+
import type { PropertyLocatorData } from './types';
|
|
5
|
+
|
|
6
|
+
describe('propertyCheckBeReadonly', () => {
|
|
7
|
+
it('should pass if properties are readonly', () => {
|
|
8
|
+
const sourceFile = createSourceFile(
|
|
9
|
+
`class MyClass { readonly prop: string; }`,
|
|
10
|
+
);
|
|
11
|
+
const data: PropertyLocatorData = {
|
|
12
|
+
type: 'PropertyLocator',
|
|
13
|
+
properties: getProperties(sourceFile),
|
|
14
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
15
|
+
program: {} as any,
|
|
16
|
+
};
|
|
17
|
+
const result = propertyCheckBeReadonly(data, false);
|
|
18
|
+
expect(result.pass).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should fail if properties are not readonly', () => {
|
|
22
|
+
const sourceFile = createSourceFile(`class MyClass { prop: string; }`);
|
|
23
|
+
const data: PropertyLocatorData = {
|
|
24
|
+
type: 'PropertyLocator',
|
|
25
|
+
properties: getProperties(sourceFile),
|
|
26
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
27
|
+
program: {} as any,
|
|
28
|
+
};
|
|
29
|
+
const result = propertyCheckBeReadonly(data, false);
|
|
30
|
+
expect(result.pass).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import type { RuleResult } from '../types';
|
|
3
|
+
import type { PropertyLocatorData } from './types';
|
|
4
|
+
|
|
5
|
+
export function propertyCheckBeReadonly(
|
|
6
|
+
locator: PropertyLocatorData,
|
|
7
|
+
isNot: boolean,
|
|
8
|
+
): RuleResult {
|
|
9
|
+
const violations: string[] = [];
|
|
10
|
+
for (const p of locator.properties) {
|
|
11
|
+
const name = ts.isIdentifier(p.name) ? p.name.text : 'Anonymous Property';
|
|
12
|
+
const modifiers = ts.canHaveModifiers(p) ? ts.getModifiers(p) : undefined;
|
|
13
|
+
const matches = modifiers?.some(
|
|
14
|
+
(m: ts.Modifier) => m.kind === ts.SyntaxKind.ReadonlyKeyword,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
if (isNot && matches) {
|
|
18
|
+
violations.push(`Property ${name} is readonly, but it shouldn't be.`);
|
|
19
|
+
} else if (!isNot && !matches) {
|
|
20
|
+
violations.push(`Property ${name} is not readonly, but it should be.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
pass: violations.length === 0,
|
|
25
|
+
message: () => violations.join('\n'),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type * as ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
export interface PropertyLocatorData {
|
|
4
|
+
type: 'PropertyLocator';
|
|
5
|
+
properties: ts.PropertyDeclaration[];
|
|
6
|
+
program: ts.Program;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface PropertyQueryOptions {
|
|
10
|
+
inFolder?: string;
|
|
11
|
+
matchNamePattern?: string | RegExp;
|
|
12
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { sharedCheckHaveMaxCyclomaticComplexity } from './sharedCheckHaveMaxCyclomaticComplexity';
|
|
3
|
+
|
|
4
|
+
describe('sharedCheckHaveMaxCyclomaticComplexity', () => {
|
|
5
|
+
it('should pass if complexity is within limits', () => {
|
|
6
|
+
const items = [{ name: 'A', complexity: 5 }];
|
|
7
|
+
const result = sharedCheckHaveMaxCyclomaticComplexity(
|
|
8
|
+
items,
|
|
9
|
+
(i) => i.name,
|
|
10
|
+
(i) => i.complexity,
|
|
11
|
+
'Item',
|
|
12
|
+
10,
|
|
13
|
+
false,
|
|
14
|
+
);
|
|
15
|
+
expect(result.pass).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should fail if complexity exceeds limits', () => {
|
|
19
|
+
const items = [{ name: 'A', complexity: 15 }];
|
|
20
|
+
const result = sharedCheckHaveMaxCyclomaticComplexity(
|
|
21
|
+
items,
|
|
22
|
+
(i) => i.name,
|
|
23
|
+
(i) => i.complexity,
|
|
24
|
+
'Item',
|
|
25
|
+
10,
|
|
26
|
+
false,
|
|
27
|
+
);
|
|
28
|
+
expect(result.pass).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ruleBuilder } from '../utils/ruleBuilder';
|
|
2
|
+
|
|
3
|
+
export function sharedCheckHaveMaxCyclomaticComplexity<T>(
|
|
4
|
+
items: T[],
|
|
5
|
+
getName: (item: T) => string | undefined,
|
|
6
|
+
getComplexity: (item: T) => number,
|
|
7
|
+
label: string,
|
|
8
|
+
max: number,
|
|
9
|
+
isNot: boolean,
|
|
10
|
+
) {
|
|
11
|
+
return ruleBuilder(items, isNot, (item) => {
|
|
12
|
+
const name = getName(item);
|
|
13
|
+
const complexity = getComplexity(item);
|
|
14
|
+
const exceeds = complexity > max;
|
|
15
|
+
const desc = `${label} ${name || 'Anonymous'}`;
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
passes: !exceeds,
|
|
19
|
+
failMessage: `${desc} has a total cyclomatic complexity of ${complexity}, which exceeds the maximum of ${max}.`,
|
|
20
|
+
failNotMessage: `${desc} has a total cyclomatic complexity of ${complexity}, which exceeds the maximum of ${max}, but it shouldn't.`,
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { sharedCheckHaveMinMaintainabilityIndex } from './sharedCheckHaveMinMaintainabilityIndex';
|
|
3
|
+
|
|
4
|
+
describe('sharedCheckHaveMinMaintainabilityIndex', () => {
|
|
5
|
+
it('should pass if MI is above limit', () => {
|
|
6
|
+
const items = [{ name: 'A', mi: 50 }];
|
|
7
|
+
const result = sharedCheckHaveMinMaintainabilityIndex(
|
|
8
|
+
items,
|
|
9
|
+
(i) => i.name,
|
|
10
|
+
(i) => i.mi,
|
|
11
|
+
'Item',
|
|
12
|
+
20,
|
|
13
|
+
false,
|
|
14
|
+
);
|
|
15
|
+
expect(result.pass).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should fail if MI is below limit', () => {
|
|
19
|
+
const items = [{ name: 'A', mi: 10 }];
|
|
20
|
+
const result = sharedCheckHaveMinMaintainabilityIndex(
|
|
21
|
+
items,
|
|
22
|
+
(i) => i.name,
|
|
23
|
+
(i) => i.mi,
|
|
24
|
+
'Item',
|
|
25
|
+
20,
|
|
26
|
+
false,
|
|
27
|
+
);
|
|
28
|
+
expect(result.pass).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ruleBuilder } from '../utils/ruleBuilder';
|
|
2
|
+
|
|
3
|
+
export function sharedCheckHaveMinMaintainabilityIndex<T>(
|
|
4
|
+
items: T[],
|
|
5
|
+
getName: (item: T) => string | undefined,
|
|
6
|
+
getMi: (item: T) => number,
|
|
7
|
+
label: string,
|
|
8
|
+
min: number,
|
|
9
|
+
isNot: boolean,
|
|
10
|
+
) {
|
|
11
|
+
return ruleBuilder(items, isNot, (item) => {
|
|
12
|
+
const name = getName(item);
|
|
13
|
+
const mi = getMi(item);
|
|
14
|
+
const fallsBelow = mi < min;
|
|
15
|
+
const desc = `${label} ${name || 'Anonymous'}`;
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
passes: !fallsBelow,
|
|
19
|
+
failMessage: `${desc} has a maintainability index of ${mi.toFixed(2)}, which falls below the minimum of ${min}.`,
|
|
20
|
+
failNotMessage: `${desc} has a maintainability index of ${mi.toFixed(2)}, which falls below the minimum of ${min}, but it shouldn't.`,
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createSourceFile, getClasses } from '../testUtils';
|
|
3
|
+
import { sharedCheckHaveModifier } from './sharedCheckHaveModifier';
|
|
4
|
+
|
|
5
|
+
describe('sharedCheckHaveModifier', () => {
|
|
6
|
+
it('should pass if node has the modifier', () => {
|
|
7
|
+
const sourceFile = createSourceFile('export class MyClass {}');
|
|
8
|
+
const classes = getClasses(sourceFile);
|
|
9
|
+
const result = sharedCheckHaveModifier(
|
|
10
|
+
classes,
|
|
11
|
+
(c) => c.name?.text,
|
|
12
|
+
'Class',
|
|
13
|
+
'export',
|
|
14
|
+
false,
|
|
15
|
+
);
|
|
16
|
+
expect(result.pass).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should fail if node does not have the modifier', () => {
|
|
20
|
+
const sourceFile = createSourceFile('class MyClass {}');
|
|
21
|
+
const classes = getClasses(sourceFile);
|
|
22
|
+
const result = sharedCheckHaveModifier(
|
|
23
|
+
classes,
|
|
24
|
+
(c) => c.name?.text,
|
|
25
|
+
'Class',
|
|
26
|
+
'export',
|
|
27
|
+
false,
|
|
28
|
+
);
|
|
29
|
+
expect(result.pass).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { ruleBuilder } from '../utils/ruleBuilder';
|
|
3
|
+
|
|
4
|
+
export function sharedCheckHaveModifier<T extends ts.Node>(
|
|
5
|
+
items: T[],
|
|
6
|
+
getName: (item: T) => string | undefined,
|
|
7
|
+
label: string,
|
|
8
|
+
modifierStr: string,
|
|
9
|
+
isNot: boolean,
|
|
10
|
+
) {
|
|
11
|
+
let targetKind: ts.SyntaxKind;
|
|
12
|
+
switch (modifierStr) {
|
|
13
|
+
case 'export':
|
|
14
|
+
targetKind = ts.SyntaxKind.ExportKeyword;
|
|
15
|
+
break;
|
|
16
|
+
case 'default':
|
|
17
|
+
targetKind = ts.SyntaxKind.DefaultKeyword;
|
|
18
|
+
break;
|
|
19
|
+
case 'abstract':
|
|
20
|
+
targetKind = ts.SyntaxKind.AbstractKeyword;
|
|
21
|
+
break;
|
|
22
|
+
case 'async':
|
|
23
|
+
targetKind = ts.SyntaxKind.AsyncKeyword;
|
|
24
|
+
break;
|
|
25
|
+
case 'public':
|
|
26
|
+
targetKind = ts.SyntaxKind.PublicKeyword;
|
|
27
|
+
break;
|
|
28
|
+
case 'private':
|
|
29
|
+
targetKind = ts.SyntaxKind.PrivateKeyword;
|
|
30
|
+
break;
|
|
31
|
+
default:
|
|
32
|
+
throw new Error(`Modifier ${modifierStr} is not fully supported.`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return ruleBuilder(items, isNot, (item) => {
|
|
36
|
+
const name = getName(item);
|
|
37
|
+
const modifiers = ts.canHaveModifiers(item)
|
|
38
|
+
? ts.getModifiers(item)
|
|
39
|
+
: undefined;
|
|
40
|
+
const passes = modifiers
|
|
41
|
+
? modifiers.some((m: ts.Modifier) => m.kind === targetKind)
|
|
42
|
+
: false;
|
|
43
|
+
const desc = `${label} ${name || 'Anonymous'}`;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
passes,
|
|
47
|
+
failMessage: `${desc} does not have modifier ${modifierStr}, but it should.`,
|
|
48
|
+
failNotMessage: `${desc} has modifier ${modifierStr}, but it shouldn't.`,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { sharedCheckHaveNameMatchingFileName } from './sharedCheckHaveNameMatchingFileName';
|
|
3
|
+
|
|
4
|
+
describe('sharedCheckHaveNameMatchingFileName', () => {
|
|
5
|
+
it('should skip nodes without source files', () => {
|
|
6
|
+
// getFunctions actually provides a getSourceFile natively
|
|
7
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
8
|
+
const items = [{ parent: undefined } as any];
|
|
9
|
+
const result = sharedCheckHaveNameMatchingFileName(
|
|
10
|
+
items,
|
|
11
|
+
() => 'name',
|
|
12
|
+
'Item',
|
|
13
|
+
false,
|
|
14
|
+
);
|
|
15
|
+
expect(result.pass).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import * as ts from 'typescript';
|
|
3
|
+
import { ruleBuilder } from '../utils/ruleBuilder';
|
|
4
|
+
|
|
5
|
+
export function sharedCheckHaveNameMatchingFileName<T extends ts.Node>(
|
|
6
|
+
items: T[],
|
|
7
|
+
getName: (item: T) => string | undefined,
|
|
8
|
+
label: string,
|
|
9
|
+
isNot: boolean,
|
|
10
|
+
) {
|
|
11
|
+
return ruleBuilder(items, isNot, (item) => {
|
|
12
|
+
const name = getName(item);
|
|
13
|
+
const sourceFile = item.getSourceFile
|
|
14
|
+
? item.getSourceFile()
|
|
15
|
+
: item.parent
|
|
16
|
+
? ts.getSourceFileOfNode(item)
|
|
17
|
+
: undefined;
|
|
18
|
+
|
|
19
|
+
if (!sourceFile) {
|
|
20
|
+
return { passes: true, failMessage: '', failNotMessage: '' };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const basename = path.basename(
|
|
24
|
+
sourceFile.fileName,
|
|
25
|
+
path.extname(sourceFile.fileName),
|
|
26
|
+
);
|
|
27
|
+
const passes = name === basename;
|
|
28
|
+
const desc = `${label} ${name || 'Anonymous'}`;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
passes,
|
|
32
|
+
failMessage: `${desc} does not have a name matching its filename ${basename}, but it should.`,
|
|
33
|
+
failNotMessage: `${desc} has a name matching its filename ${basename}, but it shouldn't.`,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { sharedCheckMatchNamePattern } from './sharedCheckMatchNamePattern';
|
|
3
|
+
|
|
4
|
+
describe('sharedCheckMatchNamePattern', () => {
|
|
5
|
+
it('should pass if all items match the pattern', () => {
|
|
6
|
+
const items = [{ name: 'UserService' }, { name: 'AuthService' }];
|
|
7
|
+
const result = sharedCheckMatchNamePattern(
|
|
8
|
+
items,
|
|
9
|
+
(i) => i.name,
|
|
10
|
+
'Item',
|
|
11
|
+
/Service$/,
|
|
12
|
+
false,
|
|
13
|
+
);
|
|
14
|
+
expect(result.pass).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should fail if any item does not match the pattern', () => {
|
|
18
|
+
const items = [{ name: 'UserService' }, { name: 'AuthRepository' }];
|
|
19
|
+
const result = sharedCheckMatchNamePattern(
|
|
20
|
+
items,
|
|
21
|
+
(i) => i.name,
|
|
22
|
+
'Item',
|
|
23
|
+
/Service$/,
|
|
24
|
+
false,
|
|
25
|
+
);
|
|
26
|
+
expect(result.pass).toBe(false);
|
|
27
|
+
expect(result.message()).toContain('AuthRepository does not match pattern');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ruleBuilder } from '../utils/ruleBuilder';
|
|
2
|
+
|
|
3
|
+
export function sharedCheckMatchNamePattern<T>(
|
|
4
|
+
items: T[],
|
|
5
|
+
getName: (item: T) => string | undefined,
|
|
6
|
+
label: string,
|
|
7
|
+
pattern: string | RegExp,
|
|
8
|
+
isNot: boolean,
|
|
9
|
+
) {
|
|
10
|
+
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
|
|
11
|
+
|
|
12
|
+
return ruleBuilder(items, isNot, (item) => {
|
|
13
|
+
const name = getName(item);
|
|
14
|
+
const passes = name ? regex.test(name) : false;
|
|
15
|
+
const desc = `${label} ${name || 'Anonymous'}`;
|
|
16
|
+
return {
|
|
17
|
+
passes,
|
|
18
|
+
failMessage: `${desc} does not match pattern ${pattern}, but it should.`,
|
|
19
|
+
failNotMessage: `${desc} matches pattern ${pattern}, but it shouldn't.`,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createSourceFile } from '../testUtils';
|
|
3
|
+
import { locateSlices } from './locateSlices';
|
|
4
|
+
|
|
5
|
+
describe('locateSlices', () => {
|
|
6
|
+
it('should group files into slices based on pattern', () => {
|
|
7
|
+
const file1 = createSourceFile('', 'src/domain/auth/index.ts');
|
|
8
|
+
const file2 = createSourceFile('', 'src/domain/users/index.ts');
|
|
9
|
+
const file3 = createSourceFile('', 'src/domain/users/user.ts');
|
|
10
|
+
const result = locateSlices(
|
|
11
|
+
[file1, file2, file3],
|
|
12
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
13
|
+
{} as any,
|
|
14
|
+
'src/domain/*',
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
expect(result.sliceIds.size).toBe(2);
|
|
18
|
+
expect(result.sliceIds.has('auth')).toBe(true);
|
|
19
|
+
expect(result.sliceIds.has('users')).toBe(true);
|
|
20
|
+
expect(result.sliceFiles.get('users')?.length).toBe(2);
|
|
21
|
+
});
|
|
22
|
+
});
|