@alexgorbatchev/typescript-ai-policy 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,6 +10,9 @@ as direct repair instructions so an agent can make the required change instead o
10
10
 
11
11
  The shared Oxlint policy is implemented in TypeScript-authored custom rule modules under `src/oxlint/`.
12
12
 
13
+ File-level diagnostics are anchored to the first top-level syntax node when possible so editors highlight a concrete
14
+ repair location instead of painting the entire file.
15
+
13
16
  Upstream rules stay enabled as baseline correctness guardrails around that stricter policy layer.
14
17
 
15
18
  These rules are designed to work as a **full policy set**, not as a grab bag of independent preferences. Disabling one
@@ -147,7 +150,9 @@ config object, and that object is deep-merged **before** the shared defaults so
147
150
  conflicting keys.
148
151
 
149
152
  For Oxlint specifically, consumer configs are extension-only: if the callback tries to redefine any shared rule name,
150
- the factory throws with guidance to change the shared package instead of overriding that rule downstream.
153
+ the factory throws with guidance to change the shared package instead of overriding that rule downstream. The shared
154
+ Oxlint config also force-disables `import/no-default-export` for `oxlint.config.ts` and `oxfmt.config.ts`, because
155
+ Oxlint and Oxfmt document those TypeScript config entrypoints as default-exported modules.
151
156
 
152
157
  When you run Oxlint manually, use Bun to launch the CLI:
153
158
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexgorbatchev/typescript-ai-policy",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Shared TypeScript AI policy configs and Oxlint rules",
5
5
  "homepage": "https://github.com/alexgorbatchev/typescript-ai-policy#readme",
