@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,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createSourceFile, getFunctions } from '../testUtils';
|
|
3
|
+
import { functionCheckHaveNameMatchingFileName } from './functionCheckHaveNameMatchingFileName';
|
|
4
|
+
import type { FunctionLocatorData } from './types';
|
|
5
|
+
|
|
6
|
+
describe('functionCheckHaveNameMatchingFileName', () => {
|
|
7
|
+
it('should pass if function matches filename', () => {
|
|
8
|
+
const sourceFile = createSourceFile(`function test() {}`, 'test.ts');
|
|
9
|
+
const data: FunctionLocatorData = {
|
|
10
|
+
type: 'FunctionLocator',
|
|
11
|
+
functions: getFunctions(sourceFile),
|
|
12
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
13
|
+
program: {} as any,
|
|
14
|
+
};
|
|
15
|
+
const result = functionCheckHaveNameMatchingFileName(data, false);
|
|
16
|
+
expect(result.pass).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should fail if function does not match filename', () => {
|
|
20
|
+
const sourceFile = createSourceFile(`function test() {}`, 'other.ts');
|
|
21
|
+
const data: FunctionLocatorData = {
|
|
22
|
+
type: 'FunctionLocator',
|
|
23
|
+
functions: getFunctions(sourceFile),
|
|
24
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
25
|
+
program: {} as any,
|
|
26
|
+
};
|
|
27
|
+
const result = functionCheckHaveNameMatchingFileName(data, false);
|
|
28
|
+
expect(result.pass).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { sharedCheckHaveNameMatchingFileName } from '../shared/sharedCheckHaveNameMatchingFileName';
|
|
3
|
+
import type { RuleResult } from '../types';
|
|
4
|
+
import type { FunctionLocatorData } from './types';
|
|
5
|
+
|
|
6
|
+
export function functionCheckHaveNameMatchingFileName(
|
|
7
|
+
locator: FunctionLocatorData,
|
|
8
|
+
isNot: boolean,
|
|
9
|
+
): RuleResult {
|
|
10
|
+
return sharedCheckHaveNameMatchingFileName(
|
|
11
|
+
locator.functions,
|
|
12
|
+
(f) => (f.name && ts.isIdentifier(f.name) ? f.name.text : undefined),
|
|
13
|
+
'Function',
|
|
14
|
+
isNot,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createSourceFile, getFunctions } from '../testUtils';
|
|
3
|
+
import { functionCheckMatchNamePattern } from './functionCheckMatchNamePattern';
|
|
4
|
+
import type { FunctionLocatorData } from './types';
|
|
5
|
+
|
|
6
|
+
describe('functionCheckMatchNamePattern', () => {
|
|
7
|
+
it('should pass if function matches pattern', () => {
|
|
8
|
+
const sourceFile = createSourceFile(`function testFunc() {}`);
|
|
9
|
+
const data: FunctionLocatorData = {
|
|
10
|
+
type: 'FunctionLocator',
|
|
11
|
+
functions: getFunctions(sourceFile),
|
|
12
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
13
|
+
program: {} as any,
|
|
14
|
+
};
|
|
15
|
+
const result = functionCheckMatchNamePattern(data, /^test/, false);
|
|
16
|
+
expect(result.pass).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should fail if function does not match pattern', () => {
|
|
20
|
+
const sourceFile = createSourceFile(`function myFunc() {}`);
|
|
21
|
+
const data: FunctionLocatorData = {
|
|
22
|
+
type: 'FunctionLocator',
|
|
23
|
+
functions: getFunctions(sourceFile),
|
|
24
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
25
|
+
program: {} as any,
|
|
26
|
+
};
|
|
27
|
+
const result = functionCheckMatchNamePattern(data, /^test/, false);
|
|
28
|
+
expect(result.pass).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { sharedCheckMatchNamePattern } from '../shared/sharedCheckMatchNamePattern';
|
|
3
|
+
import type { RuleResult } from '../types';
|
|
4
|
+
import type { FunctionLocatorData } from './types';
|
|
5
|
+
|
|
6
|
+
export function functionCheckMatchNamePattern(
|
|
7
|
+
locator: FunctionLocatorData,
|
|
8
|
+
pattern: string | RegExp,
|
|
9
|
+
isNot: boolean,
|
|
10
|
+
): RuleResult {
|
|
11
|
+
return sharedCheckMatchNamePattern(
|
|
12
|
+
locator.functions,
|
|
13
|
+
(f) => (f.name && ts.isIdentifier(f.name) ? f.name.text : undefined),
|
|
14
|
+
'Function',
|
|
15
|
+
pattern,
|
|
16
|
+
isNot,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createSourceFile, getFunctions } from '../testUtils';
|
|
3
|
+
import { locateFunctions } from './locateFunctions';
|
|
4
|
+
|
|
5
|
+
describe('locateFunctions', () => {
|
|
6
|
+
it('should filter functions by matchNamePattern', () => {
|
|
7
|
+
const file1 = createSourceFile('function a() {}; function b() {}');
|
|
8
|
+
const functions = getFunctions(file1);
|
|
9
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
10
|
+
const result = locateFunctions(functions, {} as any, {
|
|
11
|
+
matchNamePattern: 'a',
|
|
12
|
+
});
|
|
13
|
+
expect(result.functions.length).toBe(1);
|
|
14
|
+
expect(result.functions[0].name?.text).toBe('a');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import type { FunctionLocatorData, FunctionQueryOptions } from './types';
|
|
3
|
+
|
|
4
|
+
export function locateFunctions(
|
|
5
|
+
functions: ts.FunctionLikeDeclaration[],
|
|
6
|
+
program: ts.Program,
|
|
7
|
+
options?: FunctionQueryOptions,
|
|
8
|
+
): FunctionLocatorData {
|
|
9
|
+
let filtered = functions;
|
|
10
|
+
|
|
11
|
+
if (options?.inFolder) {
|
|
12
|
+
filtered = filtered.filter((f) => {
|
|
13
|
+
const sourceFile = f.getSourceFile
|
|
14
|
+
? f.getSourceFile()
|
|
15
|
+
: f.parent
|
|
16
|
+
? ts.getSourceFileOfNode(f)
|
|
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((f) => {
|
|
32
|
+
return f.name && ts.isIdentifier(f.name) && regex.test(f.name.text);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (options?.isTopLevel) {
|
|
37
|
+
filtered = filtered.filter((f) => {
|
|
38
|
+
return ts.isSourceFile(f.parent);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
type: 'FunctionLocator',
|
|
44
|
+
functions: filtered,
|
|
45
|
+
program,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type * as ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
export interface FunctionLocatorData {
|
|
4
|
+
type: 'FunctionLocator';
|
|
5
|
+
functions: ts.FunctionLikeDeclaration[];
|
|
6
|
+
program: ts.Program;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface FunctionQueryOptions {
|
|
10
|
+
inFolder?: string;
|
|
11
|
+
matchNamePattern?: string | RegExp;
|
|
12
|
+
isTopLevel?: boolean;
|
|
13
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { checkLayeredArchitecture } from './checkLayeredArchitecture';
|
|
3
|
+
import { createLayeredArchitecture } from './createLayeredArchitecture';
|
|
4
|
+
|
|
5
|
+
describe('checkLayeredArchitecture', () => {
|
|
6
|
+
it('should evaluate all assertions and return true if none fail', () => {
|
|
7
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
8
|
+
const data = createLayeredArchitecture([], {} as any);
|
|
9
|
+
data.assertions.push(() => []); // returns no violations
|
|
10
|
+
const result = checkLayeredArchitecture(data);
|
|
11
|
+
expect(result.pass).toBe(true);
|
|
12
|
+
expect(result.message()).toBe('');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should return false if any assertion yields violations', () => {
|
|
16
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
17
|
+
const data = createLayeredArchitecture([], {} as any);
|
|
18
|
+
data.assertions.push(() => ['Layer violation!']);
|
|
19
|
+
const result = checkLayeredArchitecture(data);
|
|
20
|
+
expect(result.pass).toBe(false);
|
|
21
|
+
expect(result.message()).toBe('Layer violation!');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RuleResult } from '../types';
|
|
2
|
+
import type { LayeredArchitectureData } from './types';
|
|
3
|
+
|
|
4
|
+
export function checkLayeredArchitecture(
|
|
5
|
+
data: LayeredArchitectureData,
|
|
6
|
+
): RuleResult {
|
|
7
|
+
const violations: string[] = [];
|
|
8
|
+
for (const assertion of data.assertions) {
|
|
9
|
+
violations.push(...assertion(data.files));
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
pass: violations.length === 0,
|
|
13
|
+
message: () => violations.join('\n'),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createLayeredArchitecture } from './createLayeredArchitecture';
|
|
3
|
+
|
|
4
|
+
describe('createLayeredArchitecture', () => {
|
|
5
|
+
it('should initialize a LayeredArchitectureData object', () => {
|
|
6
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
7
|
+
const data = createLayeredArchitecture([], {} as any);
|
|
8
|
+
expect(data.type).toBe('LayeredArchitecture');
|
|
9
|
+
expect(data.files).toEqual([]);
|
|
10
|
+
expect(data.layers.size).toBe(0);
|
|
11
|
+
expect(data.assertions.length).toBe(0);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type * as ts from 'typescript';
|
|
2
|
+
import type { LayeredArchitectureData } from './types';
|
|
3
|
+
|
|
4
|
+
export function createLayeredArchitecture(
|
|
5
|
+
files: ts.SourceFile[],
|
|
6
|
+
program: ts.Program,
|
|
7
|
+
): LayeredArchitectureData {
|
|
8
|
+
return {
|
|
9
|
+
type: 'LayeredArchitecture',
|
|
10
|
+
files,
|
|
11
|
+
layers: new Map<string, string>(),
|
|
12
|
+
assertions: [],
|
|
13
|
+
program,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
export function getLayerDependencies(
|
|
4
|
+
sourceFile: ts.SourceFile,
|
|
5
|
+
program: ts.Program,
|
|
6
|
+
): string[] {
|
|
7
|
+
const dependencies: string[] = [];
|
|
8
|
+
const compilerOptions = program.getCompilerOptions();
|
|
9
|
+
|
|
10
|
+
const visit = (node: ts.Node) => {
|
|
11
|
+
if (ts.isImportDeclaration(node)) {
|
|
12
|
+
const moduleSpecifier = (node.moduleSpecifier as ts.StringLiteral).text;
|
|
13
|
+
const resolved = ts.resolveModuleName(
|
|
14
|
+
moduleSpecifier,
|
|
15
|
+
sourceFile.fileName,
|
|
16
|
+
compilerOptions,
|
|
17
|
+
ts.sys,
|
|
18
|
+
);
|
|
19
|
+
if (
|
|
20
|
+
resolved.resolvedModule?.resolvedFileName &&
|
|
21
|
+
!resolved.resolvedModule.isExternalLibraryImport
|
|
22
|
+
) {
|
|
23
|
+
dependencies.push(resolved.resolvedModule.resolvedFileName);
|
|
24
|
+
}
|
|
25
|
+
} else if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
|
|
26
|
+
const moduleSpecifier = (node.moduleSpecifier as ts.StringLiteral).text;
|
|
27
|
+
const resolved = ts.resolveModuleName(
|
|
28
|
+
moduleSpecifier,
|
|
29
|
+
sourceFile.fileName,
|
|
30
|
+
compilerOptions,
|
|
31
|
+
ts.sys,
|
|
32
|
+
);
|
|
33
|
+
if (
|
|
34
|
+
resolved.resolvedModule?.resolvedFileName &&
|
|
35
|
+
!resolved.resolvedModule.isExternalLibraryImport
|
|
36
|
+
) {
|
|
37
|
+
dependencies.push(resolved.resolvedModule.resolvedFileName);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
ts.forEachChild(node, visit);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
visit(sourceFile);
|
|
44
|
+
return dependencies;
|
|
45
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createLayeredArchitecture } from './createLayeredArchitecture';
|
|
3
|
+
import { layer } from './layer';
|
|
4
|
+
|
|
5
|
+
describe('layer', () => {
|
|
6
|
+
it('should add a layer pattern to the architecture data', () => {
|
|
7
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
8
|
+
let data = createLayeredArchitecture([], {} as any);
|
|
9
|
+
data = layer(data, 'Domain', 'src/domain');
|
|
10
|
+
expect(data.layers.get('Domain')).toBe('src/domain');
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createLayeredArchitecture } from './createLayeredArchitecture';
|
|
3
|
+
import { layer } from './layer';
|
|
4
|
+
import { layerShouldNotBeAccessedByAnyLayer } from './layerShouldNotBeAccessedByAnyLayer';
|
|
5
|
+
|
|
6
|
+
describe('layerShouldNotBeAccessedByAnyLayer', () => {
|
|
7
|
+
it('should push an assertion to the data', () => {
|
|
8
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
9
|
+
let data = createLayeredArchitecture([], {} as any);
|
|
10
|
+
data = layer(data, 'Domain', 'src/domain');
|
|
11
|
+
data = layerShouldNotBeAccessedByAnyLayer(data, 'Domain');
|
|
12
|
+
expect(data.assertions.length).toBe(1);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getLayerDependencies } from './getLayerDependencies';
|
|
2
|
+
import type { LayeredArchitectureData } from './types';
|
|
3
|
+
|
|
4
|
+
export function layerShouldNotBeAccessedByAnyLayer(
|
|
5
|
+
data: LayeredArchitectureData,
|
|
6
|
+
targetLayer: string,
|
|
7
|
+
): LayeredArchitectureData {
|
|
8
|
+
if (!data.layers.has(targetLayer)) {
|
|
9
|
+
throw new Error(`Layer ${targetLayer} is not defined`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
data.assertions.push((files) => {
|
|
13
|
+
const violations: string[] = [];
|
|
14
|
+
// biome-ignore lint/style/noNonNullAssertion: Safe map lookup
|
|
15
|
+
const targetPattern = data.layers.get(targetLayer)!;
|
|
16
|
+
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
const filePath = file.fileName;
|
|
19
|
+
if (
|
|
20
|
+
!filePath.includes(`/${targetPattern}/`) &&
|
|
21
|
+
!filePath.includes(`\\${targetPattern}\\`)
|
|
22
|
+
) {
|
|
23
|
+
const dependencies = getLayerDependencies(file, data.program);
|
|
24
|
+
const importsTarget = dependencies.some(
|
|
25
|
+
(dep) =>
|
|
26
|
+
dep.includes(`/${targetPattern}/`) ||
|
|
27
|
+
dep.includes(`\\${targetPattern}\\`),
|
|
28
|
+
);
|
|
29
|
+
if (importsTarget) {
|
|
30
|
+
violations.push(
|
|
31
|
+
`File ${filePath} accesses layer ${targetLayer} but it shouldn't.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return violations;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createLayeredArchitecture } from './createLayeredArchitecture';
|
|
3
|
+
import { layer } from './layer';
|
|
4
|
+
import { layerShouldOnlyBeAccessedBy } from './layerShouldOnlyBeAccessedBy';
|
|
5
|
+
|
|
6
|
+
describe('layerShouldOnlyBeAccessedBy', () => {
|
|
7
|
+
it('should push an assertion to the data', () => {
|
|
8
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mocking TS types for tests
|
|
9
|
+
let data = createLayeredArchitecture([], {} as any);
|
|
10
|
+
data = layer(data, 'Domain', 'src/domain');
|
|
11
|
+
data = layer(data, 'Infra', 'src/infra');
|
|
12
|
+
data = layerShouldOnlyBeAccessedBy(data, 'Domain', ['Infra']);
|
|
13
|
+
expect(data.assertions.length).toBe(1);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { getLayerDependencies } from './getLayerDependencies';
|
|
2
|
+
import type { LayeredArchitectureData } from './types';
|
|
3
|
+
|
|
4
|
+
export function layerShouldOnlyBeAccessedBy(
|
|
5
|
+
data: LayeredArchitectureData,
|
|
6
|
+
targetLayer: string,
|
|
7
|
+
allowedLayers: string[],
|
|
8
|
+
): LayeredArchitectureData {
|
|
9
|
+
if (!data.layers.has(targetLayer)) {
|
|
10
|
+
throw new Error(`Layer ${targetLayer} is not defined`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
data.assertions.push((files) => {
|
|
14
|
+
const violations: string[] = [];
|
|
15
|
+
// biome-ignore lint/style/noNonNullAssertion: Safe map lookup
|
|
16
|
+
const targetPattern = data.layers.get(targetLayer)!;
|
|
17
|
+
// biome-ignore lint/style/noNonNullAssertion: Safe map lookup
|
|
18
|
+
const allowedPatterns = allowedLayers.map((l) => data.layers.get(l)!);
|
|
19
|
+
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
const filePath = file.fileName;
|
|
22
|
+
const isInAllowedLayer = allowedPatterns.some(
|
|
23
|
+
(p) => filePath.includes(`/${p}/`) || filePath.includes(`\\${p}\\`),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
if (
|
|
27
|
+
!filePath.includes(`/${targetPattern}/`) &&
|
|
28
|
+
!filePath.includes(`\\${targetPattern}\\`) &&
|
|
29
|
+
!isInAllowedLayer
|
|
30
|
+
) {
|
|
31
|
+
const dependencies = getLayerDependencies(file, data.program);
|
|
32
|
+
const importsTarget = dependencies.some(
|
|
33
|
+
(dep) =>
|
|
34
|
+
dep.includes(`/${targetPattern}/`) ||
|
|
35
|
+
dep.includes(`\\${targetPattern}\\`),
|
|
36
|
+
);
|
|
37
|
+
if (importsTarget) {
|
|
38
|
+
violations.push(
|
|
39
|
+
`File ${filePath} accesses layer ${targetLayer} but only ${allowedLayers.join(', ')} are allowed.`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return violations;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return data;
|
|
48
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createSourceFile, getFunctions } from '../testUtils';
|
|
3
|
+
import { calculateCyclomaticComplexity } from './calculateCyclomaticComplexity';
|
|
4
|
+
|
|
5
|
+
describe('calculateCyclomaticComplexity', () => {
|
|
6
|
+
it('should return 1 for a simple function', () => {
|
|
7
|
+
const sourceFile = createSourceFile('function simple() { return 1; }');
|
|
8
|
+
const fns = getFunctions(sourceFile);
|
|
9
|
+
expect(calculateCyclomaticComplexity(fns[0])).toBe(1);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should calculate higher complexity for branches', () => {
|
|
13
|
+
const sourceFile = createSourceFile(`
|
|
14
|
+
function complex(a) {
|
|
15
|
+
if (a) return 1;
|
|
16
|
+
if (!a) return 2;
|
|
17
|
+
return a ? 3 : 4;
|
|
18
|
+
}
|
|
19
|
+
`);
|
|
20
|
+
const fns = getFunctions(sourceFile);
|
|
21
|
+
expect(calculateCyclomaticComplexity(fns[0])).toBe(4);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
export function calculateCyclomaticComplexity(node: ts.Node): number {
|
|
4
|
+
let complexity = 1;
|
|
5
|
+
|
|
6
|
+
const visit = (n: ts.Node) => {
|
|
7
|
+
switch (n.kind) {
|
|
8
|
+
case ts.SyntaxKind.IfStatement:
|
|
9
|
+
case ts.SyntaxKind.ForStatement:
|
|
10
|
+
case ts.SyntaxKind.ForInStatement:
|
|
11
|
+
case ts.SyntaxKind.ForOfStatement:
|
|
12
|
+
case ts.SyntaxKind.WhileStatement:
|
|
13
|
+
case ts.SyntaxKind.DoStatement:
|
|
14
|
+
case ts.SyntaxKind.CaseClause:
|
|
15
|
+
case ts.SyntaxKind.CatchClause:
|
|
16
|
+
case ts.SyntaxKind.ConditionalExpression:
|
|
17
|
+
complexity++;
|
|
18
|
+
break;
|
|
19
|
+
case ts.SyntaxKind.BinaryExpression: {
|
|
20
|
+
const binary = n as ts.BinaryExpression;
|
|
21
|
+
if (
|
|
22
|
+
binary.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
|
|
23
|
+
binary.operatorToken.kind === ts.SyntaxKind.BarBarToken ||
|
|
24
|
+
binary.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken
|
|
25
|
+
) {
|
|
26
|
+
complexity++;
|
|
27
|
+
}
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
ts.forEachChild(n, visit);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
ts.forEachChild(node, visit);
|
|
35
|
+
return complexity;
|
|
36
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createSourceFile, getFunctions } from '../testUtils';
|
|
3
|
+
import { calculateMaintainabilityIndex } from './calculateMaintainabilityIndex';
|
|
4
|
+
|
|
5
|
+
describe('calculateMaintainabilityIndex', () => {
|
|
6
|
+
it('should return a high index for a simple function', () => {
|
|
7
|
+
const sourceFile = createSourceFile('function simple() { return 1; }');
|
|
8
|
+
const fns = getFunctions(sourceFile);
|
|
9
|
+
expect(calculateMaintainabilityIndex(fns[0])).toBeGreaterThan(80);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should return a lower index for a complex function', () => {
|
|
13
|
+
const sourceFile = createSourceFile(`
|
|
14
|
+
function complex(a, b, c) {
|
|
15
|
+
if (a) {
|
|
16
|
+
if (b) {
|
|
17
|
+
for (let i = 0; i < 10; i++) {
|
|
18
|
+
console.log(c);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
switch(a) {
|
|
23
|
+
case 1: return 1;
|
|
24
|
+
case 2: return 2;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
`);
|
|
28
|
+
const fns = getFunctions(sourceFile);
|
|
29
|
+
const complexMI = calculateMaintainabilityIndex(fns[0]);
|
|
30
|
+
const simpleSourceFile = createSourceFile(
|
|
31
|
+
'function simple() { return 1; }',
|
|
32
|
+
);
|
|
33
|
+
const simpleMI = calculateMaintainabilityIndex(
|
|
34
|
+
getFunctions(simpleSourceFile)[0],
|
|
35
|
+
);
|
|
36
|
+
expect(complexMI).toBeLessThan(simpleMI);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { calculateCyclomaticComplexity } from './calculateCyclomaticComplexity';
|
|
3
|
+
|
|
4
|
+
export function calculateMaintainabilityIndex(node: ts.Node): number {
|
|
5
|
+
let operators = 0;
|
|
6
|
+
let operands = 0;
|
|
7
|
+
const uniqueOperators = new Set<number>();
|
|
8
|
+
const uniqueOperands = new Set<string>();
|
|
9
|
+
let linesOfCode = 1;
|
|
10
|
+
|
|
11
|
+
const sourceFile = node.getSourceFile();
|
|
12
|
+
if (sourceFile) {
|
|
13
|
+
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
14
|
+
const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
|
|
15
|
+
linesOfCode = end.line - start.line + 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const visit = (n: ts.Node) => {
|
|
19
|
+
if (
|
|
20
|
+
ts.isBinaryExpression(n) ||
|
|
21
|
+
ts.isPrefixUnaryExpression(n) ||
|
|
22
|
+
ts.isPostfixUnaryExpression(n)
|
|
23
|
+
) {
|
|
24
|
+
operators++;
|
|
25
|
+
uniqueOperators.add(n.kind);
|
|
26
|
+
} else if (ts.isIdentifier(n) || ts.isLiteralExpression(n)) {
|
|
27
|
+
operands++;
|
|
28
|
+
uniqueOperands.add(n.getText(sourceFile));
|
|
29
|
+
}
|
|
30
|
+
ts.forEachChild(n, visit);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
visit(node);
|
|
34
|
+
|
|
35
|
+
const N = operators + operands;
|
|
36
|
+
const n = uniqueOperators.size + uniqueOperands.size;
|
|
37
|
+
const V = n === 0 ? 0 : N * Math.log2(n);
|
|
38
|
+
const CC = calculateCyclomaticComplexity(node);
|
|
39
|
+
|
|
40
|
+
const mi = Math.max(
|
|
41
|
+
0,
|
|
42
|
+
((171 - 5.2 * Math.log(V || 1) - 0.23 * CC - 16.2 * Math.log(linesOfCode)) *
|
|
43
|
+
100) /
|
|
44
|
+
171,
|
|
45
|
+
);
|
|
46
|
+
return mi;
|
|
47
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { parseProject } from './parseProject';
|
|
3
|
+
|
|
4
|
+
describe('parseProject', () => {
|
|
5
|
+
it('should parse the current project correctly', () => {
|
|
6
|
+
// Running from within vitest-arch, it should pick up the tsconfig
|
|
7
|
+
const project = parseProject();
|
|
8
|
+
expect(project).toHaveProperty('getFiles');
|
|
9
|
+
expect(project).toHaveProperty('getClasses');
|
|
10
|
+
expect(project).toHaveProperty('getFunctions');
|
|
11
|
+
expect(project).toHaveProperty('layeredArchitecture');
|
|
12
|
+
expect(project).toHaveProperty('getSlices');
|
|
13
|
+
|
|
14
|
+
// Verify it can get its own files
|
|
15
|
+
const coreFiles = project.getFiles({ inFolder: 'core' });
|
|
16
|
+
expect(coreFiles.files.length).toBeGreaterThan(0);
|
|
17
|
+
expect(coreFiles.type).toBe('FileLocator');
|
|
18
|
+
});
|
|
19
|
+
});
|