@alexgorbatchev/typescript-ai-policy 0.1.0

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 (62) hide show
  1. package/README.md +223 -0
  2. package/package.json +60 -0
  3. package/src/oxfmt/createOxfmtConfig.ts +26 -0
  4. package/src/oxlint/assertNoRuleCollisions.ts +40 -0
  5. package/src/oxlint/createOxlintConfig.ts +161 -0
  6. package/src/oxlint/oxlint.config.ts +3 -0
  7. package/src/oxlint/plugin.ts +90 -0
  8. package/src/oxlint/rules/component-directory-file-convention.ts +65 -0
  9. package/src/oxlint/rules/component-file-contract.ts +328 -0
  10. package/src/oxlint/rules/component-file-location-convention.ts +43 -0
  11. package/src/oxlint/rules/component-file-naming-convention.ts +260 -0
  12. package/src/oxlint/rules/component-story-file-convention.ts +108 -0
  13. package/src/oxlint/rules/fixture-export-naming-convention.ts +72 -0
  14. package/src/oxlint/rules/fixture-export-type-contract.ts +264 -0
  15. package/src/oxlint/rules/fixture-file-contract.ts +91 -0
  16. package/src/oxlint/rules/fixture-import-path-convention.ts +125 -0
  17. package/src/oxlint/rules/helpers.ts +544 -0
  18. package/src/oxlint/rules/hook-export-location-convention.ts +169 -0
  19. package/src/oxlint/rules/hook-file-contract.ts +179 -0
  20. package/src/oxlint/rules/hook-file-naming-convention.ts +151 -0
  21. package/src/oxlint/rules/hook-test-file-convention.ts +60 -0
  22. package/src/oxlint/rules/hooks-directory-file-convention.ts +75 -0
  23. package/src/oxlint/rules/index-file-contract.ts +177 -0
  24. package/src/oxlint/rules/interface-naming-convention.ts +72 -0
  25. package/src/oxlint/rules/no-conditional-logic-in-tests.ts +53 -0
  26. package/src/oxlint/rules/no-fixture-exports-outside-fixture-entrypoint.ts +68 -0
  27. package/src/oxlint/rules/no-imports-from-tests-directory.ts +114 -0
  28. package/src/oxlint/rules/no-inline-fixture-bindings-in-tests.ts +54 -0
  29. package/src/oxlint/rules/no-inline-type-expressions.ts +169 -0
  30. package/src/oxlint/rules/no-local-type-declarations-in-fixture-files.ts +55 -0
  31. package/src/oxlint/rules/no-module-mocking.ts +85 -0
  32. package/src/oxlint/rules/no-non-running-tests.ts +72 -0
  33. package/src/oxlint/rules/no-react-create-element.ts +59 -0
  34. package/src/oxlint/rules/no-test-file-exports.ts +52 -0
  35. package/src/oxlint/rules/no-throw-in-tests.ts +40 -0
  36. package/src/oxlint/rules/no-type-exports-from-constants.ts +97 -0
  37. package/src/oxlint/rules/no-type-imports-from-constants.ts +73 -0
  38. package/src/oxlint/rules/no-value-exports-from-types.ts +115 -0
  39. package/src/oxlint/rules/require-component-root-testid.ts +547 -0
  40. package/src/oxlint/rules/require-template-indent.ts +83 -0
  41. package/src/oxlint/rules/single-fixture-entrypoint.ts +142 -0
  42. package/src/oxlint/rules/stories-directory-file-convention.ts +55 -0
  43. package/src/oxlint/rules/story-export-contract.ts +343 -0
  44. package/src/oxlint/rules/story-file-location-convention.ts +64 -0
  45. package/src/oxlint/rules/story-meta-type-annotation.ts +129 -0
  46. package/src/oxlint/rules/test-file-location-convention.ts +115 -0
  47. package/src/oxlint/rules/testid-naming-convention.ts +63 -0
  48. package/src/oxlint/rules/tests-directory-file-convention.ts +55 -0
  49. package/src/oxlint/rules/types.ts +45 -0
  50. package/src/semantic-fixes/applyFileChanges.ts +81 -0
  51. package/src/semantic-fixes/applySemanticFixes.ts +239 -0
  52. package/src/semantic-fixes/applyTextEdits.ts +164 -0
  53. package/src/semantic-fixes/backends/tsgo-lsp/TsgoLspClient.ts +439 -0
  54. package/src/semantic-fixes/backends/tsgo-lsp/createTsgoLspSemanticFixBackend.ts +251 -0
  55. package/src/semantic-fixes/providers/createInterfaceNamingConventionSemanticFixProvider.ts +132 -0
  56. package/src/semantic-fixes/providers/createTestFileLocationConventionSemanticFixProvider.ts +52 -0
  57. package/src/semantic-fixes/readMovedFileTextEdits.ts +150 -0
  58. package/src/semantic-fixes/readSemanticFixRuntimePaths.ts +38 -0
  59. package/src/semantic-fixes/runApplySemanticFixes.ts +120 -0
  60. package/src/semantic-fixes/runOxlintJson.ts +139 -0
  61. package/src/semantic-fixes/types.ts +163 -0
  62. package/src/shared/mergeConfig.ts +38 -0
