Skip to content

Commit e6cf6e4

Browse files
committed
chore: add eslint rule no-all-string-literal-unions to disallow all string literal unions
1 parent 53f4f62 commit e6cf6e4

File tree

4 files changed

+165
-2
lines changed

4 files changed

+165
-2
lines changed

specification/eslint.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export default defineConfig({
9494
]
9595
}
9696
}
97-
]
97+
],
98+
'es-spec-validator/no-all-string-literal-unions': 'error'
9899
}
99100
})

validator/eslint-plugin-es-spec.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import noVariantsOnResponses from './rules/no-variants-on-responses.js'
2626
import noInlineUnions from './rules/no-inline-unions.js'
2727
import preferTaggedVariants from './rules/prefer-tagged-variants.js'
2828
import noDuplicateTypeNames from './rules/no-duplicate-type-names.js'
29+
import noAllStringLiteralUnions from './rules/no-all-string-literal-unions.js'
2930

3031
export default {
3132
rules: {
@@ -38,6 +39,7 @@ export default {
3839
'no-variants-on-responses': noVariantsOnResponses,
3940
'no-inline-unions': noInlineUnions,
4041
'prefer-tagged-variants': preferTaggedVariants,
41-
'no-duplicate-type-names': noDuplicateTypeNames
42+
'no-duplicate-type-names': noDuplicateTypeNames,
43+
'no-all-string-literal-unions': noAllStringLiteralUnions
4244
}
4345
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import { ESLintUtils } from '@typescript-eslint/utils'
20+
import ts from 'typescript'
21+
22+
export const noAllStringLiteralUnions = ESLintUtils.RuleCreator.withoutDocs({
23+
name: 'no-all-string-literal-unions',
24+
meta: {
25+
type: 'problem',
26+
docs: {
27+
description: 'Disallow all string literal unions',
28+
recommended: 'error'
29+
},
30+
messages: {
31+
noAllStringLiteralUnions:
32+
'All string literal unions are not allowed. Use an enum of string literals instead (e.g., export enum MyEnum { A = "a", B = "b" })'
33+
},
34+
schema: []
35+
},
36+
defaultOptions: [],
37+
create(context) {
38+
const services = ESLintUtils.getParserServices(context)
39+
40+
function isStringLiteralType(type) {
41+
if (type.type !== 'TSLiteralType') {
42+
return false
43+
}
44+
45+
const tsNode = services.esTreeNodeToTSNodeMap.get(type)
46+
return tsNode.literal.kind === ts.SyntaxKind.StringLiteral
47+
}
48+
49+
return {
50+
TSUnionType(node) {
51+
const allMembersAreStringLiterals =
52+
node.types.every(isStringLiteralType)
53+
54+
if (allMembersAreStringLiterals) {
55+
context.report({
56+
node,
57+
messageId: 'noAllStringLiteralUnions'
58+
})
59+
}
60+
}
61+
}
62+
}
63+
})
64+
65+
export default noAllStringLiteralUnions
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import { RuleTester } from '@typescript-eslint/rule-tester'
20+
import noAllStringLiteralUnions from '../rules/no-all-string-literal-unions.js'
21+
22+
const ruleTester = new RuleTester({
23+
languageOptions: {
24+
parserOptions: {
25+
projectService: {
26+
allowDefaultProject: ['*.ts*']
27+
},
28+
tsconfigRootDir: import.meta.dirname
29+
}
30+
}
31+
})
32+
33+
const rule = noAllStringLiteralUnions
34+
35+
ruleTester.run('no-all-string-literal-unions', rule, {
36+
valid: [
37+
{
38+
name: 'enum',
39+
code: `enum MyEnum { foo, bar, baz }
40+
type MyDict = Dictionary<MyEnum, object>`
41+
},
42+
{
43+
name: 'type',
44+
code: `type MyType = "foo" | int`
45+
},
46+
{
47+
name: 'single string literal (not a union)',
48+
code: `type SingleValue = "foo"`
49+
},
50+
{
51+
name: 'union with null/undefined',
52+
code: `type MaybeString = "active" | null`
53+
},
54+
{
55+
name: 'union with number',
56+
code: `type StringOrNumber = "default" | number`
57+
},
58+
{
59+
name: 'number literal unions (should only catch string literals)',
60+
code: `type NumericUnion = 1 | 2 | 3`
61+
},
62+
{
63+
name: 'union with type reference',
64+
code: `type MyType = string; type Mixed = "literal" | MyType`
65+
}
66+
],
67+
invalid: [
68+
{
69+
name: 'all string literal union',
70+
code: `type MyType = "foo" | "bar" | "baz"`,
71+
errors: [{ messageId: 'noAllStringLiteralUnions' }]
72+
},
73+
{
74+
name: 'interface with string literal union',
75+
code: `export interface MyInterface {
76+
some?: "foo" | "bar" | "baz"
77+
other?: 'foo' | 'bar' | 'baz'
78+
}`,
79+
errors: [
80+
{ messageId: 'noAllStringLiteralUnions' },
81+
{ messageId: 'noAllStringLiteralUnions' }
82+
]
83+
},
84+
{
85+
name: 'function with string literal union',
86+
code: `function getStatus(): "pending" | "complete" { return "pending" }`,
87+
errors: [{ messageId: 'noAllStringLiteralUnions' }]
88+
},
89+
{
90+
name: 'class with string literal union',
91+
code: `class Config { status: "active" | "inactive" }`,
92+
errors: [{ messageId: 'noAllStringLiteralUnions' }]
93+
}
94+
]
95+
})

0 commit comments

Comments
 (0)