@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,179 @@
1
+ import type {
2
+ AstDeclarationWithIdentifiers,
3
+ AstExportNamedDeclaration,
4
+ AstExportSpecifier,
5
+ AstNode,
6
+ AstProgram,
7
+ AstProgramStatement,
8
+ RuleModule,
9
+ } from "./types.ts";
10
+ import { isExemptSupportBasename, isTypeDeclaration, readPatternIdentifierNames } from "./helpers.ts";
11
+
12
+ type HookRuntimeExportEntry = {
13
+ kind:
14
+ | "class-declaration"
15
+ | "const-variable"
16
+ | "default-export"
17
+ | "enum-declaration"
18
+ | "export-all"
19
+ | "function-declaration"
20
+ | "indirect-export"
21
+ | "variable-declaration";
22
+ name?: string;
23
+ node: AstNode;
24
+ };
25
+
26
+ function readExportedSpecifierName(specifier: AstExportSpecifier): string {
27
+ if (specifier.exported.type === "Identifier") {
28
+ return specifier.exported.name;
29
+ }
30
+
31
+ return String(specifier.exported.value);
32
+ }
33
+
34
+ function isTypeOnlyExportSpecifier(
35
+ specifier: AstExportSpecifier,
36
+ exportDeclaration: AstExportNamedDeclaration,
37
+ ): boolean {
38
+ return exportDeclaration.exportKind === "type" || specifier.exportKind === "type";
39
+ }
40
+
41
+ function isTypeOnlyExportNamedDeclaration(node: AstExportNamedDeclaration): boolean {
42
+ if (node.declaration) {
43
+ return node.exportKind === "type" || isTypeDeclaration(node.declaration);
44
+ }
45
+
46
+ return node.exportKind === "type" || node.specifiers.every((specifier) => isTypeOnlyExportSpecifier(specifier, node));
47
+ }
48
+
49
+ function readRuntimeExportEntries(program: AstProgram): HookRuntimeExportEntry[] {
50
+ return program.body.flatMap((statement) => readStatementRuntimeExportEntries(statement));
51
+ }
52
+
53
+ function readStatementRuntimeExportEntries(statement: AstProgramStatement): HookRuntimeExportEntry[] {
54
+ if (statement.type === "ExportDefaultDeclaration") {
55
+ return [{ kind: "default-export", node: statement }];
56
+ }
57
+
58
+ if (statement.type === "TSExportAssignment") {
59
+ return [{ kind: "default-export", node: statement }];
60
+ }
61
+
62
+ if (statement.type === "ExportAllDeclaration") {
63
+ if (statement.exportKind === "type") {
64
+ return [];
65
+ }
66
+
67
+ return [{ kind: "export-all", node: statement }];
68
+ }
69
+
70
+ if (statement.type !== "ExportNamedDeclaration") {
71
+ return [];
72
+ }
73
+
74
+ if (isTypeOnlyExportNamedDeclaration(statement)) {
75
+ return [];
76
+ }
77
+
78
+ if (!statement.declaration) {
79
+ return statement.specifiers
80
+ .filter((specifier) => !isTypeOnlyExportSpecifier(specifier, statement))
81
+ .map((specifier) => ({
82
+ kind: "indirect-export" as const,
83
+ name: readExportedSpecifierName(specifier),
84
+ node: specifier,
85
+ }));
86
+ }
87
+
88
+ return readDeclarationRuntimeExportEntries(statement.declaration);
89
+ }
90
+
91
+ function readDeclarationRuntimeExportEntries(declaration: AstDeclarationWithIdentifiers): HookRuntimeExportEntry[] {
92
+ if (declaration.type === "FunctionDeclaration") {
93
+ return declaration.id ? [{ kind: "function-declaration", name: declaration.id.name, node: declaration }] : [];
94
+ }
95
+
96
+ if (declaration.type === "VariableDeclaration") {
97
+ return declaration.declarations.flatMap((declarator) => {
98
+ const declarationNames = readPatternIdentifierNames(declarator.id);
99
+
100
+ return declarationNames.map((name) => ({
101
+ kind: declaration.kind === "const" ? ("const-variable" as const) : ("variable-declaration" as const),
102
+ name,
103
+ node: declarator,
104
+ }));
105
+ });
106
+ }
107
+
108
+ if (declaration.type === "TSEnumDeclaration") {
109
+ return declaration.id ? [{ kind: "enum-declaration", name: declaration.id.name, node: declaration }] : [];
110
+ }
111
+
112
+ if (declaration.type === "ClassDeclaration") {
113
+ return declaration.id ? [{ kind: "class-declaration", name: declaration.id.name, node: declaration }] : [];
114
+ }
115
+
116
+ return [];
117
+ }
118
+
119
+ function isValidMainHookRuntimeExport(entry: HookRuntimeExportEntry): boolean {
120
+ return entry.kind === "function-declaration" && entry.name?.startsWith("use") === true;
121
+ }
122
+
123
+ const hookFileContractRule: RuleModule = {
124
+ meta: {
125
+ type: "problem" as const,
126
+ docs: {
127
+ description:
128
+ "Require hook ownership files to export exactly one direct named hook function and allow only type-only secondary exports",
129
+ },
130
+ schema: [],
131
+ messages: {
132
+ missingMainHookExport:
133
+ "Export exactly one main runtime hook from this file. Hook ownership files must use one direct named `export function useThing()` export plus optional type-only exports.",
134
+ invalidMainHookExport:
135
+ "Replace this export with a plain named hook function declaration. Hook ownership files must use `export function useThing() {}` and must not wrap or const-bind the main hook export.",
136
+ unexpectedAdditionalRuntimeExport:
137
+ "Remove this additional runtime export. Hook ownership files may export only one main runtime hook plus unrestricted type-only API.",
138
+ },
139
+ },
140
+ create(context) {
141
+ if (isExemptSupportBasename(context.filename)) {
142
+ return {};
143
+ }
144
+
145
+ return {
146
+ Program(node) {
147
+ const runtimeExportEntries = readRuntimeExportEntries(node);
148
+ if (runtimeExportEntries.length === 0) {
149
+ context.report({
150
+ node,
151
+ messageId: "missingMainHookExport",
152
+ });
153
+ return;
154
+ }
155
+
156
+ const [mainRuntimeExportEntry, ...additionalRuntimeExportEntries] = runtimeExportEntries;
157
+ if (!mainRuntimeExportEntry) {
158
+ return;
159
+ }
160
+
161
+ if (!isValidMainHookRuntimeExport(mainRuntimeExportEntry)) {
162
+ context.report({
163
+ node: mainRuntimeExportEntry.node,
164
+ messageId: "invalidMainHookExport",
165
+ });
166
+ }
167
+
168
+ additionalRuntimeExportEntries.forEach((runtimeExportEntry) => {
169
+ context.report({
170
+ node: runtimeExportEntry.node,
171
+ messageId: "unexpectedAdditionalRuntimeExport",
172
+ });
173
+ });
174
+ },
175
+ };
176
+ },
177
+ };
178
+
179
+ export default hookFileContractRule;
@@ -0,0 +1,151 @@
1
+ import type {
2
+ AstExportNamedDeclaration,
3
+ AstExportSpecifier,
4
+ AstProgram,
5
+ AstProgramStatement,
6
+ RuleModule,
7
+ } from "./types.ts";
8
+ import { getFilenameWithoutExtension, isExemptSupportBasename, readDeclarationIdentifierNames } from "./helpers.ts";
9
+
10
+ function isTypeOnlyExportSpecifier(
11
+ specifier: AstExportSpecifier,
12
+ exportDeclaration: AstExportNamedDeclaration,
13
+ ): boolean {
14
+ return exportDeclaration.exportKind === "type" || specifier.exportKind === "type";
15
+ }
16
+
17
+ function readExportedSpecifierName(specifier: AstExportSpecifier): string {
18
+ if (specifier.exported.type === "Identifier") {
19
+ return specifier.exported.name;
20
+ }
21
+
22
+ return String(specifier.exported.value);
23
+ }
24
+
25
+ function readFirstRuntimeExportName(program: AstProgram): string | null {
26
+ for (const statement of program.body) {
27
+ const exportName = readStatementRuntimeExportName(statement);
28
+ if (exportName !== null) {
29
+ return exportName;
30
+ }
31
+ }
32
+
33
+ return null;
34
+ }
35
+
36
+ function readStatementRuntimeExportName(statement: AstProgramStatement): string | null {
37
+ if (statement.type !== "ExportNamedDeclaration") {
38
+ return null;
39
+ }
40
+
41
+ if (statement.declaration) {
42
+ if (
43
+ statement.exportKind === "type" ||
44
+ statement.declaration.type === "TSTypeAliasDeclaration" ||
45
+ statement.declaration.type === "TSInterfaceDeclaration" ||
46
+ statement.declaration.type === "TSModuleDeclaration"
47
+ ) {
48
+ return null;
49
+ }
50
+
51
+ if (statement.declaration.type === "VariableDeclaration") {
52
+ const firstDeclarator = statement.declaration.declarations[0];
53
+ return firstDeclarator?.id.type === "Identifier" ? firstDeclarator.id.name : null;
54
+ }
55
+
56
+ return readDeclarationIdentifierNames(statement.declaration)[0] ?? null;
57
+ }
58
+
59
+ const runtimeSpecifier = statement.specifiers.find((specifier) => !isTypeOnlyExportSpecifier(specifier, statement));
60
+ if (!runtimeSpecifier) {
61
+ return null;
62
+ }
63
+
64
+ return readExportedSpecifierName(runtimeSpecifier);
65
+ }
66
+
67
+ function readExpectedHookNameFromFilename(filename: string): string | null {
68
+ const fileStem = getFilenameWithoutExtension(filename);
69
+ if (/^use[A-Z][A-Za-z0-9]*$/u.test(fileStem)) {
70
+ return fileStem;
71
+ }
72
+
73
+ if (!/^use(?:-[a-z0-9]+)+$/u.test(fileStem)) {
74
+ return null;
75
+ }
76
+
77
+ const [, ...segments] = fileStem.split("-");
78
+
79
+ return `use${segments.map((segment) => `${segment[0]?.toUpperCase() ?? ""}${segment.slice(1)}`).join("")}`;
80
+ }
81
+
82
+ function readKebabCaseHookFilename(hookName: string): string {
83
+ return `${hookName.replaceAll(/([a-z0-9])([A-Z])/gu, "$1-$2").toLowerCase()}.ts`;
84
+ }
85
+
86
+ const hookFileNamingConventionRule: RuleModule = {
87
+ meta: {
88
+ type: "problem" as const,
89
+ docs: {
90
+ description:
91
+ "Require hook ownership filenames to match their exported hook name in either camelCase or kebab-case `use*` form",
92
+ },
93
+ schema: [],
94
+ messages: {
95
+ invalidHookFileName:
96
+ 'Rename this hook file to either "useFoo.ts[x]" or "use-foo.ts[x]" so its basename maps deterministically to the exported hook name.',
97
+ invalidHookExportName:
98
+ 'Rename the exported hook to lower camelCase starting with "use". Hook ownership files must export names like "useFoo".',
99
+ mismatchedHookFileName:
100
+ 'Rename this file or the exported hook so they match exactly. "{{ exportedName }}" must live in either "{{ camelFilename }}" or "{{ kebabFilename }}".',
101
+ },
102
+ },
103
+ create(context) {
104
+ if (isExemptSupportBasename(context.filename)) {
105
+ return {};
106
+ }
107
+
108
+ return {
109
+ Program(node) {
110
+ const exportedHookName = readFirstRuntimeExportName(node);
111
+ if (!exportedHookName) {
112
+ return;
113
+ }
114
+
115
+ const expectedHookName = readExpectedHookNameFromFilename(context.filename);
116
+ if (!expectedHookName) {
117
+ context.report({
118
+ node,
119
+ messageId: "invalidHookFileName",
120
+ });
121
+ return;
122
+ }
123
+
124
+ if (!/^use[A-Z][A-Za-z0-9]*$/u.test(exportedHookName)) {
125
+ context.report({
126
+ node,
127
+ messageId: "invalidHookExportName",
128
+ });
129
+ }
130
+
131
+ if (exportedHookName === expectedHookName) {
132
+ return;
133
+ }
134
+
135
+ const extension = context.filename.endsWith(".tsx") ? ".tsx" : ".ts";
136
+
137
+ context.report({
138
+ node,
139
+ messageId: "mismatchedHookFileName",
140
+ data: {
141
+ exportedName: exportedHookName,
142
+ camelFilename: `${exportedHookName}${extension}`,
143
+ kebabFilename: readKebabCaseHookFilename(exportedHookName).replace(/\.ts$/u, extension),
144
+ },
145
+ });
146
+ },
147
+ };
148
+ },
149
+ };
150
+
151
+ export default hookFileNamingConventionRule;
@@ -0,0 +1,60 @@
1
+ import type { AstProgram, RuleModule } from "./types.ts";
2
+ import { dirname, join } from "node:path";
3
+ import {
4
+ findDescendantFilePath,
5
+ getBaseName,
6
+ getFilenameWithoutExtension,
7
+ isExemptSupportBasename,
8
+ } from "./helpers.ts";
9
+
10
+ function readRequiredTestsDirectoryPath(filename: string): string {
11
+ return join(dirname(filename), "__tests__");
12
+ }
13
+
14
+ function readRequiredHookTestFileName(filename: string): string {
15
+ const sourceBaseName = getFilenameWithoutExtension(filename);
16
+ const testExtension = getBaseName(filename).endsWith(".tsx") ? ".test.tsx" : ".test.ts";
17
+
18
+ return `${sourceBaseName}${testExtension}`;
19
+ }
20
+
21
+ const hookTestFileConventionRule: RuleModule = {
22
+ meta: {
23
+ type: "problem" as const,
24
+ docs: {
25
+ description:
26
+ 'Require every hook ownership file to have a matching "basename.test.ts" or ".test.tsx" file somewhere under a sibling "__tests__/" directory',
27
+ },
28
+ schema: [],
29
+ messages: {
30
+ missingHookTestFile:
31
+ 'Add a test file named "{{ requiredTestFileName }}" somewhere under "{{ requiredTestsDirectoryPath }}". Hook ownership files must keep their tests under a sibling "__tests__/" directory.',
32
+ },
33
+ },
34
+ create(context) {
35
+ if (isExemptSupportBasename(context.filename)) {
36
+ return {};
37
+ }
38
+
39
+ return {
40
+ Program(node: AstProgram) {
41
+ const requiredTestsDirectoryPath = readRequiredTestsDirectoryPath(context.filename);
42
+ const requiredTestFileName = readRequiredHookTestFileName(context.filename);
43
+ if (findDescendantFilePath(requiredTestsDirectoryPath, requiredTestFileName)) {
44
+ return;
45
+ }
46
+
47
+ context.report({
48
+ node,
49
+ messageId: "missingHookTestFile",
50
+ data: {
51
+ requiredTestFileName,
52
+ requiredTestsDirectoryPath,
53
+ },
54
+ });
55
+ },
56
+ };
57
+ },
58
+ };
59
+
60
+ export default hookTestFileConventionRule;
@@ -0,0 +1,75 @@
1
+ import type { RuleModule } from "./types.ts";
2
+ import {
3
+ getExtension,
4
+ getFilenameWithoutExtension,
5
+ isStrictAreaAllowedSupportFile,
6
+ readPathFromDirectory,
7
+ } from "./helpers.ts";
8
+
9
+ function isAllowedHookOwnershipBasename(filename: string): boolean {
10
+ const extension = getExtension(filename);
11
+ if (extension !== ".ts" && extension !== ".tsx") {
12
+ return false;
13
+ }
14
+
15
+ return getFilenameWithoutExtension(filename).startsWith("use");
16
+ }
17
+
18
+ function isAllowedHooksDirectoryRelativePath(relativePath: string, filename: string): boolean {
19
+ if (!relativePath) {
20
+ return false;
21
+ }
22
+
23
+ if (relativePath.startsWith("__tests__/")) {
24
+ return true;
25
+ }
26
+
27
+ if (relativePath.includes("/")) {
28
+ return false;
29
+ }
30
+
31
+ if (isStrictAreaAllowedSupportFile(filename)) {
32
+ return true;
33
+ }
34
+
35
+ return isAllowedHookOwnershipBasename(filename);
36
+ }
37
+
38
+ const hooksDirectoryFileConventionRule: RuleModule = {
39
+ meta: {
40
+ type: "problem" as const,
41
+ docs: {
42
+ description:
43
+ 'Restrict "hooks" directories to direct-child hook ownership files, exempt support basenames, and sibling "__tests__" trees',
44
+ },
45
+ schema: [],
46
+ messages: {
47
+ invalidHooksDirectoryFile:
48
+ 'Move or rename "{{ relativePath }}". A "hooks/" directory may contain only direct-child "use*.ts" or "use*.tsx" ownership files, direct-child "index.ts" or "types.ts" files, or a direct-child "__tests__/" tree.',
49
+ },
50
+ },
51
+ create(context) {
52
+ return {
53
+ Program(node) {
54
+ const relativePath = readPathFromDirectory(context.filename, "hooks");
55
+ if (relativePath === null) {
56
+ return;
57
+ }
58
+
59
+ if (isAllowedHooksDirectoryRelativePath(relativePath, context.filename)) {
60
+ return;
61
+ }
62
+
63
+ context.report({
64
+ node,
65
+ messageId: "invalidHooksDirectoryFile",
66
+ data: {
67
+ relativePath: relativePath || ".",
68
+ },
69
+ });
70
+ },
71
+ };
72
+ },
73
+ };
74
+
75
+ export default hooksDirectoryFileConventionRule;
@@ -0,0 +1,177 @@
1
+ import type { TSESTree } from "@typescript-eslint/types";
2
+ import type { AstProgramStatement, RuleModule } from "./types.ts";
3
+ import { getBaseName } from "./helpers.ts";
4
+
5
+ type IndexMessageId = "unexpectedIndexExport" | "unexpectedIndexStatement";
6
+
7
+ function isIndexModuleFile(filename: string): boolean {
8
+ const baseName = getBaseName(filename);
9
+
10
+ return baseName === "index.ts" || baseName === "index.tsx";
11
+ }
12
+
13
+ function isIndexTsxFile(filename: string): boolean {
14
+ return getBaseName(filename) === "index.tsx";
15
+ }
16
+
17
+ function isAllowedIndexReExport(node: AstProgramStatement): boolean {
18
+ return (
19
+ node.type === "ExportAllDeclaration" ||
20
+ (node.type === "ExportNamedDeclaration" && node.source !== null && node.declaration === null)
21
+ );
22
+ }
23
+
24
+ function readIndexBarrelBaseName(filename: string): string {
25
+ return getBaseName(filename);
26
+ }
27
+
28
+ function readIndexRenameSuffix(filename: string): string {
29
+ return isIndexTsxFile(filename) ? ' Then rename this file to "index.ts".' : "";
30
+ }
31
+
32
+ function readUnexpectedMessageId(node: AstProgramStatement): IndexMessageId {
33
+ if (
34
+ node.type === "ExportNamedDeclaration" ||
35
+ node.type === "ExportDefaultDeclaration" ||
36
+ node.type === "TSExportAssignment"
37
+ ) {
38
+ return "unexpectedIndexExport";
39
+ }
40
+
41
+ return "unexpectedIndexStatement";
42
+ }
43
+
44
+ function readVariableDeclarationReportNode(node: TSESTree.VariableDeclaration): TSESTree.Node {
45
+ const firstDeclarator = node.declarations[0];
46
+ return firstDeclarator?.id.type === "Identifier" ? firstDeclarator.id : node;
47
+ }
48
+
49
+ function readDeclarationStatementReportNode(statement: AstProgramStatement): TSESTree.Node | null {
50
+ if (
51
+ statement.type === "FunctionDeclaration" ||
52
+ statement.type === "ClassDeclaration" ||
53
+ statement.type === "TSEnumDeclaration" ||
54
+ statement.type === "TSInterfaceDeclaration" ||
55
+ statement.type === "TSTypeAliasDeclaration"
56
+ ) {
57
+ return statement.id ?? statement;
58
+ }
59
+
60
+ if (statement.type === "VariableDeclaration") {
61
+ return readVariableDeclarationReportNode(statement);
62
+ }
63
+
64
+ return null;
65
+ }
66
+
67
+ function readExportDefaultReportNode(node: TSESTree.ExportDefaultDeclaration): TSESTree.Node {
68
+ if (node.declaration.type === "Identifier") {
69
+ return node.declaration;
70
+ }
71
+
72
+ if (
73
+ node.declaration.type === "FunctionDeclaration" ||
74
+ node.declaration.type === "ClassDeclaration" ||
75
+ node.declaration.type === "TSEnumDeclaration" ||
76
+ node.declaration.type === "TSInterfaceDeclaration" ||
77
+ node.declaration.type === "TSTypeAliasDeclaration"
78
+ ) {
79
+ return node.declaration.id ?? node.declaration;
80
+ }
81
+
82
+ if (node.declaration.type === "VariableDeclaration") {
83
+ return readVariableDeclarationReportNode(node.declaration);
84
+ }
85
+
86
+ return node;
87
+ }
88
+
89
+ function readProgramReportNode(node: TSESTree.Program): TSESTree.Node {
90
+ return node.body[0] ?? node;
91
+ }
92
+
93
+ function readIndexViolationReportNode(statement: AstProgramStatement): TSESTree.Node {
94
+ if (statement.type === "ExportNamedDeclaration") {
95
+ if (statement.declaration) {
96
+ if (
97
+ statement.declaration.type === "FunctionDeclaration" ||
98
+ statement.declaration.type === "ClassDeclaration" ||
99
+ statement.declaration.type === "TSEnumDeclaration" ||
100
+ statement.declaration.type === "TSInterfaceDeclaration" ||
101
+ statement.declaration.type === "TSTypeAliasDeclaration"
102
+ ) {
103
+ return statement.declaration.id ?? statement.declaration;
104
+ }
105
+
106
+ if (statement.declaration.type === "VariableDeclaration") {
107
+ return readVariableDeclarationReportNode(statement.declaration);
108
+ }
109
+ }
110
+
111
+ const firstSpecifier = statement.specifiers[0];
112
+ if (!firstSpecifier) {
113
+ return statement;
114
+ }
115
+
116
+ return firstSpecifier.exported.type === "Identifier" ? firstSpecifier.exported : firstSpecifier;
117
+ }
118
+
119
+ if (statement.type === "ExportDefaultDeclaration") {
120
+ return readExportDefaultReportNode(statement);
121
+ }
122
+
123
+ return readDeclarationStatementReportNode(statement) ?? statement;
124
+ }
125
+
126
+ const indexFileContractRule: RuleModule = {
127
+ meta: {
128
+ type: "problem" as const,
129
+ docs: {
130
+ description: 'Require "index.ts" files to contain re-exports only and forbid "index.tsx" barrels',
131
+ },
132
+ schema: [],
133
+ messages: {
134
+ unexpectedIndexTsxFilename:
135
+ 'Rename this file to "index.ts". Index barrel files must not use the ".tsx" extension.',
136
+ unexpectedIndexExport:
137
+ 'Remove this local export from the index barrel. "{{ barrelBaseName }}" must not define symbols; move the definition into another module and re-export it from here instead{{ renameSuffix }}',
138
+ unexpectedIndexStatement:
139
+ 'Remove this statement from the index barrel. "{{ barrelBaseName }}" may contain re-export statements only{{ renameSuffix }}',
140
+ },
141
+ },
142
+ create(context) {
143
+ if (!isIndexModuleFile(context.filename)) {
144
+ return {};
145
+ }
146
+
147
+ return {
148
+ Program(node) {
149
+ if (isIndexTsxFile(context.filename)) {
150
+ context.report({
151
+ node: readProgramReportNode(node),
152
+ messageId: "unexpectedIndexTsxFilename",
153
+ });
154
+ }
155
+
156
+ const reportData = {
157
+ barrelBaseName: readIndexBarrelBaseName(context.filename),
158
+ renameSuffix: readIndexRenameSuffix(context.filename),
159
+ };
160
+
161
+ node.body.forEach((statement) => {
162
+ if (isAllowedIndexReExport(statement)) {
163
+ return;
164
+ }
165
+
166
+ context.report({
167
+ node: readIndexViolationReportNode(statement),
168
+ messageId: readUnexpectedMessageId(statement),
169
+ data: reportData,
170
+ });
171
+ });
172
+ },
173
+ };
174
+ },
175
+ };
176
+
177
+ export default indexFileContractRule;