@api-extractor-tools/eslint-plugin 0.1.0-alpha.0 → 0.1.0-alpha.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 (104) hide show
  1. package/ARCHITECTURE.md +201 -0
  2. package/CHANGELOG.md +24 -0
  3. package/README.md +306 -10
  4. package/api-extractor.json +1 -0
  5. package/dist/configs/recommended.d.ts +1 -1
  6. package/dist/configs/recommended.d.ts.map +1 -1
  7. package/dist/configs/recommended.js +7 -1
  8. package/dist/configs/recommended.js.map +1 -1
  9. package/dist/index.d.ts +9 -16
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +11 -15
  12. package/dist/index.js.map +1 -1
  13. package/dist/node.d.ts +28 -0
  14. package/dist/node.d.ts.map +1 -0
  15. package/dist/node.js +41 -0
  16. package/dist/node.js.map +1 -0
  17. package/dist/rules/extra-release-tag.d.ts +24 -0
  18. package/dist/rules/extra-release-tag.d.ts.map +1 -0
  19. package/dist/rules/extra-release-tag.js +141 -0
  20. package/dist/rules/extra-release-tag.js.map +1 -0
  21. package/dist/rules/forgotten-export.d.ts +24 -0
  22. package/dist/rules/forgotten-export.d.ts.map +1 -0
  23. package/dist/rules/forgotten-export.js +212 -0
  24. package/dist/rules/forgotten-export.js.map +1 -0
  25. package/dist/rules/incompatible-release-tags.d.ts +25 -0
  26. package/dist/rules/incompatible-release-tags.d.ts.map +1 -0
  27. package/dist/rules/incompatible-release-tags.js +237 -0
  28. package/dist/rules/incompatible-release-tags.js.map +1 -0
  29. package/dist/rules/index.d.ts +2 -6
  30. package/dist/rules/index.d.ts.map +1 -1
  31. package/dist/rules/index.js +13 -1
  32. package/dist/rules/index.js.map +1 -1
  33. package/dist/rules/missing-release-tag.d.ts +4 -0
  34. package/dist/rules/missing-release-tag.d.ts.map +1 -1
  35. package/dist/rules/missing-release-tag.js +14 -21
  36. package/dist/rules/missing-release-tag.js.map +1 -1
  37. package/dist/rules/override-keyword.d.ts +4 -0
  38. package/dist/rules/override-keyword.d.ts.map +1 -1
  39. package/dist/rules/override-keyword.js +9 -11
  40. package/dist/rules/override-keyword.js.map +1 -1
  41. package/dist/rules/package-documentation.d.ts +1 -1
  42. package/dist/rules/package-documentation.d.ts.map +1 -1
  43. package/dist/rules/package-documentation.js +7 -28
  44. package/dist/rules/package-documentation.js.map +1 -1
  45. package/dist/rules/public-on-non-exported.d.ts +24 -0
  46. package/dist/rules/public-on-non-exported.d.ts.map +1 -0
  47. package/dist/rules/public-on-non-exported.js +191 -0
  48. package/dist/rules/public-on-non-exported.js.map +1 -0
  49. package/dist/rules/public-on-private-member.d.ts +24 -0
  50. package/dist/rules/public-on-private-member.d.ts.map +1 -0
  51. package/dist/rules/public-on-private-member.js +111 -0
  52. package/dist/rules/public-on-private-member.js.map +1 -0
  53. package/dist/rules/valid-enum-type.d.ts +17 -0
  54. package/dist/rules/valid-enum-type.d.ts.map +1 -0
  55. package/dist/rules/valid-enum-type.js +206 -0
  56. package/dist/rules/valid-enum-type.js.map +1 -0
  57. package/dist/types.d.ts +63 -35
  58. package/dist/types.d.ts.map +1 -1
  59. package/dist/types.js +4 -1
  60. package/dist/types.js.map +1 -1
  61. package/dist/utils/tsdoc-parser.d.ts +35 -0
  62. package/dist/utils/tsdoc-parser.d.ts.map +1 -1
  63. package/dist/utils/tsdoc-parser.js +40 -0
  64. package/dist/utils/tsdoc-parser.js.map +1 -1
  65. package/docs/rules/valid-enum-type.md +153 -0
  66. package/package.json +22 -8
  67. package/src/configs/recommended.ts +7 -1
  68. package/src/index.ts +21 -15
  69. package/src/node.ts +50 -0
  70. package/src/rules/extra-release-tag.ts +201 -0
  71. package/src/rules/forgotten-export.ts +274 -0
  72. package/src/rules/incompatible-release-tags.ts +331 -0
  73. package/src/rules/index.ts +13 -1
  74. package/src/rules/missing-release-tag.ts +11 -26
  75. package/src/rules/override-keyword.ts +6 -8
  76. package/src/rules/package-documentation.ts +5 -31
  77. package/src/rules/public-on-non-exported.ts +265 -0
  78. package/src/rules/public-on-private-member.ts +157 -0
  79. package/src/rules/valid-enum-type.ts +252 -0
  80. package/src/types.ts +60 -17
  81. package/src/utils/config-loader.ts +1 -0
  82. package/src/utils/entry-point.ts +1 -0
  83. package/src/utils/tsdoc-parser.ts +67 -0
  84. package/temp/eslint-plugin.api.md +96 -47
  85. package/test/index.test.ts +1 -0
  86. package/test/rules/extra-release-tag.test.ts +276 -0
  87. package/test/rules/forgotten-export.test.ts +190 -0
  88. package/test/rules/incompatible-release-tags.test.ts +340 -0
  89. package/test/rules/missing-release-tag.test.ts +2 -1
  90. package/test/rules/override-keyword.test.ts +2 -1
  91. package/test/rules/package-documentation.test.ts +8 -6
  92. package/test/rules/public-on-non-exported.test.ts +201 -0
  93. package/test/rules/public-on-private-member.test.ts +207 -0
  94. package/test/rules/valid-enum-type.test.ts +409 -0
  95. package/test/types.test-d.ts +20 -0
  96. package/test/utils/config-loader.test.ts +1 -0
  97. package/test/utils/tsdoc-parser.test.ts +117 -9
  98. package/tsconfig.json +1 -0
  99. package/vitest.config.mts +1 -0
  100. package/dist/utils/index.d.ts +0 -8
  101. package/dist/utils/index.d.ts.map +0 -1
  102. package/dist/utils/index.js +0 -21
  103. package/dist/utils/index.js.map +0 -1
  104. package/src/utils/index.ts +0 -17
