@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,10 @@
|
|
|
1
|
+
import type * as ts from 'typescript';
|
|
2
|
+
export interface PropertyLocatorData {
|
|
3
|
+
type: 'PropertyLocator';
|
|
4
|
+
properties: ts.PropertyDeclaration[];
|
|
5
|
+
program: ts.Program;
|
|
6
|
+
}
|
|
7
|
+
export interface PropertyQueryOptions {
|
|
8
|
+
inFolder?: string;
|
|
9
|
+
matchNamePattern?: string | RegExp;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sharedCheckHaveMaxCyclomaticComplexity<T>(items: T[], getName: (item: T) => string | undefined, getComplexity: (item: T) => number, label: string, max: number, isNot: boolean): import('../types').RuleResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sharedCheckHaveMinMaintainabilityIndex<T>(items: T[], getName: (item: T) => string | undefined, getMi: (item: T) => number, label: string, min: number, isNot: boolean): import('../types').RuleResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sharedCheckMatchNamePattern<T>(items: T[], getName: (item: T) => string | undefined, label: string, pattern: string | RegExp, isNot: boolean): import('../types').RuleResult;
|
package/dist/src/core/types.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import { ClassLocatorData } from './classes/types';
|
|
2
|
+
import { FileLocatorData } from './files/types';
|
|
3
|
+
import { FunctionLocatorData } from './functions/types';
|
|
4
|
+
import { LayeredArchitectureData } from './layers/types';
|
|
5
|
+
import { PropertyLocatorData } from './properties/types';
|
|
6
|
+
import { SliceLocatorData } from './slices/types';
|
|
1
7
|
export interface RuleResult {
|
|
2
8
|
pass: boolean;
|
|
3
9
|
message: () => string;
|
|
4
10
|
}
|
|
11
|
+
export type LocatorData = FileLocatorData | ClassLocatorData | FunctionLocatorData | PropertyLocatorData | SliceLocatorData | LayeredArchitectureData;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export * from './core/
|
|
4
|
-
export * from './core/
|
|
5
|
-
export * from './core/
|
|
6
|
-
export * from './core/
|
|
7
|
-
export * from './core/
|
|
8
|
-
export * from './core/
|
|
1
|
+
export * from './core/classes/classCheckExtendClass';
|
|
2
|
+
export * from './core/classes/classCheckHaveMaxCyclomaticComplexity';
|
|
3
|
+
export * from './core/classes/classCheckHaveModifier';
|
|
4
|
+
export * from './core/classes/classCheckHaveNameMatchingFileName';
|
|
5
|
+
export * from './core/classes/classCheckImplementInterface';
|
|
6
|
+
export * from './core/classes/classCheckMatchNamePattern';
|
|
7
|
+
export * from './core/classes/classCheckResideInFolder';
|
|
8
|
+
export * from './core/classes/locateClasses';
|
|
9
|
+
export * from './core/files/checkDependOnFilesInFolder';
|
|
10
|
+
export * from './core/files/fileCheckBeFreeOfCycles';
|
|
11
|
+
export * from './core/files/fileCheckHaveMaxCyclomaticComplexity';
|
|
12
|
+
export * from './core/files/fileCheckHaveMaxExportedFunctions';
|
|
13
|
+
export * from './core/files/fileCheckHaveMinMaintainabilityIndex';
|
|
14
|
+
export * from './core/files/fileCheckMatchNamePattern';
|
|
15
|
+
export * from './core/files/locateFiles';
|
|
16
|
+
export * from './core/functions/functionCheckHaveExplicitReturnType';
|
|
17
|
+
export * from './core/functions/functionCheckHaveMaxCyclomaticComplexity';
|
|
18
|
+
export * from './core/functions/functionCheckHaveMinMaintainabilityIndex';
|
|
19
|
+
export * from './core/functions/functionCheckHaveModifier';
|
|
20
|
+
export * from './core/functions/functionCheckHaveNameMatchingFileName';
|
|
21
|
+
export * from './core/functions/functionCheckMatchNamePattern';
|
|
22
|
+
export * from './core/functions/locateFunctions';
|
|
23
|
+
export * from './core/layers/checkLayeredArchitecture';
|
|
24
|
+
export * from './core/layers/createLayeredArchitecture';
|
|
25
|
+
export * from './core/layers/layer';
|
|
26
|
+
export * from './core/layers/layerShouldNotBeAccessedByAnyLayer';
|
|
27
|
+
export * from './core/layers/layerShouldOnlyBeAccessedBy';
|
|
28
|
+
export { parseProject } from './core/project/parseProject';
|
|
29
|
+
export * from './core/properties/locateProperties';
|
|
30
|
+
export * from './core/properties/propertyCheckBeReadonly';
|
|
31
|
+
export * from './core/slices/locateSlices';
|
|
32
|
+
export * from './core/slices/sliceCheckBeFreeOfCycles';
|
|
33
|
+
export * from './core/slices/sliceCheckHaveMaxDistanceFromMainSequence';
|
|
9
34
|
export * from './core/types';
|
|
35
|
+
export { setupMatchers } from './matchers';
|
|
@@ -11,5 +11,10 @@ declare module 'vitest' {
|
|
|
11
11
|
toDependOnFilesInFolder(folder: string): void;
|
|
12
12
|
toBeFreeOfCycles(): void;
|
|
13
13
|
toMatchNamePattern(pattern: string | RegExp): void;
|
|
14
|
+
toHaveMaxCyclomaticComplexity(max: number): void;
|
|
15
|
+
toHaveMinMaintainabilityIndex(min: number): void;
|
|
16
|
+
toHaveMaxDistanceFromMainSequence(max: number): void;
|
|
17
|
+
toHaveNameMatchingFileName(): void;
|
|
18
|
+
toHaveMaxExportedFunctions(max: number): void;
|
|
14
19
|
}
|
|
15
20
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createSourceFile, getClasses } from '../testUtils';
|
|
3
|
+
import { classCheckExtendClass } from './classCheckExtendClass';
|
|
4
|
+
import type { ClassLocatorData } from './types';
|
|
5
|
+
|
|
6
|
+
describe('classCheckExtendClass', () => {
|
|
7
|
+
it('should pass if class extends correctly', () => {
|
|
8
|
+
const sourceFile = createSourceFile(`class Test extends Base {}`);
|
|
9
|
+
const data: ClassLocatorData = {
|
|
10
|
+
type: 'ClassLocator',
|
|
11
|
+
classes: getClasses(sourceFile),
|
|
12
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
13
|
+
program: {} as any,
|
|
14
|
+
};
|
|
15
|
+
const result = classCheckExtendClass(data, 'Base', false);
|
|
16
|
+
expect(result.pass).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should fail if class does not extend correctly', () => {
|
|
20
|
+
const sourceFile = createSourceFile(`class Test extends Other {}`);
|
|
21
|
+
const data: ClassLocatorData = {
|
|
22
|
+
type: 'ClassLocator',
|
|
23
|
+
classes: getClasses(sourceFile),
|
|
24
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
25
|
+
program: {} as any,
|
|
26
|
+
};
|
|
27
|
+
const result = classCheckExtendClass(data, 'Base', false);
|
|
28
|
+
expect(result.pass).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import type { RuleResult } from '../types';
|
|
3
|
+
import type { ClassLocatorData } from './types';
|
|
4
|
+
|
|
5
|
+
export function classCheckExtendClass(
|
|
6
|
+
locator: ClassLocatorData,
|
|
7
|
+
className: string,
|
|
8
|
+
isNot: boolean,
|
|
9
|
+
): RuleResult {
|
|
10
|
+
const violations: string[] = [];
|
|
11
|
+
for (const c of locator.classes) {
|
|
12
|
+
const name = c.name?.text || 'Anonymous';
|
|
13
|
+
let matches = false;
|
|
14
|
+
if (c.heritageClauses) {
|
|
15
|
+
for (const clause of c.heritageClauses) {
|
|
16
|
+
if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
|
|
17
|
+
for (const type of clause.types) {
|
|
18
|
+
if (
|
|
19
|
+
ts.isIdentifier(type.expression) &&
|
|
20
|
+
type.expression.text === className
|
|
21
|
+
) {
|
|
22
|
+
matches = true;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (isNot && matches) {
|
|
30
|
+
violations.push(`Class ${name} extends ${className}, but it shouldn't.`);
|
|
31
|
+
} else if (!isNot && !matches) {
|
|
32
|
+
violations.push(
|
|
33
|
+
`Class ${name} does not extend ${className}, but it should.`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
pass: violations.length === 0,
|
|
39
|
+
message: () => violations.join('\n'),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { classCheckHaveMaxCyclomaticComplexity } from './classCheckHaveMaxCyclomaticComplexity';
|
|
3
|
+
|
|
4
|
+
describe('classCheckHaveMaxCyclomaticComplexity', () => {
|
|
5
|
+
it('should exist and be callable', () => {
|
|
6
|
+
expect(typeof classCheckHaveMaxCyclomaticComplexity).toBe('function');
|
|
7
|
+
});
|
|
8
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { calculateCyclomaticComplexity } from '../metrics/calculateCyclomaticComplexity';
|
|
3
|
+
import { sharedCheckHaveMaxCyclomaticComplexity } from '../shared/sharedCheckHaveMaxCyclomaticComplexity';
|
|
4
|
+
import type { RuleResult } from '../types';
|
|
5
|
+
import type { ClassLocatorData } from './types';
|
|
6
|
+
|
|
7
|
+
export function classCheckHaveMaxCyclomaticComplexity(
|
|
8
|
+
locator: ClassLocatorData,
|
|
9
|
+
max: number,
|
|
10
|
+
isNot: boolean,
|
|
11
|
+
): RuleResult {
|
|
12
|
+
return sharedCheckHaveMaxCyclomaticComplexity(
|
|
13
|
+
locator.classes,
|
|
14
|
+
(c) => c.name?.text,
|
|
15
|
+
(c) => {
|
|
16
|
+
let complexity = 0;
|
|
17
|
+
const visit = (node: ts.Node) => {
|
|
18
|
+
if (
|
|
19
|
+
ts.isMethodDeclaration(node) ||
|
|
20
|
+
ts.isConstructorDeclaration(node) ||
|
|
21
|
+
ts.isGetAccessor(node) ||
|
|
22
|
+
ts.isSetAccessor(node)
|
|
23
|
+
) {
|
|
24
|
+
complexity += calculateCyclomaticComplexity(node);
|
|
25
|
+
}
|
|
26
|
+
ts.forEachChild(node, visit);
|
|
27
|
+
};
|
|
28
|
+
visit(c);
|
|
29
|
+
return complexity;
|
|
30
|
+
},
|
|
31
|
+
'Class',
|
|
32
|
+
max,
|
|
33
|
+
isNot,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createSourceFile, getClasses } from '../testUtils';
|
|
3
|
+
import { classCheckHaveModifier } from './classCheckHaveModifier';
|
|
4
|
+
import type { ClassLocatorData } from './types';
|
|
5
|
+
|
|
6
|
+
describe('classCheckHaveModifier', () => {
|
|
7
|
+
it('should pass if class has modifier', () => {
|
|
8
|
+
const sourceFile = createSourceFile(`export class Test {}`);
|
|
9
|
+
const data: ClassLocatorData = {
|
|
10
|
+
type: 'ClassLocator',
|
|
11
|
+
classes: getClasses(sourceFile),
|
|
12
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
13
|
+
program: {} as any,
|
|
14
|
+
};
|
|
15
|
+
const result = classCheckHaveModifier(data, 'export', false);
|
|
16
|
+
expect(result.pass).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should fail if class does not have modifier', () => {
|
|
20
|
+
const sourceFile = createSourceFile(`class Test {}`);
|
|
21
|
+
const data: ClassLocatorData = {
|
|
22
|
+
type: 'ClassLocator',
|
|
23
|
+
classes: getClasses(sourceFile),
|
|
24
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
25
|
+
program: {} as any,
|
|
26
|
+
};
|
|
27
|
+
const result = classCheckHaveModifier(data, 'export', false);
|
|
28
|
+
expect(result.pass).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { sharedCheckHaveModifier } from '../shared/sharedCheckHaveModifier';
|
|
2
|
+
import type { RuleResult } from '../types';
|
|
3
|
+
import type { ClassLocatorData } from './types';
|
|
4
|
+
|
|
5
|
+
export function classCheckHaveModifier(
|
|
6
|
+
locator: ClassLocatorData,
|
|
7
|
+
modifierStr: string,
|
|
8
|
+
isNot: boolean,
|
|
9
|
+
): RuleResult {
|
|
10
|
+
return sharedCheckHaveModifier(
|
|
11
|
+
locator.classes,
|
|
12
|
+
(c) => c.name?.text,
|
|
13
|
+
'Class',
|
|
14
|
+
modifierStr,
|
|
15
|
+
isNot,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createSourceFile, getClasses } from '../testUtils';
|
|
3
|
+
import { classCheckHaveNameMatchingFileName } from './classCheckHaveNameMatchingFileName';
|
|
4
|
+
import type { ClassLocatorData } from './types';
|
|
5
|
+
|
|
6
|
+
describe('classCheckHaveNameMatchingFileName', () => {
|
|
7
|
+
it('should pass if class matches filename', () => {
|
|
8
|
+
const sourceFile = createSourceFile(`class Test {}`, 'Test.ts');
|
|
9
|
+
const data: ClassLocatorData = {
|
|
10
|
+
type: 'ClassLocator',
|
|
11
|
+
classes: getClasses(sourceFile),
|
|
12
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
13
|
+
program: {} as any,
|
|
14
|
+
};
|
|
15
|
+
const result = classCheckHaveNameMatchingFileName(data, false);
|
|
16
|
+
expect(result.pass).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should fail if class does not match filename', () => {
|
|
20
|
+
const sourceFile = createSourceFile(`class Test {}`, 'Other.ts');
|
|
21
|
+
const data: ClassLocatorData = {
|
|
22
|
+
type: 'ClassLocator',
|
|
23
|
+
classes: getClasses(sourceFile),
|
|
24
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
25
|
+
program: {} as any,
|
|
26
|
+
};
|
|
27
|
+
const result = classCheckHaveNameMatchingFileName(data, false);
|
|
28
|
+
expect(result.pass).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { sharedCheckHaveNameMatchingFileName } from '../shared/sharedCheckHaveNameMatchingFileName';
|
|
2
|
+
import type { RuleResult } from '../types';
|
|
3
|
+
import type { ClassLocatorData } from './types';
|
|
4
|
+
|
|
5
|
+
export function classCheckHaveNameMatchingFileName(
|
|
6
|
+
locator: ClassLocatorData,
|
|
7
|
+
isNot: boolean,
|
|
8
|
+
): RuleResult {
|
|
9
|
+
return sharedCheckHaveNameMatchingFileName(
|
|
10
|
+
locator.classes,
|
|
11
|
+
(c) => c.name?.text,
|
|
12
|
+
'Class',
|
|
13
|
+
isNot,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createSourceFile, getClasses } from '../testUtils';
|
|
3
|
+
import { classCheckImplementInterface } from './classCheckImplementInterface';
|
|
4
|
+
import type { ClassLocatorData } from './types';
|
|
5
|
+
|
|
6
|
+
describe('classCheckImplementInterface', () => {
|
|
7
|
+
it('should pass if class implements interface', () => {
|
|
8
|
+
const sourceFile = createSourceFile(`class Test implements IBase {}`);
|
|
9
|
+
const data: ClassLocatorData = {
|
|
10
|
+
type: 'ClassLocator',
|
|
11
|
+
classes: getClasses(sourceFile),
|
|
12
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
13
|
+
program: {} as any,
|
|
14
|
+
};
|
|
15
|
+
const result = classCheckImplementInterface(data, 'IBase', false);
|
|
16
|
+
expect(result.pass).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should fail if class does not implement interface', () => {
|
|
20
|
+
const sourceFile = createSourceFile(`class Test {}`);
|
|
21
|
+
const data: ClassLocatorData = {
|
|
22
|
+
type: 'ClassLocator',
|
|
23
|
+
classes: getClasses(sourceFile),
|
|
24
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
25
|
+
program: {} as any,
|
|
26
|
+
};
|
|
27
|
+
const result = classCheckImplementInterface(data, 'IBase', false);
|
|
28
|
+
expect(result.pass).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import type { RuleResult } from '../types';
|
|
3
|
+
import type { ClassLocatorData } from './types';
|
|
4
|
+
|
|
5
|
+
export function classCheckImplementInterface(
|
|
6
|
+
locator: ClassLocatorData,
|
|
7
|
+
interfaceName: string,
|
|
8
|
+
isNot: boolean,
|
|
9
|
+
): RuleResult {
|
|
10
|
+
const violations: string[] = [];
|
|
11
|
+
for (const c of locator.classes) {
|
|
12
|
+
const name = c.name?.text || 'Anonymous';
|
|
13
|
+
let matches = false;
|
|
14
|
+
if (c.heritageClauses) {
|
|
15
|
+
for (const clause of c.heritageClauses) {
|
|
16
|
+
if (clause.token === ts.SyntaxKind.ImplementsKeyword) {
|
|
17
|
+
for (const type of clause.types) {
|
|
18
|
+
if (
|
|
19
|
+
ts.isIdentifier(type.expression) &&
|
|
20
|
+
type.expression.text === interfaceName
|
|
21
|
+
) {
|
|
22
|
+
matches = true;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (isNot && matches) {
|
|
30
|
+
violations.push(
|
|
31
|
+
`Class ${name} implements ${interfaceName}, but it shouldn't.`,
|
|
32
|
+
);
|
|
33
|
+
} else if (!isNot && !matches) {
|
|
34
|
+
violations.push(
|
|
35
|
+
`Class ${name} does not implement ${interfaceName}, but it should.`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
pass: violations.length === 0,
|
|
41
|
+
message: () => violations.join('\n'),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { classCheckMatchNamePattern } from './classCheckMatchNamePattern';
|
|
3
|
+
|
|
4
|
+
describe('classCheckMatchNamePattern', () => {
|
|
5
|
+
it('should exist and be callable', () => {
|
|
6
|
+
expect(typeof classCheckMatchNamePattern).toBe('function');
|
|
7
|
+
});
|
|
8
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { sharedCheckMatchNamePattern } from '../shared/sharedCheckMatchNamePattern';
|
|
2
|
+
import type { RuleResult } from '../types';
|
|
3
|
+
import type { ClassLocatorData } from './types';
|
|
4
|
+
|
|
5
|
+
export function classCheckMatchNamePattern(
|
|
6
|
+
locator: ClassLocatorData,
|
|
7
|
+
pattern: string | RegExp,
|
|
8
|
+
isNot: boolean,
|
|
9
|
+
): RuleResult {
|
|
10
|
+
return sharedCheckMatchNamePattern(
|
|
11
|
+
locator.classes,
|
|
12
|
+
(c) => c.name?.text,
|
|
13
|
+
'Class',
|
|
14
|
+
pattern,
|
|
15
|
+
isNot,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { classCheckResideInFolder } from './classCheckResideInFolder';
|
|
3
|
+
|
|
4
|
+
describe('classCheckResideInFolder', () => {
|
|
5
|
+
it('should exist and be callable', () => {
|
|
6
|
+
expect(typeof classCheckResideInFolder).toBe('function');
|
|
7
|
+
});
|
|
8
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RuleResult } from '../types';
|
|
2
|
+
import type { ClassLocatorData } from './types';
|
|
3
|
+
|
|
4
|
+
export function classCheckResideInFolder(
|
|
5
|
+
locator: ClassLocatorData,
|
|
6
|
+
targetFolder: string,
|
|
7
|
+
isNot: boolean,
|
|
8
|
+
): RuleResult {
|
|
9
|
+
const violations: string[] = [];
|
|
10
|
+
for (const c of locator.classes) {
|
|
11
|
+
const className = c.name?.text || 'Anonymous Class';
|
|
12
|
+
const sourceFile = c.getSourceFile();
|
|
13
|
+
const inTargetFolder =
|
|
14
|
+
sourceFile.fileName.includes(`/${targetFolder}/`) ||
|
|
15
|
+
sourceFile.fileName.includes(`\\${targetFolder}\\`);
|
|
16
|
+
|
|
17
|
+
if (isNot && inTargetFolder) {
|
|
18
|
+
violations.push(
|
|
19
|
+
`Class ${className} resides in ${targetFolder}, but it shouldn't.`,
|
|
20
|
+
);
|
|
21
|
+
} else if (!isNot && !inTargetFolder) {
|
|
22
|
+
violations.push(
|
|
23
|
+
`Class ${className} does not reside in ${targetFolder}, but it should.`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
pass: violations.length === 0,
|
|
29
|
+
message: () => violations.join('\n'),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createSourceFile, getClasses } from '../testUtils';
|
|
3
|
+
import { locateClasses } from './locateClasses';
|
|
4
|
+
|
|
5
|
+
describe('locateClasses', () => {
|
|
6
|
+
it('should filter classes by inFolder', () => {
|
|
7
|
+
const file1 = createSourceFile('class A {}', 'src/domain/A.ts');
|
|
8
|
+
const classes = getClasses(file1);
|
|
9
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
10
|
+
const result = locateClasses(classes, {} as any, { inFolder: 'infra' });
|
|
11
|
+
expect(result.classes.length).toBe(0);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import type { ClassLocatorData, ClassQueryOptions } from './types';
|
|
3
|
+
|
|
4
|
+
export function locateClasses(
|
|
5
|
+
classes: ts.ClassDeclaration[],
|
|
6
|
+
program: ts.Program,
|
|
7
|
+
options?: ClassQueryOptions,
|
|
8
|
+
): ClassLocatorData {
|
|
9
|
+
let filtered = classes;
|
|
10
|
+
|
|
11
|
+
if (options?.inFolder) {
|
|
12
|
+
filtered = filtered.filter((c) => {
|
|
13
|
+
const sourceFile = c.getSourceFile
|
|
14
|
+
? c.getSourceFile()
|
|
15
|
+
: c.parent
|
|
16
|
+
? ts.getSourceFileOfNode(c)
|
|
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
|
+
if (options?.matchNamePattern) {
|
|
26
|
+
const regex =
|
|
27
|
+
typeof options.matchNamePattern === 'string'
|
|
28
|
+
? new RegExp(options.matchNamePattern)
|
|
29
|
+
: options.matchNamePattern;
|
|
30
|
+
filtered = filtered.filter((c) => {
|
|
31
|
+
const name = c.name?.text;
|
|
32
|
+
return name && regex.test(name);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (options?.withDecorator) {
|
|
36
|
+
filtered = filtered.filter((c) => {
|
|
37
|
+
const decorators = ts.canHaveDecorators(c)
|
|
38
|
+
? ts.getDecorators(c)
|
|
39
|
+
: undefined;
|
|
40
|
+
let hasDecorator = false;
|
|
41
|
+
if (decorators) {
|
|
42
|
+
for (const dec of decorators) {
|
|
43
|
+
const exp = dec.expression;
|
|
44
|
+
let name = '';
|
|
45
|
+
if (ts.isIdentifier(exp)) {
|
|
46
|
+
name = exp.text;
|
|
47
|
+
} else if (
|
|
48
|
+
ts.isCallExpression(exp) &&
|
|
49
|
+
ts.isIdentifier(exp.expression)
|
|
50
|
+
) {
|
|
51
|
+
name = exp.expression.text;
|
|
52
|
+
}
|
|
53
|
+
if (name === options.withDecorator) {
|
|
54
|
+
hasDecorator = true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return hasDecorator;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (options?.extending) {
|
|
62
|
+
filtered = filtered.filter((c) => {
|
|
63
|
+
if (c.heritageClauses) {
|
|
64
|
+
for (const clause of c.heritageClauses) {
|
|
65
|
+
if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
|
|
66
|
+
for (const type of clause.types) {
|
|
67
|
+
if (
|
|
68
|
+
ts.isIdentifier(type.expression) &&
|
|
69
|
+
type.expression.text === options.extending
|
|
70
|
+
) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (options?.implementing) {
|
|
81
|
+
filtered = filtered.filter((c) => {
|
|
82
|
+
if (c.heritageClauses) {
|
|
83
|
+
for (const clause of c.heritageClauses) {
|
|
84
|
+
if (clause.token === ts.SyntaxKind.ImplementsKeyword) {
|
|
85
|
+
for (const type of clause.types) {
|
|
86
|
+
if (
|
|
87
|
+
ts.isIdentifier(type.expression) &&
|
|
88
|
+
type.expression.text === options.implementing
|
|
89
|
+
) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (options?.havingModifier) {
|
|
100
|
+
let targetKind: ts.SyntaxKind;
|
|
101
|
+
switch (options.havingModifier) {
|
|
102
|
+
case 'export':
|
|
103
|
+
targetKind = ts.SyntaxKind.ExportKeyword;
|
|
104
|
+
break;
|
|
105
|
+
case 'default':
|
|
106
|
+
targetKind = ts.SyntaxKind.DefaultKeyword;
|
|
107
|
+
break;
|
|
108
|
+
case 'abstract':
|
|
109
|
+
targetKind = ts.SyntaxKind.AbstractKeyword;
|
|
110
|
+
break;
|
|
111
|
+
default:
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Modifier ${options.havingModifier} is not fully supported.`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
filtered = filtered.filter((c) => {
|
|
117
|
+
const modifiers = ts.canHaveModifiers(c) ? ts.getModifiers(c) : undefined;
|
|
118
|
+
return modifiers?.some((m: ts.Modifier) => m.kind === targetKind);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
type: 'ClassLocator',
|
|
124
|
+
classes: filtered,
|
|
125
|
+
program,
|
|
126
|
+
};
|
|
127
|
+
}
|