6
6
  "repository": {
@@ -82,6 +82,12 @@ const DEFAULT_OXLINT_CONFIG = defineConfig({
82
82
  "@alexgorbatchev/hook-test-file-convention": "error",
83
83
  },
84
84
  },
85
+ {
86
+ files: ["**/oxlint.config.ts", "**/oxfmt.config.ts"],
87
+ rules: {
88
+ "import/no-default-export": "off",
89
+ },
90
+ },
85
91
  {
86
92
  files: ["**/index.{ts,tsx}"],
87
93
  rules: {
@@ -1,5 +1,11 @@
1
1
  import type { RuleModule } from "./types.ts";
2
- import { getBaseName, getExtension, isExemptSupportBasename, readPathFromFirstMatchingDirectory } from "./helpers.ts";
2
+ import {
3
+ getBaseName,
4
+ getExtension,
5
+ isExemptSupportBasename,
6
+ readPathFromFirstMatchingDirectory,
7
+ readProgramReportNode,
8
+ } from "./helpers.ts";
3
9
 
4
10
  const COMPONENT_DIRECTORY_NAMES = new Set(["components", "templates", "layouts"]);
5
11
  const COMPONENT_ALLOWED_SUPPORT_FILES = new Set(["constants.ts", "index.ts", "types.ts"]);
@@ -50,7 +56,7 @@ const componentDirectoryFileConventionRule: RuleModule = {
50
56
  }
51
57
 
52
58
  context.report({
53
- node,
59
+ node: readProgramReportNode(node),
54
60
  messageId: "invalidComponentDirectoryFile",
55
61
  data: {
56
62
  directoryName: componentDirectoryMatch.directoryName,
@@ -15,6 +15,7 @@ import {
15
15
  isInTestsDirectory,
16
16
  isTypeDeclaration,
17
17
  readDeclarationIdentifierNames,
18
+ readProgramReportNode,
18
19
  readMultipartComponentRootName,
19
20
  readPatternIdentifierNames,
20
21
  unwrapExpression,
@@ -286,7 +287,7 @@ const componentFileContractRule: RuleModule = {
286
287
  const runtimeExportEntries = readRuntimeExportEntries(node);
287
288
  if (runtimeExportEntries.length === 0) {
288
289
  context.report({
289
- node,
290
+ node: readProgramReportNode(node),
290
291
  messageId: "missingMainComponentExport",
291
292
  });
292
293
  return;
@@ -1,5 +1,5 @@
1
1
  import type { RuleModule } from "./types.ts";
2
- import { getExtension, hasPathSegment, isInTestsDirectory } from "./helpers.ts";
2
+ import { getExtension, hasPathSegment, isInTestsDirectory, readProgramReportNode } from "./helpers.ts";
3
3
 
4
4
  const COMPONENT_DIRECTORY_NAMES = new Set(["components", "templates", "layouts"]);
5
5
 
@@ -32,7 +32,7 @@ const componentFileLocationConventionRule: RuleModule = {
32
32
  }
33
33
 
34
34
  context.report({
35
- node,
35
+ node: readProgramReportNode(node),
36
36
  messageId: "unexpectedComponentFileLocation",
37
37
  });
38
38
  },
@@ -15,6 +15,7 @@ import {
15
15
  isPascalCase,
16
16
  readDeclarationIdentifierNames,
17
17
  readMultipartComponentRootName,
18
+ readProgramReportNode,
18
19
  } from "./helpers.ts";
19
20
 
20
21
  function isTypeOnlyExportSpecifier(
@@ -187,10 +188,6 @@ function readExpectedComponentNameFromFilename(filename: string): string | null
187
188
  .join("");
188
189
  }
189
190
 
190
- function readInvalidFilenameReportNode(program: AstProgram): TSESTree.Node {
191
- return program.body[0] ?? program;
192
- }
193
-
194
191
  const componentFileNamingConventionRule: RuleModule = {
195
192
  meta: {
196
193
  type: "problem" as const,
@@ -228,7 +225,7 @@ const componentFileNamingConventionRule: RuleModule = {
228
225
  const expectedComponentName = readExpectedComponentNameFromFilename(context.filename);
229
226
  if (!expectedComponentName) {
230
227
  context.report({
231
- node: readInvalidFilenameReportNode(node),
228
+ node: readProgramReportNode(node),
232
229
  messageId: "invalidComponentFileName",
233
230
  });
234
231
  return;
@@ -1,5 +1,4 @@
1
- import type { TSESTree } from "@typescript-eslint/types";
2
- import type { AstProgram, AstProgramStatement, RuleModule } from "./types.ts";
1
+ import type { AstNode, AstProgram, RuleModule } from "./types.ts";
3
2
  import { dirname, join } from "node:path";
4
3
  import {
5
4
  findDescendantFilePath,
@@ -8,6 +7,7 @@ import {
8
7
  isInStoriesDirectory,
9
8
  isInTestsDirectory,
10
9
  readAbbreviatedSiblingDirectoryPath,
10
+ readProgramReportNode,
11
11
  } from "./helpers.ts";
12
12
 
13
13
  function readRequiredStoriesDirectoryPath(filename: string): string {
@@ -20,9 +20,7 @@ function readRequiredStoryFileName(filename: string): string {
20
20
  return `${sourceBaseName}.stories.tsx`;
21
21
  }
22
22
 
23
- type ReportNode = AstProgramStatement | TSESTree.Node;
24
-
25
- function readReportNode(program: AstProgram): ReportNode {
23
+ function readReportNode(program: AstProgram): AstNode {
26
24
  for (const statement of program.body) {
27
25
  if (statement.type === "ExportNamedDeclaration") {
28
26
  if (statement.exportKind === "type") {
@@ -60,7 +58,7 @@ function readReportNode(program: AstProgram): ReportNode {
60
58
  }
61
59
  }
62
60
 
63
- return program.body[0] ?? program;
61
+ return readProgramReportNode(program);
64
62
  }
65
63
 
66
64
  const componentStoryFileConventionRule: RuleModule = {
@@ -7,6 +7,7 @@ import type {
7
7
  AstDestructuringPattern,
8
8
  AstFunctionLike,
9
9
  AstNode,
10
+ AstProgram,
10
11
  AstTypeDeclaration,
11
12
  } from "./types.ts";
12
13
 
@@ -84,6 +85,10 @@ export function readAbbreviatedSiblingDirectoryPath(filename: string, siblingDir
84
85
  return readAbbreviatedPath(`${dirname(filename)}/${siblingDirectoryName}`);
85
86
  }
86
87
 
88
+ export function readProgramReportNode(program: AstProgram): AstNode {
89
+ return program.body[0] ?? program;
90
+ }
91
+
87
92
  export function readPathFromFirstMatchingDirectory(
88
93
  filename: string,
89
94
  expectedDirectoryNames: DirectoryNames,
@@ -7,7 +7,14 @@ import type {
7
7
  AstProgramStatement,
8
8
  RuleModule,
9
9
  } from "./types.ts";
10
- import { isExemptSupportBasename, isTypeDeclaration, readPatternIdentifierNames } from "./helpers.ts";
10
+ import {
11
+ isExemptSupportBasename,
12
+ isInStoriesDirectory,
13
+ isInTestsDirectory,
14
+ isTypeDeclaration,
15
+ readPatternIdentifierNames,
16
+ readProgramReportNode,
17
+ } from "./helpers.ts";
11
18
 
12
19
  type HookRuntimeExportEntry = {
13
20
  kind:
@@ -138,7 +145,11 @@ const hookFileContractRule: RuleModule = {
138
145
  },
139
146
  },
140
147
  create(context) {
141
- if (isExemptSupportBasename(context.filename)) {
148
+ if (
149
+ isExemptSupportBasename(context.filename) ||
150
+ isInStoriesDirectory(context.filename) ||
151
+ isInTestsDirectory(context.filename)
152
+ ) {
142
153
  return {};
143
154
  }
144
155
 
@@ -147,7 +158,7 @@ const hookFileContractRule: RuleModule = {
147
158
  const runtimeExportEntries = readRuntimeExportEntries(node);
148
159
  if (runtimeExportEntries.length === 0) {
149
160
  context.report({
150
- node,
161
+ node: readProgramReportNode(node),
151
162
  messageId: "missingMainHookExport",
152
163
  });
153
164
  return;
@@ -1,11 +1,19 @@
1
1
  import type {
2
2
  AstExportNamedDeclaration,
3
3
  AstExportSpecifier,
4
+ AstNode,
4
5
  AstProgram,
5
6
  AstProgramStatement,
6
7
  RuleModule,
7
8
  } from "./types.ts";
8
- import { getFilenameWithoutExtension, isExemptSupportBasename, readDeclarationIdentifierNames } from "./helpers.ts";
9
+ import {
10
+ getFilenameWithoutExtension,
11
+ isExemptSupportBasename,
12
+ isInStoriesDirectory,
13
+ isInTestsDirectory,
14
+ readDeclarationIdentifierNames,
15
+ readProgramReportNode,
16
+ } from "./helpers.ts";
9
17
 
10
18
  function isTypeOnlyExportSpecifier(
11
19
  specifier: AstExportSpecifier,
@@ -22,18 +30,23 @@ function readExportedSpecifierName(specifier: AstExportSpecifier): string {
22
30
  return String(specifier.exported.value);
23
31
  }
24
32
 
25
- function readFirstRuntimeExportName(program: AstProgram): string | null {
33
+ type HookRuntimeExportEntry = {
34
+ name: string;
35
+ reportNode: AstNode;
36
+ };
37
+
38
+ function readFirstRuntimeExportEntry(program: AstProgram): HookRuntimeExportEntry | null {
26
39
  for (const statement of program.body) {
27
- const exportName = readStatementRuntimeExportName(statement);
28
- if (exportName !== null) {
29
- return exportName;
40
+ const exportEntry = readStatementRuntimeExportEntry(statement);
41
+ if (exportEntry !== null) {
42
+ return exportEntry;
30
43
  }
31
44
  }
32
45
 
33
46
  return null;
34
47
  }
35
48
 
36
- function readStatementRuntimeExportName(statement: AstProgramStatement): string | null {
49
+ function readStatementRuntimeExportEntry(statement: AstProgramStatement): HookRuntimeExportEntry | null {
37
50
  if (statement.type !== "ExportNamedDeclaration") {
38
51
  return null;
39
52
  }
@@ -50,10 +63,28 @@ function readStatementRuntimeExportName(statement: AstProgramStatement): string
50
63
 
51
64
  if (statement.declaration.type === "VariableDeclaration") {
52
65
  const firstDeclarator = statement.declaration.declarations[0];
53
- return firstDeclarator?.id.type === "Identifier" ? firstDeclarator.id.name : null;
66
+ if (!firstDeclarator || firstDeclarator.id.type !== "Identifier") {
67
+ return null;
68
+ }
69
+
70
+ return {
71
+ name: firstDeclarator.id.name,
72
+ reportNode: firstDeclarator.id,
73
+ };
74
+ }
75
+
76
+ const declarationName = readDeclarationIdentifierNames(statement.declaration)[0];
77
+ if (!declarationName) {
78
+ return null;
54
79
  }
55
80
 
56
- return readDeclarationIdentifierNames(statement.declaration)[0] ?? null;
81
+ const reportNode =
82
+ "id" in statement.declaration && statement.declaration.id ? statement.declaration.id : statement.declaration;
83
+
84
+ return {
85
+ name: declarationName,
86
+ reportNode,
87
+ };
57
88
  }
58
89
 
59
90
  const runtimeSpecifier = statement.specifiers.find((specifier) => !isTypeOnlyExportSpecifier(specifier, statement));
@@ -61,7 +92,10 @@ function readStatementRuntimeExportName(statement: AstProgramStatement): string
61
92
  return null;
62
93
  }
63
94
 
64
- return readExportedSpecifierName(runtimeSpecifier);
95
+ return {
96
+ name: readExportedSpecifierName(runtimeSpecifier),
97
+ reportNode: runtimeSpecifier.exported.type === "Identifier" ? runtimeSpecifier.exported : runtimeSpecifier,
98
+ };
65
99
  }
66
100
 
67
101
  function readExpectedHookNameFromFilename(filename: string): string | null {
@@ -101,21 +135,26 @@ const hookFileNamingConventionRule: RuleModule = {
101
135
  },
102
136
  },
103
137
  create(context) {
104
- if (isExemptSupportBasename(context.filename)) {
138
+ if (
139
+ isExemptSupportBasename(context.filename) ||
140
+ isInStoriesDirectory(context.filename) ||
141
+ isInTestsDirectory(context.filename)
142
+ ) {
105
143
  return {};
106
144
  }
107
145
 
108
146
  return {
109
147
  Program(node) {
110
- const exportedHookName = readFirstRuntimeExportName(node);
111
- if (!exportedHookName) {
148
+ const exportedHookEntry = readFirstRuntimeExportEntry(node);
149
+ if (!exportedHookEntry) {
112
150
  return;
113
151
  }
114
152
 
153
+ const { name: exportedHookName, reportNode } = exportedHookEntry;
115
154
  const expectedHookName = readExpectedHookNameFromFilename(context.filename);
116
155
  if (!expectedHookName) {
117
156
  context.report({
118
- node,
157
+ node: readProgramReportNode(node),
119
158
  messageId: "invalidHookFileName",
120
159
  });
121
160
  return;
@@ -123,7 +162,7 @@ const hookFileNamingConventionRule: RuleModule = {
123
162
 
124
163
  if (!/^use[A-Z][A-Za-z0-9]*$/u.test(exportedHookName)) {
125
164
  context.report({
126
- node,
165
+ node: reportNode,
127
166
  messageId: "invalidHookExportName",
128
167
  });
129
168
  }
@@ -135,7 +174,7 @@ const hookFileNamingConventionRule: RuleModule = {
135
174
  const extension = context.filename.endsWith(".tsx") ? ".tsx" : ".ts";
136
175
 
137
176
  context.report({
138
- node,
177
+ node: reportNode,
139
178
  messageId: "mismatchedHookFileName",
140
179
  data: {
141
180
  exportedName: exportedHookName,
@@ -1,12 +1,14 @@
1
1
  import type { AstProgram, RuleModule } from "./types.ts";
2
- import type { TSESTree } from "@typescript-eslint/types";
3
2
  import { dirname, join } from "node:path";
4
3
  import {
5
4
  findDescendantFilePath,
6
5
  getBaseName,
7
6
  getFilenameWithoutExtension,
8
7
  isExemptSupportBasename,
8
+ isInStoriesDirectory,
9
+ isInTestsDirectory,
9
10
  readAbbreviatedSiblingDirectoryPath,
11
+ readProgramReportNode,
10
12
  } from "./helpers.ts";
11
13
 
12
14
  function readRequiredTestsDirectoryPath(filename: string): string {
@@ -20,10 +22,6 @@ function readRequiredHookTestFileName(filename: string): string {
20
22
  return `${sourceBaseName}${testExtension}`;
21
23
  }
22
24
 
23
- function readReportNode(program: AstProgram): TSESTree.Node {
24
- return program.body[0] ?? program;
25
- }
26
-
27
25
  const hookTestFileConventionRule: RuleModule = {
28
26
  meta: {
29
27
  type: "problem" as const,
@@ -38,7 +36,11 @@ const hookTestFileConventionRule: RuleModule = {
38
36
  },
39
37
  },
40
38
  create(context) {
41
- if (isExemptSupportBasename(context.filename)) {
39
+ if (
40
+ isExemptSupportBasename(context.filename) ||
41
+ isInStoriesDirectory(context.filename) ||
42
+ isInTestsDirectory(context.filename)
43
+ ) {
42
44
  return {};
43
45
  }
44
46
 
@@ -52,7 +54,7 @@ const hookTestFileConventionRule: RuleModule = {
52
54
  }
53
55
 
54
56
  context.report({
55
- node: readReportNode(node),
57
+ node: readProgramReportNode(node),
56
58
  messageId: "missingHookTestFile",
57
59
  data: {
58
60
  requiredTestFileName,
@@ -4,6 +4,7 @@ import {
4
4
  getFilenameWithoutExtension,
5
5
  isStrictAreaAllowedSupportFile,
6
6
  readPathFromDirectory,
7
+ readProgramReportNode,
7
8
  } from "./helpers.ts";
8
9
 
9
10
  function isAllowedHookOwnershipBasename(filename: string): boolean {
@@ -61,7 +62,7 @@ const hooksDirectoryFileConventionRule: RuleModule = {
61
62
  }
62
63
 
63
64
  context.report({
64
- node,
65
+ node: readProgramReportNode(node),
65
66
  messageId: "invalidHooksDirectoryFile",
66
67
  data: {
67
68
  relativePath: relativePath || ".",
@@ -1,6 +1,6 @@
1
1
  import type { TSESTree } from "@typescript-eslint/types";
2
2
  import type { AstProgramStatement, RuleModule } from "./types.ts";
3
- import { getBaseName } from "./helpers.ts";
3
+ import { getBaseName, readProgramReportNode } from "./helpers.ts";
4
4
 
5
5
  type IndexMessageId = "unexpectedIndexExport" | "unexpectedIndexStatement";
6
6
 
@@ -86,10 +86,6 @@ function readExportDefaultReportNode(node: TSESTree.ExportDefaultDeclaration): T
86
86
  return node;
87
87
  }
88
88
 
89
- function readProgramReportNode(node: TSESTree.Program): TSESTree.Node {
90
- return node.body[0] ?? node;
91
- }
92
-
93
89
  function readIndexViolationReportNode(statement: AstProgramStatement): TSESTree.Node {
94
90
  if (statement.type === "ExportNamedDeclaration") {
95
91
  if (statement.declaration) {
@@ -1,7 +1,7 @@
1
1
  import type { RuleModule } from "./types.ts";
2
2
  import { existsSync, statSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
- import { readPathFromStoriesDirectory, readPathFromTestsDirectory } from "./helpers.ts";
4
+ import { readPathFromStoriesDirectory, readPathFromTestsDirectory, readProgramReportNode } from "./helpers.ts";
5
5
 
6
6
  const FIXTURE_ENTRYPOINT_CANDIDATE_KEYS = ["fixtures.ts", "fixtures.tsx", "fixtures/"];
7
7
 
@@ -127,7 +127,7 @@ const singleFixtureEntrypointRule: RuleModule = {
127
127
  return {
128
128
  Program(node) {
129
129
  context.report({
130
- node,
130
+ node: readProgramReportNode(node),
131
131
  messageId: "conflictingFixtureEntrypoints",
132
132
  data: {
133
133
  directoryLabel,
@@ -1,5 +1,5 @@
1
1
  import type { RuleModule } from "./types.ts";
2
- import { isInStoriesDirectory, readPathFromStoriesDirectory } from "./helpers.ts";
2
+ import { isInStoriesDirectory, readPathFromStoriesDirectory, readProgramReportNode } from "./helpers.ts";
3
3
 
4
4
  const ALLOWED_ROOT_STORY_FILES_PATTERN = /^[^/]+\.stories\.tsx$/u;
5
5
  const ALLOWED_SUPPORT_FILES = new Set(["fixtures.ts", "fixtures.tsx", "helpers.ts", "helpers.tsx"]);
@@ -41,7 +41,7 @@ const storiesDirectoryFileConventionRule: RuleModule = {
41
41
  }
42
42
 
43
43
  context.report({
44
- node,
44
+ node: readProgramReportNode(node),
45
45
  messageId: "invalidStoriesDirectoryFile",
46
46
  data: {
47
47
  relativePath,
@@ -1,6 +1,12 @@
1
1
  import type { TSESTree } from "@typescript-eslint/types";
2
2
  import type { AstProgram, RuleModule } from "./types.ts";
3
- import { getStorySourceBaseName, isPascalCase, unwrapExpression, unwrapTypeScriptExpression } from "./helpers.ts";
3
+ import {
4
+ getStorySourceBaseName,
5
+ isPascalCase,
6
+ readProgramReportNode,
7
+ unwrapExpression,
8
+ unwrapTypeScriptExpression,
9
+ } from "./helpers.ts";
4
10
 
5
11
  type StoryExportRecord = {
6
12
  exportedName: string;
@@ -245,7 +251,7 @@ const storyExportContractRule: RuleModule = {
245
251
 
246
252
  if (storyEntries.length === 0) {
247
253
  context.report({
248
- node,
254
+ node: readProgramReportNode(node),
249
255
  messageId: "missingStoryExport",
250
256
  });
251
257
  return;
@@ -285,7 +291,7 @@ const storyExportContractRule: RuleModule = {
285
291
  const exportedStoryEntries = storyEntries.filter((storyEntry) => storyEntry.exports.length > 0);
286
292
  if (exportedStoryEntries.length === 0) {
287
293
  context.report({
288
- node,
294
+ node: readProgramReportNode(node),
289
295
  messageId: "missingStoryExport",
290
296
  });
291
297
  return;
@@ -1,4 +1,3 @@
1
- import type { TSESTree } from "@typescript-eslint/types";
2
1
  import type { AstProgram, RuleModule } from "./types.ts";
3
2
  import { existsSync } from "node:fs";
4
3
  import { join } from "node:path";
@@ -6,6 +5,7 @@ import {
6
5
  getStorySourceBaseName,
7
6
  readAbbreviatedPath,
8
7
  readPathFromStoriesDirectory,
8
+ readProgramReportNode,
9
9
  readRootPathBeforeDirectory,
10
10
  } from "./helpers.ts";
11
11
 
@@ -23,10 +23,6 @@ function readRequiredSiblingComponentFilePath(filename: string): string | null {
23
23
  return join(siblingDirectoryPath, `${storySourceBaseName}.tsx`);
24
24
  }
25
25
 
26
- function readReportNode(program: AstProgram): TSESTree.Node {
27
- return program.body[0] ?? program;
28
- }
29
-
30
26
  const storyFileLocationConventionRule: RuleModule = {
31
27
  meta: {
32
28
  type: "problem" as const,
@@ -45,7 +41,7 @@ const storyFileLocationConventionRule: RuleModule = {
45
41
  create(context) {
46
42
  return {
47
43
  Program(node: AstProgram) {
48
- const reportNode = readReportNode(node);
44
+ const reportNode = readProgramReportNode(node);
49
45
  const relativeStoryPath = readPathFromStoriesDirectory(context.filename);
50
46
  if (relativeStoryPath === null) {
51
47
  context.report({
@@ -1,6 +1,6 @@
1
1
  import type { TSESTree } from "@typescript-eslint/types";
2
2
  import type { AstProgram, RuleModule } from "./types.ts";
3
- import { unwrapExpression } from "./helpers.ts";
3
+ import { readProgramReportNode, unwrapExpression } from "./helpers.ts";
4
4
 
5
5
  type MetaBinding = {
6
6
  declaration: TSESTree.VariableDeclaration;
@@ -79,7 +79,7 @@ const storyMetaTypeAnnotationRule: RuleModule = {
79
79
  const defaultExportDeclaration = readDefaultExportDeclaration(node);
80
80
  if (!defaultExportDeclaration || defaultExportDeclaration.declaration.type !== "Identifier") {
81
81
  context.report({
82
- node: defaultExportDeclaration ?? node,
82
+ node: defaultExportDeclaration ?? readProgramReportNode(node),
83
83
  messageId: "invalidMetaBinding",
84
84
  });
85
85
  return;
@@ -1,13 +1,11 @@
1
- import type { AstExpression, AstProgram, AstProgramStatement, RuleModule } from "./types.ts";
2
- import { getBaseName, isInTestsDirectory } from "./helpers.ts";
1
+ import type { AstExpression, RuleModule } from "./types.ts";
2
+ import { getBaseName, isInTestsDirectory, readProgramReportNode } from "./helpers.ts";
3
3
 
4
4
  const TEST_FRAMEWORK_MODULES = new Set(["@jest/globals", "bun:test", "node:test", "vitest"]);
5
5
  const TEST_IMPORT_NAMES = new Set(["describe", "it", "test"]);
6
6
  const REQUIRED_TEST_FILE_NAME_PATTERN = /\.test\.tsx?$/u;
7
7
  const SPEC_TEST_FILE_NAME_PATTERN = /\.spec\.tsx?$/u;
8
8
 
9
- type ProgramReportNode = AstProgram | AstProgramStatement;
10
-
11
9
  function readCallTargetName(node: AstExpression): string | null {
12
10
  if (node.type === "Identifier") {
13
11
  return node.name;
@@ -25,10 +23,6 @@ function readCallTargetName(node: AstExpression): string | null {
25
23
  return null;
26
24
  }
27
25
 
28
- function readProgramReportNode(node: AstProgram): ProgramReportNode {
29
- return node.body[0] ?? node;
30
- }
31
-
32
26
  const testFileLocationConventionRule: RuleModule = {
33
27
  meta: {
34
28
  type: "problem" as const,
@@ -1,5 +1,5 @@
1
1
  import type { RuleModule } from "./types.ts";
2
- import { isInTestsDirectory, readPathFromTestsDirectory } from "./helpers.ts";
2
+ import { isInTestsDirectory, readPathFromTestsDirectory, readProgramReportNode } from "./helpers.ts";
3
3
 
4
4
  const ALLOWED_ROOT_TEST_FILES_PATTERN = /^[^/]+\.test\.tsx?$/u;
5
5
  const ALLOWED_SUPPORT_FILES = new Set(["fixtures.ts", "fixtures.tsx", "helpers.ts", "helpers.tsx"]);
@@ -41,7 +41,7 @@ const testsDirectoryFileConventionRule: RuleModule = {
41
41
  }
42
42
 
43
43
  context.report({
44
- node,
44
+ node: readProgramReportNode(node),
45
45
  messageId: "invalidTestsDirectoryFile",
46
46
  data: {
47
47
  relativePath,