@@ -0,0 +1,265 @@
1
+ /**
2
+ * ESLint rule preventing the use of @public tag on non-exported symbols.
3
+ *
4
+ * @remarks
5
+ * The @public tag indicates that a symbol is part of the public API, but non-exported
6
+ * symbols cannot be accessed by consumers and should not be marked as public.
7
+ *
8
+ * @internal
9
+ */
10
+
11
+ import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'
12
+ import {
13
+ getLeadingTSDocComment,
14
+ parseTSDocComment,
15
+ extractReleaseTag,
16
+ } from '../utils/tsdoc-parser'
17
+ import type { ApiExtractorLogLevel } from '../types'
18
+
19
+ const createRule = ESLintUtils.RuleCreator(
20
+ (name) =>
21
+ `https://github.com/mike-north/api-extractor-tools/blob/main/tools/eslint-plugin/docs/rules/${name}.md`,
22
+ )
23
+
24
+ type MessageIds = 'publicOnNonExported'
25
+
26
+ /**
27
+ * Options for the public-on-non-exported rule.
28
+ * @alpha
29
+ */
30
+ export interface PublicOnNonExportedRuleOptions {
31
+ /**
32
+ * Severity level for public tags on non-exported symbols.
33
+ * @defaultValue 'error'
34
+ */
35
+ severity?: ApiExtractorLogLevel
36
+ }
37
+
38
+ export const publicOnNonExported = createRule<
39
+ [PublicOnNonExportedRuleOptions],
40
+ MessageIds
41
+ >({
42
+ name: 'public-on-non-exported',
43
+ meta: {
44
+ type: 'problem',
45
+ docs: {
46
+ description:
47
+ 'Prevent the use of @public tag on symbols that are not exported',
48
+ },
49
+ messages: {
50
+ publicOnNonExported:
51
+ 'Symbol "{{name}}" has the @public tag but is not exported. Only exported symbols can be marked as @public.',
52
+ },
53
+ schema: [
54
+ {
55
+ type: 'object',
56
+ properties: {
57
+ severity: {
58
+ type: 'string',
59
+ enum: ['error', 'warning', 'none'],
60
+ description:
61
+ 'Severity level for public tags on non-exported symbols',
62
+ },
63
+ },
64
+ additionalProperties: false,
65
+ },
66
+ ],
67
+ },
68
+ defaultOptions: [{}],
69
+ create(context) {
70
+ const options = context.options[0] ?? {}
71
+ const severity = options.severity ?? 'error'
72
+
73
+ // If severity is 'none', disable the rule
74
+ if (severity === 'none') {
75
+ return {}
76
+ }
77
+
78
+ const sourceCode = context.sourceCode
79
+ const exportedSymbols = new Set<string>()
80
+ const symbolsWithPublicTag = new Map<
81
+ string,
82
+ { node: TSESTree.Node; name: string }
83
+ >()
84
+
85
+ /**
86
+ * Checks if a node has the @public release tag.
87
+ */
88
+ function hasPublicTag(node: TSESTree.Node): boolean {
89
+ const commentText = getLeadingTSDocComment(sourceCode, node)
90
+ if (!commentText) {
91
+ return false
92
+ }
93
+
94
+ const parsed = parseTSDocComment(commentText)
95
+ if (parsed.docComment) {
96
+ const tag = extractReleaseTag(parsed.docComment)
97
+ return tag === 'public'
98
+ }
99
+
100
+ return false
101
+ }
102
+
103
+ /**
104
+ * Gets the name of a declaration.
105
+ */
106
+ function getDeclarationName(
107
+ node:
108
+ | TSESTree.FunctionDeclaration
109
+ | TSESTree.ClassDeclaration
110
+ | TSESTree.TSInterfaceDeclaration
111
+ | TSESTree.TSTypeAliasDeclaration
112
+ | TSESTree.TSEnumDeclaration
113
+ | TSESTree.VariableDeclaration,
114
+ ): string | undefined {
115
+ if (node.type === AST_NODE_TYPES.VariableDeclaration) {
116
+ const firstDeclarator = node.declarations[0]
117
+ if (firstDeclarator?.id.type === AST_NODE_TYPES.Identifier) {
118
+ return firstDeclarator.id.name
119
+ }
120
+ return undefined
121
+ }
122
+
123
+ if ('id' in node && node.id) {
124
+ return node.id.name
125
+ }
126
+
127
+ return undefined
128
+ }
129
+
130
+ /**
131
+ * Collects all exported symbol names.
132
+ */
133
+ function collectExportedSymbols(
134
+ node: TSESTree.ExportNamedDeclaration | TSESTree.ExportDefaultDeclaration,
135
+ ): void {
136
+ if (node.type === AST_NODE_TYPES.ExportNamedDeclaration) {
137
+ // export { foo, bar }
138
+ if (node.specifiers) {
139
+ for (const specifier of node.specifiers) {
140
+ if (specifier.type === AST_NODE_TYPES.ExportSpecifier) {
141
+ if (specifier.exported.type === AST_NODE_TYPES.Identifier) {
142
+ exportedSymbols.add(specifier.exported.name)
143
+ }
144
+ // Also add the local name in case it's different
145
+ if (specifier.local.type === AST_NODE_TYPES.Identifier) {
146
+ exportedSymbols.add(specifier.local.name)
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ // export function foo() {} or export class Bar {}
153
+ if (node.declaration) {
154
+ const decl = node.declaration
155
+ if (
156
+ 'id' in decl &&
157
+ decl.id &&
158
+ decl.id.type === AST_NODE_TYPES.Identifier
159
+ ) {
160
+ exportedSymbols.add(decl.id.name)
161
+ } else if (decl.type === AST_NODE_TYPES.VariableDeclaration) {
162
+ for (const declarator of decl.declarations) {
163
+ if (declarator.id.type === AST_NODE_TYPES.Identifier) {
164
+ exportedSymbols.add(declarator.id.name)
165
+ }
166
+ }
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Checks if a node is directly exported.
174
+ */
175
+ function isDirectlyExported(node: TSESTree.Node): boolean {
176
+ const parent = node.parent
177
+ return (
178
+ parent?.type === AST_NODE_TYPES.ExportNamedDeclaration ||
179
+ parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration
180
+ )
181
+ }
182
+
183
+ /**
184
+ * Collects symbols with @public tag.
185
+ */
186
+ function collectSymbolWithPublicTag(
187
+ node:
188
+ | TSESTree.FunctionDeclaration
189
+ | TSESTree.ClassDeclaration
190
+ | TSESTree.TSInterfaceDeclaration
191
+ | TSESTree.TSTypeAliasDeclaration
192
+ | TSESTree.TSEnumDeclaration
193
+ | TSESTree.VariableDeclaration,
194
+ ): void {
195
+ // Check if the node has @public tag
196
+ if (!hasPublicTag(node) && !isDirectlyExported(node)) {
197
+ // Check if the export has the tag
198
+ const parent = node.parent
199
+ if (
200
+ parent &&
201
+ (parent.type === AST_NODE_TYPES.ExportNamedDeclaration ||
202
+ parent.type === AST_NODE_TYPES.ExportDefaultDeclaration)
203
+ ) {
204
+ if (!hasPublicTag(parent)) {
205
+ return
206
+ }
207
+ } else {
208
+ return
209
+ }
210
+ }
211
+
212
+ const name = getDeclarationName(node)
213
+ if (name && hasPublicTag(node)) {
214
+ symbolsWithPublicTag.set(name, { node, name })
215
+ }
216
+ }
217
+
218
+ return {
219
+ ExportNamedDeclaration(node): void {
220
+ collectExportedSymbols(node)
221
+ },
222
+
223
+ ExportDefaultDeclaration(node): void {
224
+ collectExportedSymbols(node)
225
+ },
226
+
227
+ FunctionDeclaration(node): void {
228
+ collectSymbolWithPublicTag(node)
229
+ },
230
+
231
+ ClassDeclaration(node): void {
232
+ collectSymbolWithPublicTag(node)
233
+ },
234
+
235
+ TSInterfaceDeclaration(node): void {
236
+ collectSymbolWithPublicTag(node)
237
+ },
238
+
239
+ TSTypeAliasDeclaration(node): void {
240
+ collectSymbolWithPublicTag(node)
241
+ },
242
+
243
+ TSEnumDeclaration(node): void {
244
+ collectSymbolWithPublicTag(node)
245
+ },
246
+
247
+ VariableDeclaration(node): void {
248
+ collectSymbolWithPublicTag(node)
249
+ },
250
+
251
+ 'Program:exit'(): void {
252
+ // Check for @public on non-exported symbols
253
+ for (const [name, { node }] of symbolsWithPublicTag) {
254
+ if (!exportedSymbols.has(name)) {
255
+ context.report({
256
+ node,
257
+ messageId: 'publicOnNonExported',
258
+ data: { name },
259
+ })
260
+ }
261
+ }
262
+ },
263
+ }
264
+ },
265
+ })
@@ -0,0 +1,157 @@
1
+ /**
2
+ * ESLint rule preventing the use of @public tag on private or protected class members.
3
+ *
4
+ * @remarks
5
+ * Private and protected members cannot be public API since they are not accessible
6
+ * outside the class or to external consumers.
7
+ *
8
+ * @internal
9
+ */
10
+
11
+ import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'
12
+ import {
13
+ getLeadingTSDocComment,
14
+ parseTSDocComment,
15
+ extractReleaseTag,
16
+ } from '../utils/tsdoc-parser'
17
+ import type { ApiExtractorLogLevel } from '../types'
18
+
19
+ const createRule = ESLintUtils.RuleCreator(
20
+ (name) =>
21
+ `https://github.com/mike-north/api-extractor-tools/blob/main/tools/eslint-plugin/docs/rules/${name}.md`,
22
+ )
23
+
24
+ type MessageIds = 'publicOnPrivateMember'
25
+
26
+ /**
27
+ * Options for the public-on-private-member rule.
28
+ * @alpha
29
+ */
30
+ export interface PublicOnPrivateMemberRuleOptions {
31
+ /**
32
+ * Severity level for public tags on private/protected members.
33
+ * @defaultValue 'error'
34
+ */
35
+ severity?: ApiExtractorLogLevel
36
+ }
37
+
38
+ export const publicOnPrivateMember = createRule<
39
+ [PublicOnPrivateMemberRuleOptions],
40
+ MessageIds
41
+ >({
42
+ name: 'public-on-private-member',
43
+ meta: {
44
+ type: 'problem',
45
+ docs: {
46
+ description:
47
+ 'Prevent the use of @public tag on private or protected class members',
48
+ },
49
+ messages: {
50
+ publicOnPrivateMember:
51
+ '{{accessibility}} member "{{name}}" cannot have the @public tag. Only public members can be marked as @public.',
52
+ },
53
+ schema: [
54
+ {
55
+ type: 'object',
56
+ properties: {
57
+ severity: {
58
+ type: 'string',
59
+ enum: ['error', 'warning', 'none'],
60
+ description:
61
+ 'Severity level for public tags on private/protected members',
62
+ },
63
+ },
64
+ additionalProperties: false,
65
+ },
66
+ ],
67
+ },
68
+ defaultOptions: [{}],
69
+ create(context) {
70
+ const options = context.options[0] ?? {}
71
+ const severity = options.severity ?? 'error'
72
+
73
+ // If severity is 'none', disable the rule
74
+ if (severity === 'none') {
75
+ return {}
76
+ }
77
+
78
+ const sourceCode = context.sourceCode
79
+
80
+ /**
81
+ * Checks if a node has the @public release tag.
82
+ */
83
+ function hasPublicTag(node: TSESTree.Node): boolean {
84
+ const commentText = getLeadingTSDocComment(sourceCode, node)
85
+ if (!commentText) {
86
+ return false
87
+ }
88
+
89
+ const parsed = parseTSDocComment(commentText)
90
+ if (parsed.docComment) {
91
+ const tag = extractReleaseTag(parsed.docComment)
92
+ return tag === 'public'
93
+ }
94
+
95
+ return false
96
+ }
97
+
98
+ /**
99
+ * Gets the name of a member.
100
+ */
101
+ function getMemberName(
102
+ node: TSESTree.PropertyDefinition | TSESTree.MethodDefinition,
103
+ ): string {
104
+ if (node.key.type === AST_NODE_TYPES.Identifier) {
105
+ return node.key.name
106
+ }
107
+ if (
108
+ node.key.type === AST_NODE_TYPES.Literal &&
109
+ typeof node.key.value === 'string'
110
+ ) {
111
+ return node.key.value
112
+ }
113
+ return '<computed>'
114
+ }
115
+
116
+ /**
117
+ * Checks a class member for @public tag on private/protected members.
118
+ */
119
+ function checkClassMember(
120
+ node: TSESTree.PropertyDefinition | TSESTree.MethodDefinition,
121
+ ): void {
122
+ // Skip if not private or protected
123
+ if (
124
+ node.accessibility !== 'private' &&
125
+ node.accessibility !== 'protected'
126
+ ) {
127
+ return
128
+ }
129
+
130
+ // Check if it has @public tag
131
+ if (hasPublicTag(node)) {
132
+ const name = getMemberName(node)
133
+ const accessibility =
134
+ node.accessibility.charAt(0).toUpperCase() +
135
+ node.accessibility.slice(1)
136
+
137
+ context.report({
138
+ node,
139
+ messageId: 'publicOnPrivateMember',
140
+ data: {
141
+ name,
142
+ accessibility,
143
+ },
144
+ })
145
+ }
146
+ }
147
+
148
+ return {
149
+ PropertyDefinition(node): void {
150
+ checkClassMember(node)
151
+ },
152
+ MethodDefinition(node): void {
153
+ checkClassMember(node)
154
+ },
155
+ }
156
+ },
157
+ })
@@ -0,0 +1,252 @@
1
+ /**
2
+ * ESLint rule for validating `@enumType` TSDoc tag usage.
3
+ *
4
+ * @remarks
5
+ * This rule validates that `@enumType` tags are:
6
+ * - Only used on enum declarations or string literal union type aliases
7
+ * - Have a valid value ('open' or 'closed')
8
+ * - Not duplicated on a single declaration
9
+ *
10
+ * @internal
11
+ */
12
+
13
+ import { AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'
14
+ import { getLeadingTSDocComment, extractEnumType } from '../utils/tsdoc-parser'
15
+ import type { ValidEnumTypeRuleOptions } from '../types'
16
+
17
+ const createRule = ESLintUtils.RuleCreator(
18
+ (name) =>
19
+ `https://github.com/mike-north/api-extractor-tools/blob/main/tools/eslint-plugin/docs/rules/${name}.md`,
20
+ )
21
+
22
+ type MessageIds =
23
+ | 'missingValue'
24
+ | 'invalidValue'
25
+ | 'multipleEnumTypes'
26
+ | 'invalidConstruct'
27
+ | 'missingEnumType'
28
+
29
+ export const validEnumType = createRule<[ValidEnumTypeRuleOptions], MessageIds>(
30
+ {
31
+ name: 'valid-enum-type',
32
+ meta: {
33
+ type: 'problem',
34
+ docs: {
35
+ description:
36
+ 'Validate @enumType TSDoc tag usage on enums and string literal unions',
37
+ },
38
+ messages: {
39
+ missingValue: '@enumType tag requires a value of "open" or "closed"',
40
+ invalidValue:
41
+ '@enumType tag value "{{value}}" is invalid. Use "open" or "closed"',
42
+ multipleEnumTypes: 'Multiple @enumType tags found. Only one is allowed',
43
+ invalidConstruct:
44
+ '@enumType is only valid on enum declarations and string literal union type aliases',
45
+ missingEnumType:
46
+ 'Exported {{kind}} "{{name}}" is missing @enumType tag. Add @enumType open or @enumType closed',
47
+ },
48
+ schema: [
49
+ {
50
+ type: 'object',
51
+ properties: {
52
+ requireOnExported: {
53
+ type: 'boolean',
54
+ description:
55
+ 'Require @enumType on all exported enums and string literal unions',
56
+ },
57
+ },
58
+ additionalProperties: false,
59
+ },
60
+ ],
61
+ },
62
+ defaultOptions: [{}],
63
+ create(context) {
64
+ const options = context.options[0] ?? {}
65
+ const requireOnExported = options.requireOnExported ?? false
66
+
67
+ const sourceCode = context.sourceCode
68
+
69
+ /**
70
+ * Gets the TSDoc comment text for a node, checking both the node and its export parent.
71
+ */
72
+ function getTSDocComment(node: TSESTree.Node): string | undefined {
73
+ // First check the node itself
74
+ let commentText = getLeadingTSDocComment(sourceCode, node)
75
+ if (commentText) {
76
+ return commentText
77
+ }
78
+
79
+ // If node is inside an export, check the export statement
80
+ const parent = node.parent
81
+ if (
82
+ parent?.type === AST_NODE_TYPES.ExportNamedDeclaration ||
83
+ parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration
84
+ ) {
85
+ commentText = getLeadingTSDocComment(sourceCode, parent)
86
+ if (commentText) {
87
+ return commentText
88
+ }
89
+ }
90
+
91
+ return undefined
92
+ }
93
+
94
+ /**
95
+ * Checks if a node is exported.
96
+ */
97
+ function isExported(node: TSESTree.Node): boolean {
98
+ const parent = node.parent
99
+ if (parent?.type === AST_NODE_TYPES.ExportNamedDeclaration) {
100
+ return true
101
+ }
102
+ if (parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration) {
103
+ return true
104
+ }
105
+ return false
106
+ }
107
+
108
+ /**
109
+ * Checks if a type alias is a string literal union.
110
+ * A string literal union is a union type where all members are string literal types.
111
+ */
112
+ function isStringLiteralUnion(
113
+ typeAnnotation: TSESTree.TypeNode,
114
+ ): boolean {
115
+ // Check if it's a union type
116
+ if (typeAnnotation.type !== AST_NODE_TYPES.TSUnionType) {
117
+ // Could be a single string literal - that's also valid
118
+ return (
119
+ typeAnnotation.type === AST_NODE_TYPES.TSLiteralType &&
120
+ typeAnnotation.literal.type === AST_NODE_TYPES.Literal &&
121
+ typeof typeAnnotation.literal.value === 'string'
122
+ )
123
+ }
124
+
125
+ // All union members must be string literals
126
+ return typeAnnotation.types.every((member) => {
127
+ if (member.type === AST_NODE_TYPES.TSLiteralType) {
128
+ return (
129
+ member.literal.type === AST_NODE_TYPES.Literal &&
130
+ typeof member.literal.value === 'string'
131
+ )
132
+ }
133
+ return false
134
+ })
135
+ }
136
+
137
+ /**
138
+ * Validates `@enumType` usage on a node that should have it (enum or string literal union).
139
+ */
140
+ function validateEnumTypeTag(
141
+ node: TSESTree.TSEnumDeclaration | TSESTree.TSTypeAliasDeclaration,
142
+ kind: 'enum' | 'type',
143
+ ): void {
144
+ const commentText = getTSDocComment(node)
145
+ const name = node.id.name
146
+
147
+ if (!commentText) {
148
+ // No TSDoc comment - check if we should require `@enumType`
149
+ if (requireOnExported && isExported(node)) {
150
+ context.report({
151
+ node,
152
+ messageId: 'missingEnumType',
153
+ data: { kind, name },
154
+ })
155
+ }
156
+ return
157
+ }
158
+
159
+ const extraction = extractEnumType(commentText)
160
+
161
+ if (!extraction.found) {
162
+ // No @enumType tag - check if we should require it
163
+ if (requireOnExported && isExported(node)) {
164
+ context.report({
165
+ node,
166
+ messageId: 'missingEnumType',
167
+ data: { kind, name },
168
+ })
169
+ }
170
+ return
171
+ }
172
+
173
+ // Multiple @enumType tags
174
+ if (extraction.count > 1) {
175
+ context.report({
176
+ node,
177
+ messageId: 'multipleEnumTypes',
178
+ })
179
+ return
180
+ }
181
+
182
+ // @enumType without value
183
+ if (!extraction.rawValue) {
184
+ context.report({
185
+ node,
186
+ messageId: 'missingValue',
187
+ })
188
+ return
189
+ }
190
+
191
+ // Invalid value
192
+ if (!extraction.isValid) {
193
+ context.report({
194
+ node,
195
+ messageId: 'invalidValue',
196
+ data: { value: extraction.rawValue },
197
+ })
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Checks for `@enumType` on invalid constructs (non-enum, non-string-literal-union).
203
+ */
204
+ function checkInvalidEnumTypeUsage(node: TSESTree.Node): void {
205
+ const commentText = getTSDocComment(node)
206
+ if (!commentText) {
207
+ return
208
+ }
209
+
210
+ const extraction = extractEnumType(commentText)
211
+ if (extraction.found) {
212
+ context.report({
213
+ node,
214
+ messageId: 'invalidConstruct',
215
+ })
216
+ }
217
+ }
218
+
219
+ return {
220
+ // Check enum declarations - @enumType is valid here
221
+ TSEnumDeclaration(node): void {
222
+ validateEnumTypeTag(node, 'enum')
223
+ },
224
+
225
+ // Check type alias declarations
226
+ TSTypeAliasDeclaration(node): void {
227
+ if (isStringLiteralUnion(node.typeAnnotation)) {
228
+ // This is a string literal union - @enumType is valid
229
+ validateEnumTypeTag(node, 'type')
230
+ } else {
231
+ // Not a string literal union - @enumType is invalid
232
+ checkInvalidEnumTypeUsage(node)
233
+ }
234
+ },
235
+
236
+ // Check invalid constructs - @enumType should not be on these
237
+ FunctionDeclaration(node): void {
238
+ checkInvalidEnumTypeUsage(node)
239
+ },
240
+ ClassDeclaration(node): void {
241
+ checkInvalidEnumTypeUsage(node)
242
+ },
243
+ TSInterfaceDeclaration(node): void {
244
+ checkInvalidEnumTypeUsage(node)
245
+ },
246
+ VariableDeclaration(node): void {
247
+ checkInvalidEnumTypeUsage(node)
248
+ },
249
+ }
250
+ },
251
+ },
252
+ )