@archest/vitest 1.0.0 → 1.0.2
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.d.ts +3 -0
- package/dist/index.js +1 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +118 -551
- package/dist/index.mjs.map +1 -1
- package/dist/matchers/index.d.ts +21 -0
- package/dist/matchers/models.d.ts +201 -0
- package/node_modules/@archest/core/dist/classes/classCheckExtendClass.d.ts +3 -0
- package/node_modules/@archest/core/dist/classes/classCheckHaveMaxCyclomaticComplexity.d.ts +3 -0
- package/node_modules/@archest/core/dist/classes/classCheckHaveModifier.d.ts +3 -0
- package/node_modules/@archest/core/dist/classes/classCheckHaveNameMatchingFileName.d.ts +3 -0
- package/node_modules/@archest/core/dist/classes/classCheckImplementInterface.d.ts +3 -0
- package/node_modules/@archest/core/dist/classes/classCheckMatchNamePattern.d.ts +3 -0
- package/node_modules/@archest/core/dist/classes/classCheckResideInFolder.d.ts +3 -0
- package/node_modules/@archest/core/dist/classes/locateClasses.d.ts +5 -0
- package/node_modules/@archest/core/dist/classes/types.d.ts +25 -0
- package/node_modules/@archest/core/dist/dto.d.ts +78 -0
- package/node_modules/@archest/core/dist/files/checkDependOnExternalModule.d.ts +3 -0
- package/node_modules/@archest/core/dist/files/checkDependOnFilesInFolder.d.ts +3 -0
- package/node_modules/@archest/core/dist/files/fileCheckBeFreeOfCycles.d.ts +3 -0
- package/node_modules/@archest/core/dist/files/fileCheckHaveMaxCyclomaticComplexity.d.ts +3 -0
- package/node_modules/@archest/core/dist/files/fileCheckHaveMaxExportedFunctions.d.ts +3 -0
- package/node_modules/@archest/core/dist/files/fileCheckHaveMinMaintainabilityIndex.d.ts +3 -0
- package/node_modules/@archest/core/dist/files/fileCheckMatchNamePattern.d.ts +3 -0
- package/node_modules/@archest/core/dist/files/getFileDependencies.d.ts +2 -0
- package/node_modules/@archest/core/dist/files/locateFiles.d.ts +4 -0
- package/node_modules/@archest/core/dist/files/types.d.ts +17 -0
- package/node_modules/@archest/core/dist/functions/functionCheckHaveExplicitReturnType.d.ts +3 -0
- package/node_modules/@archest/core/dist/functions/functionCheckHaveMaxCyclomaticComplexity.d.ts +3 -0
- package/node_modules/@archest/core/dist/functions/functionCheckHaveMinMaintainabilityIndex.d.ts +3 -0
- package/node_modules/@archest/core/dist/functions/functionCheckHaveModifier.d.ts +3 -0
- package/node_modules/@archest/core/dist/functions/functionCheckHaveNameMatchingFileName.d.ts +3 -0
- package/node_modules/@archest/core/dist/functions/functionCheckMatchNamePattern.d.ts +3 -0
- package/node_modules/@archest/core/dist/functions/locateFunctions.d.ts +5 -0
- package/node_modules/@archest/core/dist/functions/types.d.ts +19 -0
- package/node_modules/@archest/core/dist/index.d.ts +43 -0
- package/node_modules/@archest/core/dist/index.js +14 -0
- package/node_modules/@archest/core/dist/index.js.map +1 -0
- package/node_modules/@archest/core/dist/index.mjs +615 -0
- package/node_modules/@archest/core/dist/index.mjs.map +1 -0
- package/node_modules/@archest/core/dist/layers/checkLayeredArchitecture.d.ts +3 -0
- package/node_modules/@archest/core/dist/layers/createLayeredArchitecture.d.ts +3 -0
- package/node_modules/@archest/core/dist/layers/getLayerDependencies.d.ts +2 -0
- package/node_modules/@archest/core/dist/layers/layer.d.ts +2 -0
- package/node_modules/@archest/core/dist/layers/layerShouldNotBeAccessedByAnyLayer.d.ts +2 -0
- package/node_modules/@archest/core/dist/layers/layerShouldOnlyBeAccessedBy.d.ts +2 -0
- package/node_modules/@archest/core/dist/layers/types.d.ts +8 -0
- package/node_modules/@archest/core/dist/metrics/calculateCyclomaticComplexity.d.ts +2 -0
- package/node_modules/@archest/core/dist/metrics/calculateMaintainabilityIndex.d.ts +2 -0
- package/node_modules/@archest/core/dist/project/parseProject.d.ts +55 -0
- package/node_modules/@archest/core/dist/properties/locateProperties.d.ts +5 -0
- package/node_modules/@archest/core/dist/properties/propertyCheckBeReadonly.d.ts +3 -0
- package/node_modules/@archest/core/dist/properties/types.d.ts +17 -0
- package/node_modules/@archest/core/dist/shared/sharedCheckHaveMaxCyclomaticComplexity.d.ts +1 -0
- package/node_modules/@archest/core/dist/shared/sharedCheckHaveMinMaintainabilityIndex.d.ts +1 -0
- package/node_modules/@archest/core/dist/shared/sharedCheckHaveModifier.d.ts +7 -0
- package/node_modules/@archest/core/dist/shared/sharedCheckHaveNameMatchingFileName.d.ts +3 -0
- package/node_modules/@archest/core/dist/shared/sharedCheckMatchNamePattern.d.ts +1 -0
- package/node_modules/@archest/core/dist/slices/locateSlices.d.ts +3 -0
- package/node_modules/@archest/core/dist/slices/sliceCheckBeFreeOfCycles.d.ts +3 -0
- package/node_modules/@archest/core/dist/slices/sliceCheckHaveMaxDistanceFromMainSequence.d.ts +3 -0
- package/node_modules/@archest/core/dist/slices/types.d.ts +8 -0
- package/node_modules/@archest/core/dist/types.d.ts +11 -0
- package/node_modules/@archest/core/dist/utils/ruleBuilder.d.ts +6 -0
- package/node_modules/@archest/core/package.json +49 -0
- package/node_modules/@archest/core-rust/core-rust.darwin-arm64.node +0 -0
- package/node_modules/@archest/core-rust/core-rust.linux-x64-gnu.node +0 -0
- package/node_modules/@archest/core-rust/core-rust.win32-x64-msvc.node +0 -0
- package/node_modules/@archest/core-rust/index.d.ts +16 -0
- package/node_modules/@archest/core-rust/index.js +316 -0
- package/node_modules/@archest/core-rust/package.json +34 -0
- package/package.json +12 -1
- 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/dist/src/core/types.d.ts +0 -4
- package/dist/src/index.d.ts +0 -9
- package/dist/src/matchers/index.d.ts +0 -15
- 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
- package/src/core/types.ts +0 -4
- package/src/index.ts +0 -9
- package/src/matchers/index.ts +0 -113
- package/test/architecture.test.ts +0 -59
- package/tsconfig.json +0 -17
- package/vite.config.ts +0 -18
package/src/core/ClassLocator.ts
DELETED
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript';
|
|
2
|
-
import { RuleResult } from './types';
|
|
3
|
-
|
|
4
|
-
export interface ClassQueryOptions {
|
|
5
|
-
inFolder?: string;
|
|
6
|
-
matchNamePattern?: string | RegExp;
|
|
7
|
-
withDecorator?: string;
|
|
8
|
-
extending?: string;
|
|
9
|
-
implementing?: string;
|
|
10
|
-
havingModifier?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class ClassLocator {
|
|
14
|
-
public readonly classes: ts.ClassDeclaration[];
|
|
15
|
-
|
|
16
|
-
constructor(classes: ts.ClassDeclaration[], private program: ts.Program, options?: ClassQueryOptions) {
|
|
17
|
-
let filtered = classes;
|
|
18
|
-
|
|
19
|
-
if (options?.inFolder) {
|
|
20
|
-
filtered = filtered.filter((c) => {
|
|
21
|
-
const sourceFile = c.getSourceFile();
|
|
22
|
-
return sourceFile.fileName.includes(`/${options.inFolder}/`) || sourceFile.fileName.includes(`\\${options.inFolder}\\`);
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
if (options?.matchNamePattern) {
|
|
26
|
-
const regex = typeof options.matchNamePattern === 'string' ? new RegExp(options.matchNamePattern) : options.matchNamePattern;
|
|
27
|
-
filtered = filtered.filter((c) => {
|
|
28
|
-
const name = c.name?.text;
|
|
29
|
-
return name && regex.test(name);
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
if (options?.withDecorator) {
|
|
33
|
-
filtered = filtered.filter((c) => {
|
|
34
|
-
const decorators = ts.canHaveDecorators(c) ? ts.getDecorators(c) : undefined;
|
|
35
|
-
let hasDecorator = false;
|
|
36
|
-
if (decorators) {
|
|
37
|
-
for (const dec of decorators) {
|
|
38
|
-
const exp = dec.expression;
|
|
39
|
-
let name = '';
|
|
40
|
-
if (ts.isIdentifier(exp)) {
|
|
41
|
-
name = exp.text;
|
|
42
|
-
} else if (ts.isCallExpression(exp) && ts.isIdentifier(exp.expression)) {
|
|
43
|
-
name = exp.expression.text;
|
|
44
|
-
}
|
|
45
|
-
if (name === options.withDecorator) {
|
|
46
|
-
hasDecorator = true;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return hasDecorator;
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
if (options?.extending) {
|
|
54
|
-
filtered = filtered.filter((c) => {
|
|
55
|
-
if (c.heritageClauses) {
|
|
56
|
-
for (const clause of c.heritageClauses) {
|
|
57
|
-
if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
|
|
58
|
-
for (const type of clause.types) {
|
|
59
|
-
if (ts.isIdentifier(type.expression) && type.expression.text === options.extending) {
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return false;
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
if (options?.implementing) {
|
|
70
|
-
filtered = filtered.filter((c) => {
|
|
71
|
-
if (c.heritageClauses) {
|
|
72
|
-
for (const clause of c.heritageClauses) {
|
|
73
|
-
if (clause.token === ts.SyntaxKind.ImplementsKeyword) {
|
|
74
|
-
for (const type of clause.types) {
|
|
75
|
-
if (ts.isIdentifier(type.expression) && type.expression.text === options.implementing) {
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return false;
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
if (options?.havingModifier) {
|
|
86
|
-
let targetKind: ts.SyntaxKind;
|
|
87
|
-
switch (options.havingModifier) {
|
|
88
|
-
case 'export': targetKind = ts.SyntaxKind.ExportKeyword; break;
|
|
89
|
-
case 'default': targetKind = ts.SyntaxKind.DefaultKeyword; break;
|
|
90
|
-
case 'abstract': targetKind = ts.SyntaxKind.AbstractKeyword; break;
|
|
91
|
-
default:
|
|
92
|
-
throw new Error(`Modifier ${options.havingModifier} is not fully supported.`);
|
|
93
|
-
}
|
|
94
|
-
filtered = filtered.filter((c) => {
|
|
95
|
-
const modifiers = ts.canHaveModifiers(c) ? ts.getModifiers(c) : undefined;
|
|
96
|
-
return modifiers && modifiers.some((m: ts.Modifier) => m.kind === targetKind);
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
this.classes = filtered;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
checkResideInFolder(targetFolder: string, isNot: boolean): RuleResult {
|
|
104
|
-
const violations: string[] = [];
|
|
105
|
-
for (const c of this.classes) {
|
|
106
|
-
const className = c.name?.text || 'Anonymous Class';
|
|
107
|
-
const sourceFile = c.getSourceFile();
|
|
108
|
-
const inTargetFolder = sourceFile.fileName.includes(`/${targetFolder}/`) || sourceFile.fileName.includes(`\\${targetFolder}\\`);
|
|
109
|
-
|
|
110
|
-
if (isNot && inTargetFolder) {
|
|
111
|
-
violations.push(`Class ${className} resides in ${targetFolder}, but it shouldn't.`);
|
|
112
|
-
} else if (!isNot && !inTargetFolder) {
|
|
113
|
-
violations.push(`Class ${className} does not reside in ${targetFolder}, but it should.`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return {
|
|
117
|
-
pass: violations.length === 0,
|
|
118
|
-
message: () => violations.join('\n'),
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
checkMatchNamePattern(pattern: string | RegExp, isNot: boolean): RuleResult {
|
|
123
|
-
const violations: string[] = [];
|
|
124
|
-
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
|
|
125
|
-
|
|
126
|
-
for (const c of this.classes) {
|
|
127
|
-
const name = c.name?.text;
|
|
128
|
-
const matches = name && regex.test(name);
|
|
129
|
-
if (isNot && matches) {
|
|
130
|
-
violations.push(`Class ${name} matches pattern ${pattern}, but it shouldn't.`);
|
|
131
|
-
} else if (!isNot && !matches) {
|
|
132
|
-
violations.push(`Class ${name || 'Anonymous'} does not match pattern ${pattern}, but it should.`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return {
|
|
136
|
-
pass: violations.length === 0,
|
|
137
|
-
message: () => violations.join('\n'),
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
checkExtendClass(className: string, isNot: boolean): RuleResult {
|
|
142
|
-
const violations: string[] = [];
|
|
143
|
-
for (const c of this.classes) {
|
|
144
|
-
const name = c.name?.text || 'Anonymous';
|
|
145
|
-
let matches = false;
|
|
146
|
-
if (c.heritageClauses) {
|
|
147
|
-
for (const clause of c.heritageClauses) {
|
|
148
|
-
if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
|
|
149
|
-
for (const type of clause.types) {
|
|
150
|
-
if (ts.isIdentifier(type.expression) && type.expression.text === className) {
|
|
151
|
-
matches = true;
|
|
152
|
-
break;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
if (isNot && matches) {
|
|
159
|
-
violations.push(`Class ${name} extends ${className}, but it shouldn't.`);
|
|
160
|
-
} else if (!isNot && !matches) {
|
|
161
|
-
violations.push(`Class ${name} does not extend ${className}, but it should.`);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return {
|
|
165
|
-
pass: violations.length === 0,
|
|
166
|
-
message: () => violations.join('\n'),
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
checkImplementInterface(interfaceName: string, isNot: boolean): RuleResult {
|
|
171
|
-
const violations: string[] = [];
|
|
172
|
-
for (const c of this.classes) {
|
|
173
|
-
const name = c.name?.text || 'Anonymous';
|
|
174
|
-
let matches = false;
|
|
175
|
-
if (c.heritageClauses) {
|
|
176
|
-
for (const clause of c.heritageClauses) {
|
|
177
|
-
if (clause.token === ts.SyntaxKind.ImplementsKeyword) {
|
|
178
|
-
for (const type of clause.types) {
|
|
179
|
-
if (ts.isIdentifier(type.expression) && type.expression.text === interfaceName) {
|
|
180
|
-
matches = true;
|
|
181
|
-
break;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
if (isNot && matches) {
|
|
188
|
-
violations.push(`Class ${name} implements ${interfaceName}, but it shouldn't.`);
|
|
189
|
-
} else if (!isNot && !matches) {
|
|
190
|
-
violations.push(`Class ${name} does not implement ${interfaceName}, but it should.`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return {
|
|
194
|
-
pass: violations.length === 0,
|
|
195
|
-
message: () => violations.join('\n'),
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
checkHaveModifier(modifierStr: string, isNot: boolean): RuleResult {
|
|
200
|
-
let targetKind: ts.SyntaxKind;
|
|
201
|
-
switch (modifierStr) {
|
|
202
|
-
case 'export': targetKind = ts.SyntaxKind.ExportKeyword; break;
|
|
203
|
-
case 'default': targetKind = ts.SyntaxKind.DefaultKeyword; break;
|
|
204
|
-
case 'abstract': targetKind = ts.SyntaxKind.AbstractKeyword; break;
|
|
205
|
-
default:
|
|
206
|
-
throw new Error(`Modifier ${modifierStr} is not fully supported.`);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const violations: string[] = [];
|
|
210
|
-
for (const c of this.classes) {
|
|
211
|
-
const name = c.name?.text || 'Anonymous';
|
|
212
|
-
const modifiers = ts.canHaveModifiers(c) ? ts.getModifiers(c) : undefined;
|
|
213
|
-
const matches = modifiers && modifiers.some((m: ts.Modifier) => m.kind === targetKind);
|
|
214
|
-
|
|
215
|
-
if (isNot && matches) {
|
|
216
|
-
violations.push(`Class ${name} has modifier ${modifierStr}, but it shouldn't.`);
|
|
217
|
-
} else if (!isNot && !matches) {
|
|
218
|
-
violations.push(`Class ${name} does not have modifier ${modifierStr}, but it should.`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return {
|
|
222
|
-
pass: violations.length === 0,
|
|
223
|
-
message: () => violations.join('\n'),
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
}
|
package/src/core/FileLocator.ts
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript';
|
|
2
|
-
import { RuleResult } from './types';
|
|
3
|
-
|
|
4
|
-
export interface FileQueryOptions {
|
|
5
|
-
inFolder?: string;
|
|
6
|
-
matchNamePattern?: string | RegExp;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export class FileLocator {
|
|
10
|
-
public readonly files: ts.SourceFile[];
|
|
11
|
-
|
|
12
|
-
constructor(sourceFiles: ts.SourceFile[], private program: ts.Program, options?: FileQueryOptions) {
|
|
13
|
-
let filtered = sourceFiles;
|
|
14
|
-
|
|
15
|
-
if (options?.inFolder) {
|
|
16
|
-
filtered = filtered.filter((file) => file.fileName.includes(`/${options.inFolder}/`) || file.fileName.includes(`\\${options.inFolder}\\`));
|
|
17
|
-
}
|
|
18
|
-
if (options?.matchNamePattern) {
|
|
19
|
-
const regex = typeof options.matchNamePattern === 'string' ? new RegExp(options.matchNamePattern) : options.matchNamePattern;
|
|
20
|
-
filtered = filtered.filter((file) => regex.test(file.fileName));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
this.files = filtered;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
private getFileDependencies(sourceFile: ts.SourceFile): string[] {
|
|
27
|
-
const dependencies: string[] = [];
|
|
28
|
-
const compilerOptions = this.program.getCompilerOptions();
|
|
29
|
-
|
|
30
|
-
const visit = (node: ts.Node) => {
|
|
31
|
-
if (ts.isImportDeclaration(node)) {
|
|
32
|
-
const moduleSpecifier = (node.moduleSpecifier as ts.StringLiteral).text;
|
|
33
|
-
const resolved = ts.resolveModuleName(
|
|
34
|
-
moduleSpecifier,
|
|
35
|
-
sourceFile.fileName,
|
|
36
|
-
compilerOptions,
|
|
37
|
-
ts.sys
|
|
38
|
-
);
|
|
39
|
-
if (resolved.resolvedModule && resolved.resolvedModule.resolvedFileName) {
|
|
40
|
-
if (!resolved.resolvedModule.isExternalLibraryImport) {
|
|
41
|
-
dependencies.push(resolved.resolvedModule.resolvedFileName);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
} else if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
|
|
45
|
-
const moduleSpecifier = (node.moduleSpecifier as ts.StringLiteral).text;
|
|
46
|
-
const resolved = ts.resolveModuleName(
|
|
47
|
-
moduleSpecifier,
|
|
48
|
-
sourceFile.fileName,
|
|
49
|
-
compilerOptions,
|
|
50
|
-
ts.sys
|
|
51
|
-
);
|
|
52
|
-
if (resolved.resolvedModule && resolved.resolvedModule.resolvedFileName) {
|
|
53
|
-
if (!resolved.resolvedModule.isExternalLibraryImport) {
|
|
54
|
-
dependencies.push(resolved.resolvedModule.resolvedFileName);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
ts.forEachChild(node, visit);
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
visit(sourceFile);
|
|
62
|
-
return dependencies;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
checkDependOnFilesInFolder(targetFolder: string, isNot: boolean): RuleResult {
|
|
66
|
-
const violations: string[] = [];
|
|
67
|
-
|
|
68
|
-
for (const file of this.files) {
|
|
69
|
-
const dependencies = this.getFileDependencies(file);
|
|
70
|
-
|
|
71
|
-
const dependsOnTarget = dependencies.some((depPath) => depPath.includes(`/${targetFolder}/`) || depPath.includes(`\\${targetFolder}\\`));
|
|
72
|
-
|
|
73
|
-
if (isNot && dependsOnTarget) {
|
|
74
|
-
violations.push(`File ${file.fileName} depends on files in ${targetFolder}, but it shouldn't.`);
|
|
75
|
-
} else if (!isNot && !dependsOnTarget) {
|
|
76
|
-
violations.push(`File ${file.fileName} does not depend on files in ${targetFolder}, but it should.`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
pass: violations.length === 0,
|
|
82
|
-
message: () => violations.join('\n'),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
checkBeFreeOfCycles(isNot: boolean): RuleResult {
|
|
87
|
-
const violations: string[] = [];
|
|
88
|
-
const targetedFilePaths = new Set<string>(this.files.map(f => f.fileName));
|
|
89
|
-
const graph = new Map<string, string[]>();
|
|
90
|
-
|
|
91
|
-
for (const file of this.files) {
|
|
92
|
-
const filePath = file.fileName;
|
|
93
|
-
const dependencies = this.getFileDependencies(file)
|
|
94
|
-
.filter((p) => targetedFilePaths.has(p));
|
|
95
|
-
|
|
96
|
-
graph.set(filePath, dependencies);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const visited = new Set<string>();
|
|
100
|
-
const recStack = new Set<string>();
|
|
101
|
-
const cycles: string[][] = [];
|
|
102
|
-
|
|
103
|
-
const isCyclicUtil = (node: string, path: string[]) => {
|
|
104
|
-
if (recStack.has(node)) {
|
|
105
|
-
const cycleStartIndex = path.indexOf(node);
|
|
106
|
-
const cycleStr = [...path.slice(cycleStartIndex), node].join('->');
|
|
107
|
-
if (!cycles.some(c => c.join('->') === cycleStr)) {
|
|
108
|
-
cycles.push([...path.slice(cycleStartIndex), node]);
|
|
109
|
-
}
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (visited.has(node)) return false;
|
|
114
|
-
|
|
115
|
-
visited.add(node);
|
|
116
|
-
recStack.add(node);
|
|
117
|
-
path.push(node);
|
|
118
|
-
|
|
119
|
-
const neighbors = graph.get(node) || [];
|
|
120
|
-
for (const neighbor of neighbors) {
|
|
121
|
-
isCyclicUtil(neighbor, path);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
recStack.delete(node);
|
|
125
|
-
path.pop();
|
|
126
|
-
return false;
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
for (const node of graph.keys()) {
|
|
130
|
-
if (!visited.has(node)) {
|
|
131
|
-
isCyclicUtil(node, []);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (isNot && cycles.length === 0) {
|
|
136
|
-
violations.push('Expected cyclic dependencies, but found none.');
|
|
137
|
-
} else if (!isNot && cycles.length > 0) {
|
|
138
|
-
for (const cycle of cycles) {
|
|
139
|
-
violations.push(`Cycle detected: ${cycle.join(' -> ')}`);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
pass: violations.length === 0,
|
|
145
|
-
message: () => violations.join('\n'),
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
checkMatchNamePattern(pattern: string | RegExp, isNot: boolean): RuleResult {
|
|
150
|
-
const violations: string[] = [];
|
|
151
|
-
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
|
|
152
|
-
|
|
153
|
-
for (const file of this.files) {
|
|
154
|
-
const matches = regex.test(file.fileName);
|
|
155
|
-
|
|
156
|
-
if (isNot && matches) {
|
|
157
|
-
violations.push(`File ${file.fileName} matches pattern ${pattern}, but it shouldn't.`);
|
|
158
|
-
} else if (!isNot && !matches) {
|
|
159
|
-
violations.push(`File ${file.fileName} does not match pattern ${pattern}, but it should.`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
pass: violations.length === 0,
|
|
165
|
-
message: () => violations.join('\n'),
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript';
|
|
2
|
-
import { RuleResult } from './types';
|
|
3
|
-
|
|
4
|
-
export interface FunctionQueryOptions {
|
|
5
|
-
inFolder?: string;
|
|
6
|
-
matchNamePattern?: string | RegExp;
|
|
7
|
-
isTopLevel?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export class FunctionLocator {
|
|
11
|
-
public readonly functions: ts.FunctionLikeDeclaration[];
|
|
12
|
-
|
|
13
|
-
constructor(functions: ts.FunctionLikeDeclaration[], private program: ts.Program, options?: FunctionQueryOptions) {
|
|
14
|
-
let filtered = functions;
|
|
15
|
-
|
|
16
|
-
if (options?.inFolder) {
|
|
17
|
-
filtered = filtered.filter((f) => {
|
|
18
|
-
const sourceFile = f.getSourceFile();
|
|
19
|
-
return sourceFile.fileName.includes(`/${options.inFolder}/`) || sourceFile.fileName.includes(`\\${options.inFolder}\\`);
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (options?.matchNamePattern) {
|
|
24
|
-
const regex = typeof options.matchNamePattern === 'string' ? new RegExp(options.matchNamePattern) : options.matchNamePattern;
|
|
25
|
-
filtered = filtered.filter((f) => {
|
|
26
|
-
return f.name && ts.isIdentifier(f.name) && regex.test(f.name.text);
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (options?.isTopLevel) {
|
|
31
|
-
filtered = filtered.filter((f) => {
|
|
32
|
-
return ts.isSourceFile(f.parent);
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
this.functions = filtered;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
checkHaveModifier(modifierStr: string, isNot: boolean): RuleResult {
|
|
40
|
-
let targetKind: ts.SyntaxKind;
|
|
41
|
-
switch (modifierStr) {
|
|
42
|
-
case 'export': targetKind = ts.SyntaxKind.ExportKeyword; break;
|
|
43
|
-
case 'async': targetKind = ts.SyntaxKind.AsyncKeyword; break;
|
|
44
|
-
case 'private': targetKind = ts.SyntaxKind.PrivateKeyword; break;
|
|
45
|
-
case 'public': targetKind = ts.SyntaxKind.PublicKeyword; break;
|
|
46
|
-
default:
|
|
47
|
-
throw new Error(`Modifier ${modifierStr} is not fully supported.`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const violations: string[] = [];
|
|
51
|
-
for (const f of this.functions) {
|
|
52
|
-
const name = f.name && ts.isIdentifier(f.name) ? f.name.text : 'Anonymous Function';
|
|
53
|
-
const modifiers = ts.canHaveModifiers(f) ? ts.getModifiers(f) : undefined;
|
|
54
|
-
const matches = modifiers && modifiers.some((m: ts.Modifier) => m.kind === targetKind);
|
|
55
|
-
|
|
56
|
-
if (isNot && matches) {
|
|
57
|
-
violations.push(`Function ${name} has modifier ${modifierStr}, but it shouldn't.`);
|
|
58
|
-
} else if (!isNot && !matches) {
|
|
59
|
-
violations.push(`Function ${name} does not have modifier ${modifierStr}, but it should.`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return {
|
|
63
|
-
pass: violations.length === 0,
|
|
64
|
-
message: () => violations.join('\n'),
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
checkHaveExplicitReturnType(isNot: boolean): RuleResult {
|
|
69
|
-
const violations: string[] = [];
|
|
70
|
-
for (const f of this.functions) {
|
|
71
|
-
const name = f.name && ts.isIdentifier(f.name) ? f.name.text : 'Anonymous Function';
|
|
72
|
-
const hasType = !!f.type;
|
|
73
|
-
|
|
74
|
-
if (isNot && hasType) {
|
|
75
|
-
violations.push(`Function ${name} has an explicit return type, but it shouldn't.`);
|
|
76
|
-
} else if (!isNot && !hasType) {
|
|
77
|
-
violations.push(`Function ${name} does not have an explicit return type, but it should.`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return {
|
|
81
|
-
pass: violations.length === 0,
|
|
82
|
-
message: () => violations.join('\n'),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
checkMatchNamePattern(pattern: string | RegExp, isNot: boolean): RuleResult {
|
|
87
|
-
const violations: string[] = [];
|
|
88
|
-
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
|
|
89
|
-
|
|
90
|
-
for (const f of this.functions) {
|
|
91
|
-
const name = f.name && ts.isIdentifier(f.name) ? f.name.text : undefined;
|
|
92
|
-
const matches = name && regex.test(name);
|
|
93
|
-
|
|
94
|
-
if (isNot && matches) {
|
|
95
|
-
violations.push(`Function ${name} matches pattern ${pattern}, but it shouldn't.`);
|
|
96
|
-
} else if (!isNot && !matches) {
|
|
97
|
-
violations.push(`Function ${name || 'Anonymous Function'} does not match pattern ${pattern}, but it should.`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return {
|
|
101
|
-
pass: violations.length === 0,
|
|
102
|
-
message: () => violations.join('\n'),
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
}
|
package/src/core/LayerLocator.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript';
|
|
2
|
-
import { RuleResult } from './types';
|
|
3
|
-
|
|
4
|
-
export class LayeredArchitectureBuilder {
|
|
5
|
-
private files: ts.SourceFile[];
|
|
6
|
-
private layers = new Map<string, string>();
|
|
7
|
-
private assertions: Array<(files: ts.SourceFile[]) => string[]> = [];
|
|
8
|
-
|
|
9
|
-
constructor(files: ts.SourceFile[], public program: ts.Program) {
|
|
10
|
-
this.files = files;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
layer(name: string, folderPattern: string): this {
|
|
14
|
-
this.layers.set(name, folderPattern);
|
|
15
|
-
return this;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
whereLayer(name: string): LayerAssertionBuilder {
|
|
19
|
-
if (!this.layers.has(name)) {
|
|
20
|
-
throw new Error(`Layer ${name} is not defined`);
|
|
21
|
-
}
|
|
22
|
-
return new LayerAssertionBuilder(this, name);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
addAssertion(assertion: (files: ts.SourceFile[]) => string[]) {
|
|
26
|
-
this.assertions.push(assertion);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
getFiles(): ts.SourceFile[] {
|
|
30
|
-
return this.files;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
getLayerPattern(name: string): string {
|
|
34
|
-
return this.layers.get(name)!;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
getFileDependencies(sourceFile: ts.SourceFile): string[] {
|
|
38
|
-
const dependencies: string[] = [];
|
|
39
|
-
const compilerOptions = this.program.getCompilerOptions();
|
|
40
|
-
|
|
41
|
-
const visit = (node: ts.Node) => {
|
|
42
|
-
if (ts.isImportDeclaration(node)) {
|
|
43
|
-
const moduleSpecifier = (node.moduleSpecifier as ts.StringLiteral).text;
|
|
44
|
-
const resolved = ts.resolveModuleName(moduleSpecifier, sourceFile.fileName, compilerOptions, ts.sys);
|
|
45
|
-
if (resolved.resolvedModule && resolved.resolvedModule.resolvedFileName && !resolved.resolvedModule.isExternalLibraryImport) {
|
|
46
|
-
dependencies.push(resolved.resolvedModule.resolvedFileName);
|
|
47
|
-
}
|
|
48
|
-
} else if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
|
|
49
|
-
const moduleSpecifier = (node.moduleSpecifier as ts.StringLiteral).text;
|
|
50
|
-
const resolved = ts.resolveModuleName(moduleSpecifier, sourceFile.fileName, compilerOptions, ts.sys);
|
|
51
|
-
if (resolved.resolvedModule && resolved.resolvedModule.resolvedFileName && !resolved.resolvedModule.isExternalLibraryImport) {
|
|
52
|
-
dependencies.push(resolved.resolvedModule.resolvedFileName);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
ts.forEachChild(node, visit);
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
visit(sourceFile);
|
|
59
|
-
return dependencies;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
check(): RuleResult {
|
|
63
|
-
const violations: string[] = [];
|
|
64
|
-
for (const assertion of this.assertions) {
|
|
65
|
-
violations.push(...assertion(this.files));
|
|
66
|
-
}
|
|
67
|
-
return {
|
|
68
|
-
pass: violations.length === 0,
|
|
69
|
-
message: () => violations.join('\n')
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export class LayerAssertionBuilder {
|
|
75
|
-
constructor(private parent: LayeredArchitectureBuilder, private targetLayer: string) {}
|
|
76
|
-
|
|
77
|
-
shouldNotBeAccessedByAnyLayer(): LayeredArchitectureBuilder {
|
|
78
|
-
this.parent.addAssertion((files) => {
|
|
79
|
-
const violations: string[] = [];
|
|
80
|
-
const targetPattern = this.parent.getLayerPattern(this.targetLayer);
|
|
81
|
-
|
|
82
|
-
for (const file of files) {
|
|
83
|
-
const filePath = file.fileName;
|
|
84
|
-
if (!filePath.includes(`/${targetPattern}/`) && !filePath.includes(`\\${targetPattern}\\`)) {
|
|
85
|
-
const dependencies = this.parent.getFileDependencies(file);
|
|
86
|
-
const importsTarget = dependencies.some(dep => dep.includes(`/${targetPattern}/`) || dep.includes(`\\${targetPattern}\\`));
|
|
87
|
-
if (importsTarget) {
|
|
88
|
-
violations.push(`File ${filePath} accesses layer ${this.targetLayer} but it shouldn't.`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return violations;
|
|
93
|
-
});
|
|
94
|
-
return this.parent;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
shouldOnlyBeAccessedBy(...allowedLayers: string[]): LayeredArchitectureBuilder {
|
|
98
|
-
this.parent.addAssertion((files) => {
|
|
99
|
-
const violations: string[] = [];
|
|
100
|
-
const targetPattern = this.parent.getLayerPattern(this.targetLayer);
|
|
101
|
-
const allowedPatterns = allowedLayers.map(l => this.parent.getLayerPattern(l));
|
|
102
|
-
|
|
103
|
-
for (const file of files) {
|
|
104
|
-
const filePath = file.fileName;
|
|
105
|
-
const isInAllowedLayer = allowedPatterns.some(p => filePath.includes(`/${p}/`) || filePath.includes(`\\${p}\\`));
|
|
106
|
-
|
|
107
|
-
if (!filePath.includes(`/${targetPattern}/`) && !filePath.includes(`\\${targetPattern}\\`) && !isInAllowedLayer) {
|
|
108
|
-
const dependencies = this.parent.getFileDependencies(file);
|
|
109
|
-
const importsTarget = dependencies.some(dep => dep.includes(`/${targetPattern}/`) || dep.includes(`\\${targetPattern}\\`));
|
|
110
|
-
if (importsTarget) {
|
|
111
|
-
violations.push(`File ${filePath} accesses layer ${this.targetLayer} but only ${allowedLayers.join(', ')} are allowed.`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return violations;
|
|
116
|
-
});
|
|
117
|
-
return this.parent;
|
|
118
|
-
}
|
|
119
|
-
}
|