@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.
Files changed (168) hide show
  1. package/dist/index.js +13 -17
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +776 -489
  4. package/dist/index.mjs.map +1 -1
  5. package/dist/src/core/classes/classCheckExtendClass.d.ts +3 -0
  6. package/dist/src/core/classes/classCheckHaveMaxCyclomaticComplexity.d.ts +3 -0
  7. package/dist/src/core/classes/classCheckHaveModifier.d.ts +3 -0
  8. package/dist/src/core/classes/classCheckHaveNameMatchingFileName.d.ts +3 -0
  9. package/dist/src/core/classes/classCheckImplementInterface.d.ts +3 -0
  10. package/dist/src/core/classes/classCheckMatchNamePattern.d.ts +3 -0
  11. package/dist/src/core/classes/classCheckResideInFolder.d.ts +3 -0
  12. package/dist/src/core/classes/locateClasses.d.ts +3 -0
  13. package/dist/src/core/classes/types.d.ts +14 -0
  14. package/dist/src/core/files/checkDependOnFilesInFolder.d.ts +3 -0
  15. package/dist/src/core/files/fileCheckBeFreeOfCycles.d.ts +3 -0
  16. package/dist/src/core/files/fileCheckHaveMaxCyclomaticComplexity.d.ts +3 -0
  17. package/dist/src/core/files/fileCheckHaveMaxExportedFunctions.d.ts +3 -0
  18. package/dist/src/core/files/fileCheckHaveMinMaintainabilityIndex.d.ts +3 -0
  19. package/dist/src/core/files/fileCheckMatchNamePattern.d.ts +3 -0
  20. package/dist/src/core/files/getFileDependencies.d.ts +2 -0
  21. package/dist/src/core/files/locateFiles.d.ts +3 -0
  22. package/dist/src/core/files/types.d.ts +10 -0
  23. package/dist/src/core/functions/functionCheckHaveExplicitReturnType.d.ts +3 -0
  24. package/dist/src/core/functions/functionCheckHaveMaxCyclomaticComplexity.d.ts +3 -0
  25. package/dist/src/core/functions/functionCheckHaveMinMaintainabilityIndex.d.ts +3 -0
  26. package/dist/src/core/functions/functionCheckHaveModifier.d.ts +3 -0
  27. package/dist/src/core/functions/functionCheckHaveNameMatchingFileName.d.ts +3 -0
  28. package/dist/src/core/functions/functionCheckMatchNamePattern.d.ts +3 -0
  29. package/dist/src/core/functions/locateFunctions.d.ts +3 -0
  30. package/dist/src/core/functions/types.d.ts +11 -0
  31. package/dist/src/core/layers/checkLayeredArchitecture.d.ts +3 -0
  32. package/dist/src/core/layers/createLayeredArchitecture.d.ts +3 -0
  33. package/dist/src/core/layers/getLayerDependencies.d.ts +2 -0
  34. package/dist/src/core/layers/layer.d.ts +2 -0
  35. package/dist/src/core/layers/layerShouldNotBeAccessedByAnyLayer.d.ts +2 -0
  36. package/dist/src/core/layers/layerShouldOnlyBeAccessedBy.d.ts +2 -0
  37. package/dist/src/core/layers/types.d.ts +8 -0
  38. package/dist/src/core/metrics/calculateCyclomaticComplexity.d.ts +2 -0
  39. package/dist/src/core/metrics/calculateMaintainabilityIndex.d.ts +2 -0
  40. package/dist/src/core/project/parseProject.d.ts +22 -0
  41. package/dist/src/core/properties/locateProperties.d.ts +3 -0
  42. package/dist/src/core/properties/propertyCheckBeReadonly.d.ts +3 -0
  43. package/dist/src/core/properties/types.d.ts +10 -0
  44. package/dist/src/core/shared/sharedCheckHaveMaxCyclomaticComplexity.d.ts +1 -0
  45. package/dist/src/core/shared/sharedCheckHaveMinMaintainabilityIndex.d.ts +1 -0
  46. package/dist/src/core/shared/sharedCheckHaveModifier.d.ts +2 -0
  47. package/dist/src/core/shared/sharedCheckHaveNameMatchingFileName.d.ts +2 -0
  48. package/dist/src/core/shared/sharedCheckMatchNamePattern.d.ts +1 -0
  49. package/dist/src/core/slices/locateSlices.d.ts +3 -0
  50. package/dist/src/core/slices/sliceCheckBeFreeOfCycles.d.ts +3 -0
  51. package/dist/src/core/slices/sliceCheckHaveMaxDistanceFromMainSequence.d.ts +3 -0
  52. package/dist/src/core/slices/types.d.ts +8 -0
  53. package/dist/src/core/types.d.ts +7 -0
  54. package/dist/src/core/utils/ruleBuilder.d.ts +6 -0
  55. package/dist/src/index.d.ts +34 -8
  56. package/dist/src/matchers/index.d.ts +5 -0
  57. package/package.json +1 -1
  58. package/src/core/classes/classCheckExtendClass.test.ts +30 -0
  59. package/src/core/classes/classCheckExtendClass.ts +41 -0
  60. package/src/core/classes/classCheckHaveMaxCyclomaticComplexity.test.ts +8 -0
  61. package/src/core/classes/classCheckHaveMaxCyclomaticComplexity.ts +35 -0
  62. package/src/core/classes/classCheckHaveModifier.test.ts +30 -0
  63. package/src/core/classes/classCheckHaveModifier.ts +17 -0
  64. package/src/core/classes/classCheckHaveNameMatchingFileName.test.ts +30 -0
  65. package/src/core/classes/classCheckHaveNameMatchingFileName.ts +15 -0
  66. package/src/core/classes/classCheckImplementInterface.test.ts +30 -0
  67. package/src/core/classes/classCheckImplementInterface.ts +43 -0
  68. package/src/core/classes/classCheckMatchNamePattern.test.ts +8 -0
  69. package/src/core/classes/classCheckMatchNamePattern.ts +17 -0
  70. package/src/core/classes/classCheckResideInFolder.test.ts +8 -0
  71. package/src/core/classes/classCheckResideInFolder.ts +31 -0
  72. package/src/core/classes/locateClasses.test.ts +13 -0
  73. package/src/core/classes/locateClasses.ts +127 -0
  74. package/src/core/classes/types.ts +16 -0
  75. package/src/core/files/checkDependOnFilesInFolder.test.ts +7 -0
  76. package/src/core/files/checkDependOnFilesInFolder.ts +36 -0
  77. package/src/core/files/fileCheckBeFreeOfCycles.test.ts +7 -0
  78. package/src/core/files/fileCheckBeFreeOfCycles.ts +72 -0
  79. package/src/core/files/fileCheckHaveMaxCyclomaticComplexity.test.ts +8 -0
  80. package/src/core/files/fileCheckHaveMaxCyclomaticComplexity.ts +35 -0
  81. package/src/core/files/fileCheckHaveMaxExportedFunctions.test.ts +32 -0
  82. package/src/core/files/fileCheckHaveMaxExportedFunctions.ts +45 -0
  83. package/src/core/files/fileCheckHaveMinMaintainabilityIndex.test.ts +8 -0
  84. package/src/core/files/fileCheckHaveMinMaintainabilityIndex.ts +19 -0
  85. package/src/core/files/fileCheckMatchNamePattern.test.ts +30 -0
  86. package/src/core/files/fileCheckMatchNamePattern.ts +17 -0
  87. package/src/core/files/getFileDependencies.ts +43 -0
  88. package/src/core/files/locateFiles.test.ts +30 -0
  89. package/src/core/files/locateFiles.ts +31 -0
  90. package/src/core/files/types.ts +12 -0
  91. package/src/core/functions/functionCheckHaveExplicitReturnType.test.ts +30 -0
  92. package/src/core/functions/functionCheckHaveExplicitReturnType.ts +29 -0
  93. package/src/core/functions/functionCheckHaveMaxCyclomaticComplexity.test.ts +8 -0
  94. package/src/core/functions/functionCheckHaveMaxCyclomaticComplexity.ts +20 -0
  95. package/src/core/functions/functionCheckHaveMinMaintainabilityIndex.test.ts +8 -0
  96. package/src/core/functions/functionCheckHaveMinMaintainabilityIndex.ts +20 -0
  97. package/src/core/functions/functionCheckHaveModifier.test.ts +30 -0
  98. package/src/core/functions/functionCheckHaveModifier.ts +18 -0
  99. package/src/core/functions/functionCheckHaveNameMatchingFileName.test.ts +30 -0
  100. package/src/core/functions/functionCheckHaveNameMatchingFileName.ts +16 -0
  101. package/src/core/functions/functionCheckMatchNamePattern.test.ts +30 -0
  102. package/src/core/functions/functionCheckMatchNamePattern.ts +18 -0
  103. package/src/core/functions/locateFunctions.test.ts +16 -0
  104. package/src/core/functions/locateFunctions.ts +47 -0
  105. package/src/core/functions/types.ts +13 -0
  106. package/src/core/layers/checkLayeredArchitecture.test.ts +23 -0
  107. package/src/core/layers/checkLayeredArchitecture.ts +15 -0
  108. package/src/core/layers/createLayeredArchitecture.test.ts +13 -0
  109. package/src/core/layers/createLayeredArchitecture.ts +15 -0
  110. package/src/core/layers/getLayerDependencies.ts +45 -0
  111. package/src/core/layers/layer.test.ts +12 -0
  112. package/src/core/layers/layer.ts +10 -0
  113. package/src/core/layers/layerShouldNotBeAccessedByAnyLayer.test.ts +14 -0
  114. package/src/core/layers/layerShouldNotBeAccessedByAnyLayer.ts +40 -0
  115. package/src/core/layers/layerShouldOnlyBeAccessedBy.test.ts +15 -0
  116. package/src/core/layers/layerShouldOnlyBeAccessedBy.ts +48 -0
  117. package/src/core/layers/types.ts +9 -0
  118. package/src/core/metrics/calculateCyclomaticComplexity.test.ts +23 -0
  119. package/src/core/metrics/calculateCyclomaticComplexity.ts +36 -0
  120. package/src/core/metrics/calculateMaintainabilityIndex.test.ts +38 -0
  121. package/src/core/metrics/calculateMaintainabilityIndex.ts +47 -0
  122. package/src/core/project/parseProject.test.ts +19 -0
  123. package/src/core/project/parseProject.ts +216 -0
  124. package/src/core/properties/locateProperties.test.ts +19 -0
  125. package/src/core/properties/locateProperties.ts +41 -0
  126. package/src/core/properties/propertyCheckBeReadonly.test.ts +32 -0
  127. package/src/core/properties/propertyCheckBeReadonly.ts +27 -0
  128. package/src/core/properties/types.ts +12 -0
  129. package/src/core/shared/sharedCheckHaveMaxCyclomaticComplexity.test.ts +30 -0
  130. package/src/core/shared/sharedCheckHaveMaxCyclomaticComplexity.ts +23 -0
  131. package/src/core/shared/sharedCheckHaveMinMaintainabilityIndex.test.ts +30 -0
  132. package/src/core/shared/sharedCheckHaveMinMaintainabilityIndex.ts +23 -0
  133. package/src/core/shared/sharedCheckHaveModifier.test.ts +31 -0
  134. package/src/core/shared/sharedCheckHaveModifier.ts +51 -0
  135. package/src/core/shared/sharedCheckHaveNameMatchingFileName.test.ts +17 -0
  136. package/src/core/shared/sharedCheckHaveNameMatchingFileName.ts +36 -0
  137. package/src/core/shared/sharedCheckMatchNamePattern.test.ts +29 -0
  138. package/src/core/shared/sharedCheckMatchNamePattern.ts +22 -0
  139. package/src/core/slices/locateSlices.test.ts +22 -0
  140. package/src/core/slices/locateSlices.ts +35 -0
  141. package/src/core/slices/sliceCheckBeFreeOfCycles.test.ts +26 -0
  142. package/src/core/slices/sliceCheckBeFreeOfCycles.ts +91 -0
  143. package/src/core/slices/sliceCheckHaveMaxDistanceFromMainSequence.test.ts +27 -0
  144. package/src/core/slices/sliceCheckHaveMaxDistanceFromMainSequence.ts +110 -0
  145. package/src/core/slices/types.ts +9 -0
  146. package/src/core/testUtils.ts +61 -0
  147. package/src/core/types.ts +15 -0
  148. package/src/core/utils/ruleBuilder.test.ts +24 -0
  149. package/src/core/utils/ruleBuilder.ts +28 -0
  150. package/src/index.ts +34 -8
  151. package/src/matchers/index.ts +269 -51
  152. package/test/architecture.test.ts +109 -16
  153. package/tsconfig.json +2 -1
  154. package/vite.config.ts +2 -2
  155. package/dist/src/core/ClassLocator.d.ts +0 -20
  156. package/dist/src/core/FileLocator.d.ts +0 -15
  157. package/dist/src/core/FunctionLocator.d.ts +0 -15
  158. package/dist/src/core/LayerLocator.d.ts +0 -23
  159. package/dist/src/core/Project.d.ts +0 -20
  160. package/dist/src/core/PropertyLocator.d.ts +0 -12
  161. package/dist/src/core/SliceLocator.d.ts +0 -10
  162. package/src/core/ClassLocator.ts +0 -226
  163. package/src/core/FileLocator.ts +0 -168
  164. package/src/core/FunctionLocator.ts +0 -105
  165. package/src/core/LayerLocator.ts +0 -119
  166. package/src/core/Project.ts +0 -171
  167. package/src/core/PropertyLocator.ts +0 -50
  168. 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,10 @@
1
+ import type { LayeredArchitectureData } from './types';
2
+
3
+ export function layer(
4
+ data: LayeredArchitectureData,
5
+ name: string,
6
+ folderPattern: string,
7
+ ): LayeredArchitectureData {
8
+ data.layers.set(name, folderPattern);
9
+ return data;
10
+ }
@@ -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,9 @@
1
+ import type * as ts from 'typescript';
2
+
3
+ export interface LayeredArchitectureData {
4
+ type: 'LayeredArchitecture';
5
+ files: ts.SourceFile[];
6
+ layers: Map<string, string>;
7
+ assertions: Array<(files: ts.SourceFile[]) => string[]>;
8
+ program: ts.Program;
9
+ }
@@ -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
+ });