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