@@ -0,0 +1,108 @@
1
+ import type { TSESTree } from "@typescript-eslint/types";
2
+ import type { AstProgram, AstProgramStatement, RuleModule } from "./types.ts";
3
+ import { dirname, join } from "node:path";
4
+ import {
5
+ findDescendantFilePath,
6
+ getFilenameWithoutExtension,
7
+ isExemptSupportBasename,
8
+ isInStoriesDirectory,
9
+ isInTestsDirectory,
10
+ } from "./helpers.ts";
11
+
12
+ function readRequiredStoriesDirectoryPath(filename: string): string {
13
+ return join(dirname(filename), "stories");
14
+ }
15
+
16
+ function readRequiredStoryFileName(filename: string): string {
17
+ const sourceBaseName = getFilenameWithoutExtension(filename);
18
+
19
+ return `${sourceBaseName}.stories.tsx`;
20
+ }
21
+
22
+ type ReportNode = AstProgramStatement | TSESTree.Node;
23
+
24
+ function readReportNode(program: AstProgram): ReportNode {
25
+ for (const statement of program.body) {
26
+ if (statement.type === "ExportNamedDeclaration") {
27
+ if (statement.exportKind === "type") {
28
+ continue;
29
+ }
30
+
31
+ if (!statement.declaration) {
32
+ return statement.specifiers[0] ?? statement;
33
+ }
34
+
35
+ if (
36
+ statement.declaration.type === "FunctionDeclaration" ||
37
+ statement.declaration.type === "ClassDeclaration" ||
38
+ statement.declaration.type === "TSEnumDeclaration"
39
+ ) {
40
+ return statement.declaration.id ?? statement.declaration;
41
+ }
42
+
43
+ if (statement.declaration.type === "VariableDeclaration") {
44
+ const firstDeclarator = statement.declaration.declarations[0];
45
+ return firstDeclarator?.id.type === "Identifier"
46
+ ? firstDeclarator.id
47
+ : (firstDeclarator ?? statement.declaration);
48
+ }
49
+
50
+ return statement.declaration;
51
+ }
52
+
53
+ if (statement.type === "ExportDefaultDeclaration" || statement.type === "TSExportAssignment") {
54
+ return statement;
55
+ }
56
+
57
+ if (statement.type === "ExportAllDeclaration" && statement.exportKind !== "type") {
58
+ return statement;
59
+ }
60
+ }
61
+
62
+ return program.body[0] ?? program;
63
+ }
64
+
65
+ const componentStoryFileConventionRule: RuleModule = {
66
+ meta: {
67
+ type: "problem" as const,
68
+ docs: {
69
+ description:
70
+ 'Require every component ownership file to have a matching "basename.stories.tsx" file somewhere under a sibling "stories/" directory',
71
+ },
72
+ schema: [],
73
+ messages: {
74
+ missingComponentStoryFile:
75
+ 'Add a story file named "{{ requiredStoryFileName }}" somewhere under "{{ requiredStoriesDirectoryPath }}". Component ownership files must keep their Storybook coverage under a sibling "stories/" directory.',
76
+ },
77
+ },
78
+ create(context) {
79
+ if (
80
+ isExemptSupportBasename(context.filename) ||
81
+ isInStoriesDirectory(context.filename) ||
82
+ isInTestsDirectory(context.filename)
83
+ ) {
84
+ return {};
85
+ }
86
+
87
+ return {
88
+ Program(node: AstProgram) {
89
+ const requiredStoriesDirectoryPath = readRequiredStoriesDirectoryPath(context.filename);
90
+ const requiredStoryFileName = readRequiredStoryFileName(context.filename);
91
+ if (findDescendantFilePath(requiredStoriesDirectoryPath, requiredStoryFileName)) {
92
+ return;
93
+ }
94
+
95
+ context.report({
96
+ node: readReportNode(node),
97
+ messageId: "missingComponentStoryFile",
98
+ data: {
99
+ requiredStoriesDirectoryPath,
100
+ requiredStoryFileName,
101
+ },
102
+ });
103
+ },
104
+ };
105
+ },
106
+ };
107
+
108
+ export default componentStoryFileConventionRule;
@@ -0,0 +1,72 @@
1
+ import type { RuleModule } from "./types.ts";
2
+ import { isFactoryFunctionName, isFixtureConstName, isFixturesFile } from "./helpers.ts";
3
+
4
+ const fixtureExportNamingConventionRule: RuleModule = {
5
+ meta: {
6
+ type: "problem" as const,
7
+ docs: {
8
+ description:
9
+ "Enforce fixture_ and factory_ lowerCamelCase export names in the __tests__/fixtures or stories/fixtures entrypoint",
10
+ },
11
+ schema: [],
12
+ messages: {
13
+ invalidFactoryExportName:
14
+ 'Rename this factory export to the "factory_<lowerCamelCase>" form. Received "{{ name }}".',
15
+ invalidFixtureExportName:
16
+ 'Rename this fixture export to the "fixture_<lowerCamelCase>" form. Received "{{ name }}".',
17
+ },
18
+ },
19
+ create(context) {
20
+ if (!isFixturesFile(context.filename)) {
21
+ return {};
22
+ }
23
+
24
+ return {
25
+ ExportNamedDeclaration(node) {
26
+ const declaration = node.declaration;
27
+ if (!declaration) {
28
+ return;
29
+ }
30
+
31
+ if (declaration.type === "VariableDeclaration" && declaration.kind === "const") {
32
+ declaration.declarations.forEach((declarator) => {
33
+ if (declarator.id.type !== "Identifier") {
34
+ return;
35
+ }
36
+
37
+ if (isFixtureConstName(declarator.id.name)) {
38
+ return;
39
+ }
40
+
41
+ context.report({
42
+ node: declarator.id,
43
+ messageId: "invalidFixtureExportName",
44
+ data: {
45
+ name: declarator.id.name,
46
+ },
47
+ });
48
+ });
49
+ return;
50
+ }
51
+
52
+ if (declaration.type !== "FunctionDeclaration" || !declaration.id) {
53
+ return;
54
+ }
55
+
56
+ if (isFactoryFunctionName(declaration.id.name)) {
57
+ return;
58
+ }
59
+
60
+ context.report({
61
+ node: declaration.id,
62
+ messageId: "invalidFactoryExportName",
63
+ data: {
64
+ name: declaration.id.name,
65
+ },
66
+ });
67
+ },
68
+ };
69
+ },
70
+ };
71
+
72
+ export default fixtureExportNamingConventionRule;
@@ -0,0 +1,264 @@
1
+ import type { TSESTree } from "@typescript-eslint/types";
2
+ import type { AstProgram, AstVariableDeclarator, RuleModule } from "./types.ts";
3
+ import { isFactoryFunctionName, isFixtureConstName, isFixturesFile, readChildNodes } from "./helpers.ts";
4
+
5
+ type TypeContractAnalysis = {
6
+ hasAnyKeyword: boolean;
7
+ hasImportedTypeReference: boolean;
8
+ hasUnknownKeyword: boolean;
9
+ };
10
+
11
+ function readRootQualifiedIdentifierName(node: TSESTree.EntityName | null | undefined): string | null {
12
+ if (!node) {
13
+ return null;
14
+ }
15
+
16
+ if (node.type === "Identifier") {
17
+ return node.name;
18
+ }
19
+
20
+ if (node.type === "ThisExpression") {
21
+ return null;
22
+ }
23
+
24
+ return readRootQualifiedIdentifierName(node.left);
25
+ }
26
+
27
+ function readTypeContractNodesForFixtureDeclarator(declarator: AstVariableDeclarator): TSESTree.TypeNode[] {
28
+ const typeContractNodes: TSESTree.TypeNode[] = [];
29
+
30
+ if (declarator.id.type === "Identifier" && declarator.id.typeAnnotation) {
31
+ typeContractNodes.push(declarator.id.typeAnnotation.typeAnnotation);
32
+ }
33
+
34
+ if (declarator.init?.type === "TSSatisfiesExpression") {
35
+ typeContractNodes.push(declarator.init.typeAnnotation);
36
+ }
37
+
38
+ return typeContractNodes;
39
+ }
40
+
41
+ // Best-effort only: this rule is intentionally syntactic. It can require fixture
42
+ // exports to reference imported type names and can reject explicit any/unknown in
43
+ // the local type contract, but it does not resolve imported aliases transitively.
44
+ // If an imported type eventually resolves to any/unknown elsewhere, this rule
45
+ // will not detect that.
46
+ function analyzeTypeContractNode(
47
+ typeNode: TSESTree.TypeNode,
48
+ importedTypeNames: ReadonlySet<string>,
49
+ ): TypeContractAnalysis {
50
+ const state: TypeContractAnalysis = {
51
+ hasImportedTypeReference: false,
52
+ hasAnyKeyword: false,
53
+ hasUnknownKeyword: false,
54
+ };
55
+
56
+ visitNode(typeNode);
57
+ return state;
58
+
59
+ function visitNode(node: TSESTree.Node | null | undefined): void {
60
+ if (!node) {
61
+ return;
62
+ }
63
+
64
+ if (node.type === "TSAnyKeyword") {
65
+ state.hasAnyKeyword = true;
66
+ }
67
+
68
+ if (node.type === "TSUnknownKeyword") {
69
+ state.hasUnknownKeyword = true;
70
+ }
71
+
72
+ if (node.type === "TSTypeReference") {
73
+ const rootQualifiedIdentifierName = readRootQualifiedIdentifierName(node.typeName);
74
+ if (rootQualifiedIdentifierName && importedTypeNames.has(rootQualifiedIdentifierName)) {
75
+ state.hasImportedTypeReference = true;
76
+ }
77
+ }
78
+
79
+ readChildNodes(node).forEach(visitNode);
80
+ }
81
+ }
82
+
83
+ function readImportedTypeNames(program: AstProgram): Set<string> {
84
+ const importedTypeNames = new Set<string>();
85
+
86
+ program.body.forEach((statement) => {
87
+ if (statement.type !== "ImportDeclaration") {
88
+ return;
89
+ }
90
+
91
+ statement.specifiers.forEach((specifier) => {
92
+ if (specifier.local.type !== "Identifier") {
93
+ return;
94
+ }
95
+
96
+ importedTypeNames.add(specifier.local.name);
97
+ });
98
+ });
99
+
100
+ return importedTypeNames;
101
+ }
102
+
103
+ const fixtureExportTypeContractRule: RuleModule = {
104
+ meta: {
105
+ type: "problem" as const,
106
+ docs: {
107
+ description:
108
+ "Require __tests__/fixtures and stories/fixtures exports to use imported types and ban explicit any/unknown type contracts",
109
+ },
110
+ schema: [],
111
+ messages: {
112
+ forbiddenFactoryReturnTypeKeyword:
113
+ 'Replace "{{ keyword }}" with an imported concrete return type for factory export "{{ name }}". Do not use "{{ keyword }}" in fixture entrypoints.',
114
+ forbiddenFixtureTypeKeyword:
115
+ 'Replace "{{ keyword }}" with an imported concrete type for fixture export "{{ name }}". Do not use "{{ keyword }}" in fixture entrypoints.',
116
+ missingFactoryReturnTypeContract:
117
+ 'Add an explicit return type to factory export "{{ name }}" and make that return type an imported concrete type.',
118
+ missingFixtureTypeContract:
119
+ 'Add a type annotation or satisfies clause to fixture export "{{ name }}" and make it reference an imported concrete type.',
120
+ missingImportedFactoryReturnType:
121
+ 'Change the return type of factory export "{{ name }}" so it references at least one imported concrete type.',
122
+ missingImportedFixtureType:
123
+ 'Change the type annotation or satisfies clause of fixture export "{{ name }}" so it references at least one imported concrete type.',
124
+ },
125
+ },
126
+ create(context) {
127
+ if (!isFixturesFile(context.filename)) {
128
+ return {};
129
+ }
130
+
131
+ let importedTypeNames = new Set<string>();
132
+
133
+ return {
134
+ Program(program) {
135
+ importedTypeNames = readImportedTypeNames(program);
136
+ },
137
+ ExportNamedDeclaration(node) {
138
+ const declaration = node.declaration;
139
+ if (!declaration) {
140
+ return;
141
+ }
142
+
143
+ if (declaration.type === "VariableDeclaration") {
144
+ declaration.declarations.forEach((declarator) => {
145
+ if (declarator.id.type !== "Identifier" || !isFixtureConstName(declarator.id.name)) {
146
+ return;
147
+ }
148
+
149
+ const typeContractNodes = readTypeContractNodesForFixtureDeclarator(declarator);
150
+ if (typeContractNodes.length === 0) {
151
+ context.report({
152
+ node: declarator.id,
153
+ messageId: "missingFixtureTypeContract",
154
+ data: {
155
+ name: declarator.id.name,
156
+ },
157
+ });
158
+ return;
159
+ }
160
+
161
+ const typeContractAnalyses = typeContractNodes.map((typeContractNode) => {
162
+ return analyzeTypeContractNode(typeContractNode, importedTypeNames);
163
+ });
164
+ if (typeContractAnalyses.some((typeContractAnalysis) => typeContractAnalysis.hasAnyKeyword)) {
165
+ context.report({
166
+ node: declarator.id,
167
+ messageId: "forbiddenFixtureTypeKeyword",
168
+ data: {
169
+ name: declarator.id.name,
170
+ keyword: "any",
171
+ },
172
+ });
173
+ return;
174
+ }
175
+
176
+ if (typeContractAnalyses.some((typeContractAnalysis) => typeContractAnalysis.hasUnknownKeyword)) {
177
+ context.report({
178
+ node: declarator.id,
179
+ messageId: "forbiddenFixtureTypeKeyword",
180
+ data: {
181
+ name: declarator.id.name,
182
+ keyword: "unknown",
183
+ },
184
+ });
185
+ return;
186
+ }
187
+
188
+ if (typeContractAnalyses.some((typeContractAnalysis) => typeContractAnalysis.hasImportedTypeReference)) {
189
+ return;
190
+ }
191
+
192
+ context.report({
193
+ node: declarator.id,
194
+ messageId: "missingImportedFixtureType",
195
+ data: {
196
+ name: declarator.id.name,
197
+ },
198
+ });
199
+ });
200
+ return;
201
+ }
202
+
203
+ if (
204
+ declaration.type !== "FunctionDeclaration" ||
205
+ !declaration.id ||
206
+ !isFactoryFunctionName(declaration.id.name)
207
+ ) {
208
+ return;
209
+ }
210
+
211
+ const returnTypeNode = declaration.returnType?.typeAnnotation;
212
+ if (!returnTypeNode) {
213
+ context.report({
214
+ node: declaration.id,
215
+ messageId: "missingFactoryReturnTypeContract",
216
+ data: {
217
+ name: declaration.id.name,
218
+ },
219
+ });
220
+ return;
221
+ }
222
+
223
+ const returnTypeAnalysis = analyzeTypeContractNode(returnTypeNode, importedTypeNames);
224
+ if (returnTypeAnalysis.hasAnyKeyword) {
225
+ context.report({
226
+ node: declaration.id,
227
+ messageId: "forbiddenFactoryReturnTypeKeyword",
228
+ data: {
229
+ name: declaration.id.name,
230
+ keyword: "any",
231
+ },
232
+ });
233
+ return;
234
+ }
235
+
236
+ if (returnTypeAnalysis.hasUnknownKeyword) {
237
+ context.report({
238
+ node: declaration.id,
239
+ messageId: "forbiddenFactoryReturnTypeKeyword",
240
+ data: {
241
+ name: declaration.id.name,
242
+ keyword: "unknown",
243
+ },
244
+ });
245
+ return;
246
+ }
247
+
248
+ if (returnTypeAnalysis.hasImportedTypeReference) {
249
+ return;
250
+ }
251
+
252
+ context.report({
253
+ node: declaration.id,
254
+ messageId: "missingImportedFactoryReturnType",
255
+ data: {
256
+ name: declaration.id.name,
257
+ },
258
+ });
259
+ },
260
+ };
261
+ },
262
+ };
263
+
264
+ export default fixtureExportTypeContractRule;
@@ -0,0 +1,91 @@
1
+ import type { RuleModule } from "./types.ts";
2
+ import { isFixturesFile } from "./helpers.ts";
3
+
4
+ const fixtureFileContractRule: RuleModule = {
5
+ meta: {
6
+ type: "problem" as const,
7
+ docs: {
8
+ description:
9
+ 'Only allow direct const fixture exports and function factory exports in nested "fixtures.ts" or "fixtures.tsx" entrypoints under "__tests__/" or "stories/"',
10
+ },
11
+ schema: [],
12
+ messages: {
13
+ unexpectedDefaultExport:
14
+ 'Remove the default export. A nested "fixtures.ts" or "fixtures.tsx" entrypoint under "__tests__/" or "stories/" must use only named exports.',
15
+ unexpectedExportDeclaration:
16
+ 'Replace this export with either "export const fixture_* = ..." or "export function factory_*() { ... }" in a nested "fixtures.ts" or "fixtures.tsx" entrypoint under "__tests__/" or "stories/".',
17
+ unexpectedExportList:
18
+ "Inline the exported declaration in this nested fixture entrypoint. Do not use export lists or re-exports here.",
19
+ unexpectedExportPattern:
20
+ 'Bind the exported const to a direct identifier, for example "export const fixture_user = ...". Do not export destructuring patterns from a nested fixture entrypoint.',
21
+ unexpectedVariableKind:
22
+ 'Change this exported "{{ kind }}" declaration to "const". Nested fixture entrypoints allow only exported const declarations.',
23
+ },
24
+ },
25
+ create(context) {
26
+ if (!isFixturesFile(context.filename)) {
27
+ return {};
28
+ }
29
+
30
+ return {
31
+ ExportAllDeclaration(node) {
32
+ context.report({
33
+ node,
34
+ messageId: "unexpectedExportList",
35
+ });
36
+ },
37
+ ExportDefaultDeclaration(node) {
38
+ context.report({
39
+ node,
40
+ messageId: "unexpectedDefaultExport",
41
+ });
42
+ },
43
+ ExportNamedDeclaration(node) {
44
+ if (node.source || !node.declaration) {
45
+ context.report({
46
+ node,
47
+ messageId: "unexpectedExportList",
48
+ });
49
+ return;
50
+ }
51
+
52
+ const declaration = node.declaration;
53
+ if (declaration.type === "VariableDeclaration") {
54
+ if (declaration.kind !== "const") {
55
+ context.report({
56
+ node: declaration,
57
+ messageId: "unexpectedVariableKind",
58
+ data: {
59
+ kind: declaration.kind,
60
+ },
61
+ });
62
+ return;
63
+ }
64
+
65
+ declaration.declarations.forEach((declarator) => {
66
+ if (declarator.id.type === "Identifier") {
67
+ return;
68
+ }
69
+
70
+ context.report({
71
+ node: declarator.id,
72
+ messageId: "unexpectedExportPattern",
73
+ });
74
+ });
75
+ return;
76
+ }
77
+
78
+ if (declaration.type === "FunctionDeclaration" && declaration.id) {
79
+ return;
80
+ }
81
+
82
+ context.report({
83
+ node: declaration,
84
+ messageId: "unexpectedExportDeclaration",
85
+ });
86
+ },
87
+ };
88
+ },
89
+ };
90
+
91
+ export default fixtureFileContractRule;
@@ -0,0 +1,125 @@
1
+ import type { AstImportClause, RuleModule } from "./types.ts";
2
+ import { isAllowedFixturesImportPath, isFixtureConsumerFile, isFixtureLikeName } from "./helpers.ts";
3
+
4
+ type ImportSpecifierNames = {
5
+ importedName: string | null;
6
+ isNamedImport: boolean;
7
+ localName: string;
8
+ };
9
+
10
+ function readImportSpecifierNames(specifier: AstImportClause): ImportSpecifierNames {
11
+ if (specifier.type === "ImportSpecifier") {
12
+ return {
13
+ importedName:
14
+ specifier.imported.type === "Identifier" ? specifier.imported.name : String(specifier.imported.value),
15
+ localName: specifier.local.type === "Identifier" ? specifier.local.name : "",
16
+ isNamedImport: true,
17
+ };
18
+ }
19
+
20
+ return {
21
+ importedName: null,
22
+ localName: specifier.local.type === "Identifier" ? specifier.local.name : "",
23
+ isNamedImport: false,
24
+ };
25
+ }
26
+
27
+ const fixtureImportPathConventionRule: RuleModule = {
28
+ meta: {
29
+ type: "problem" as const,
30
+ docs: {
31
+ description:
32
+ 'Require test and story files to import fixture_ and factory_ bindings only as named imports from a relative "fixtures" module inside the same "__tests__/" or "stories/" tree',
33
+ },
34
+ schema: [],
35
+ messages: {
36
+ invalidFixturesImportAlias:
37
+ 'Import "{{ name }}" from a relative "fixtures" module without renaming it. The local binding must stay "{{ name }}".',
38
+ invalidFixturesImportName:
39
+ 'Only named imports that start with "fixture_" or "factory_" are allowed from a relative "fixtures" module. Remove or rename "{{ name }}".',
40
+ invalidFixturesImportPath:
41
+ 'Change this import so "{{ name }}" comes from a relative "fixtures" module inside the same "__tests__/" or "stories/" tree.',
42
+ invalidFixturesImportStyle:
43
+ 'Rewrite this as a named import from a relative "fixtures" module, for example: import { fixture_name } from "./fixtures".',
44
+ },
45
+ },
46
+ create(context) {
47
+ if (!isFixtureConsumerFile(context.filename)) {
48
+ return {};
49
+ }
50
+
51
+ return {
52
+ ImportDeclaration(node) {
53
+ const importPath = typeof node.source.value === "string" ? node.source.value : "";
54
+
55
+ if (isAllowedFixturesImportPath(importPath, context.filename)) {
56
+ if (node.specifiers.length === 0) {
57
+ context.report({
58
+ node,
59
+ messageId: "invalidFixturesImportStyle",
60
+ });
61
+ return;
62
+ }
63
+
64
+ node.specifiers.forEach((specifier) => {
65
+ const { importedName, localName, isNamedImport } = readImportSpecifierNames(specifier);
66
+
67
+ if (!isNamedImport) {
68
+ context.report({
69
+ node: specifier,
70
+ messageId: "invalidFixturesImportStyle",
71
+ });
72
+ return;
73
+ }
74
+
75
+ if (!importedName || !isFixtureLikeName(importedName)) {
76
+ context.report({
77
+ node: specifier,
78
+ messageId: "invalidFixturesImportName",
79
+ data: {
80
+ name: importedName ?? localName,
81
+ },
82
+ });
83
+ return;
84
+ }
85
+
86
+ if (localName !== importedName) {
87
+ context.report({
88
+ node: specifier,
89
+ messageId: "invalidFixturesImportAlias",
90
+ data: {
91
+ name: importedName,
92
+ },
93
+ });
94
+ }
95
+ });
96
+
97
+ return;
98
+ }
99
+
100
+ node.specifiers.forEach((specifier) => {
101
+ const { importedName, localName, isNamedImport } = readImportSpecifierNames(specifier);
102
+ const fixtureLikeName = isNamedImport
103
+ ? [importedName, localName].find((name) => typeof name === "string" && isFixtureLikeName(name))
104
+ : isFixtureLikeName(localName)
105
+ ? localName
106
+ : null;
107
+
108
+ if (!fixtureLikeName) {
109
+ return;
110
+ }
111
+
112
+ context.report({
113
+ node: specifier,
114
+ messageId: "invalidFixturesImportPath",
115
+ data: {
116
+ name: fixtureLikeName,
117
+ },
118
+ });
119
+ });
120
+ },
121
+ };
122
+ },
123
+ };
124
+
125
+ export default fixtureImportPathConventionRule;