Skip to content

Commit 6489293

Browse files
authored
fix(eslint-plugin): [no-type-alias] Fix parenthesized type handling (#576)
1 parent 31d5bd4 commit 6489293

File tree

2 files changed

+88
-99
lines changed

2 files changed

+88
-99
lines changed

packages/eslint-plugin/src/rules/no-type-alias.ts

+84-99
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {
22
AST_NODE_TYPES,
3-
TSESLint,
43
TSESTree,
54
} from '@typescript-eslint/experimental-utils';
65
import * as util from '../util';
@@ -30,6 +29,14 @@ type Options = [
3029
];
3130
type MessageIds = 'noTypeAlias' | 'noCompositionAlias';
3231

32+
type CompositionType =
33+
| AST_NODE_TYPES.TSUnionType
34+
| AST_NODE_TYPES.TSIntersectionType;
35+
interface TypeWithLabel {
36+
node: TSESTree.Node;
37+
compositionType: CompositionType | null;
38+
}
39+
3340
export default util.createRule<Options, MessageIds>({
3441
name: 'no-type-alias',
3542
meta: {
@@ -106,24 +113,13 @@ export default util.createRule<Options, MessageIds>({
106113
'in-intersections',
107114
'in-unions-and-intersections',
108115
];
109-
const aliasTypes = [
116+
const aliasTypes = new Set([
110117
AST_NODE_TYPES.TSArrayType,
111118
AST_NODE_TYPES.TSTypeReference,
112119
AST_NODE_TYPES.TSLiteralType,
113120
AST_NODE_TYPES.TSTypeQuery,
114-
];
115-
116-
type CompositionType = TSESTree.TSUnionType | TSESTree.TSIntersectionType;
117-
/**
118-
* Determines if the given node is a union or an intersection.
119-
*/
120-
function isComposition(node: TSESTree.TypeNode): node is CompositionType {
121-
return (
122-
node &&
123-
(node.type === AST_NODE_TYPES.TSUnionType ||
124-
node.type === AST_NODE_TYPES.TSIntersectionType)
125-
);
126-
}
121+
AST_NODE_TYPES.TSIndexedAccessType,
122+
]);
127123

128124
/**
129125
* Determines if the composition type is supported by the allowed flags.
@@ -133,7 +129,7 @@ export default util.createRule<Options, MessageIds>({
133129
*/
134130
function isSupportedComposition(
135131
isTopLevel: boolean,
136-
compositionType: string | undefined,
132+
compositionType: CompositionType | null,
137133
allowed: string,
138134
): boolean {
139135
return (
@@ -146,43 +142,6 @@ export default util.createRule<Options, MessageIds>({
146142
);
147143
}
148144

149-
/**
150-
* Determines if the given node is an alias type (keywords, arrays, type references and constants).
151-
* @param node the node to be evaluated.
152-
*/
153-
function isAlias(
154-
node: TSESTree.Node,
155-
): boolean /* not worth enumerating the ~25 individual types here */ {
156-
return (
157-
node &&
158-
(/Keyword$/.test(node.type) || aliasTypes.indexOf(node.type) > -1)
159-
);
160-
}
161-
162-
/**
163-
* Determines if the given node is a callback type.
164-
* @param node the node to be evaluated.
165-
*/
166-
function isCallback(node: TSESTree.Node): node is TSESTree.TSFunctionType {
167-
return node && node.type === AST_NODE_TYPES.TSFunctionType;
168-
}
169-
170-
/**
171-
* Determines if the given node is a literal type (objects).
172-
* @param node the node to be evaluated.
173-
*/
174-
function isLiteral(node: TSESTree.Node): node is TSESTree.TSTypeLiteral {
175-
return node && node.type === AST_NODE_TYPES.TSTypeLiteral;
176-
}
177-
178-
/**
179-
* Determines if the given node is a mapped type.
180-
* @param node the node to be evaluated.
181-
*/
182-
function isMappedType(node: TSESTree.Node): node is TSESTree.TSMappedType {
183-
return node && node.type === AST_NODE_TYPES.TSMappedType;
184-
}
185-
186145
/**
187146
* Gets the message to be displayed based on the node type and whether the node is a top level declaration.
188147
* @param node the location
@@ -191,108 +150,134 @@ export default util.createRule<Options, MessageIds>({
191150
* @param isRoot a flag indicating we are dealing with the top level declaration.
192151
* @param type the kind of type alias being validated.
193152
*/
194-
function getMessage(
153+
function reportError(
195154
node: TSESTree.Node,
196-
compositionType: string | undefined,
155+
compositionType: CompositionType | null,
197156
isRoot: boolean,
198-
type?: string,
199-
): TSESLint.ReportDescriptor<MessageIds> {
157+
type: string,
158+
): void {
200159
if (isRoot) {
201-
return {
160+
return context.report({
202161
node,
203162
messageId: 'noTypeAlias',
204163
data: {
205-
alias: type || 'aliases',
164+
alias: type.toLowerCase(),
206165
},
207-
};
166+
});
208167
}
209168

210-
return {
169+
return context.report({
211170
node,
212171
messageId: 'noCompositionAlias',
213172
data: {
214173
compositionType:
215174
compositionType === AST_NODE_TYPES.TSUnionType
216175
? 'union'
217176
: 'intersection',
218-
typeName: util.upperCaseFirst(type!),
177+
typeName: type,
219178
},
220-
};
179+
});
221180
}
222181

223182
/**
224183
* Validates the node looking for aliases, callbacks and literals.
225184
* @param node the node to be validated.
226-
* @param isTopLevel a flag indicating this is the top level node.
227-
* @param compositionType the type of composition this alias is part of (undefined if not
185+
* @param compositionType the type of composition this alias is part of (null if not
228186
* part of a composition)
187+
* @param isTopLevel a flag indicating this is the top level node.
229188
*/
230189
function validateTypeAliases(
231-
node: TSESTree.Node,
232-
isTopLevel: boolean,
233-
compositionType?: string,
190+
type: TypeWithLabel,
191+
isTopLevel: boolean = false,
234192
): void {
235-
if (isCallback(node)) {
193+
if (type.node.type === AST_NODE_TYPES.TSFunctionType) {
194+
// callback
236195
if (allowCallbacks === 'never') {
237-
context.report(
238-
getMessage(node, compositionType, isTopLevel, 'callbacks'),
239-
);
196+
reportError(type.node, type.compositionType, isTopLevel, 'Callbacks');
240197
}
241-
} else if (isLiteral(node)) {
198+
} else if (type.node.type === AST_NODE_TYPES.TSTypeLiteral) {
199+
// literal object type
242200
if (
243201
allowLiterals === 'never' ||
244-
!isSupportedComposition(isTopLevel, compositionType, allowLiterals!)
202+
!isSupportedComposition(
203+
isTopLevel,
204+
type.compositionType,
205+
allowLiterals!,
206+
)
245207
) {
246-
context.report(
247-
getMessage(node, compositionType, isTopLevel, 'literals'),
248-
);
208+
reportError(type.node, type.compositionType, isTopLevel, 'Literals');
249209
}
250-
} else if (isMappedType(node)) {
210+
} else if (type.node.type === AST_NODE_TYPES.TSMappedType) {
211+
// mapped type
251212
if (
252213
allowMappedTypes === 'never' ||
253214
!isSupportedComposition(
254215
isTopLevel,
255-
compositionType,
216+
type.compositionType,
256217
allowMappedTypes!,
257218
)
258219
) {
259-
context.report(
260-
getMessage(node, compositionType, isTopLevel, 'mapped types'),
220+
reportError(
221+
type.node,
222+
type.compositionType,
223+
isTopLevel,
224+
'Mapped types',
261225
);
262226
}
263-
} else if (isAlias(node)) {
227+
} else if (
228+
/Keyword$/.test(type.node.type) ||
229+
aliasTypes.has(type.node.type)
230+
) {
231+
// alias / keyword
264232
if (
265233
allowAliases === 'never' ||
266-
!isSupportedComposition(isTopLevel, compositionType, allowAliases!)
234+
!isSupportedComposition(
235+
isTopLevel,
236+
type.compositionType,
237+
allowAliases!,
238+
)
267239
) {
268-
context.report(
269-
getMessage(node, compositionType, isTopLevel, 'aliases'),
270-
);
240+
reportError(type.node, type.compositionType, isTopLevel, 'Aliases');
271241
}
272242
} else {
273-
context.report(getMessage(node, compositionType, isTopLevel));
243+
// unhandled type - shouldn't happen
244+
reportError(type.node, type.compositionType, isTopLevel, 'Unhandled');
274245
}
275246
}
276247

277248
/**
278-
* Validates compositions (unions and/or intersections).
249+
* Flatten the given type into an array of its dependencies
279250
*/
280-
function validateCompositions(node: CompositionType): void {
281-
node.types.forEach(type => {
282-
if (isComposition(type)) {
283-
validateCompositions(type);
284-
} else {
285-
validateTypeAliases(type, false, node.type);
286-
}
287-
});
251+
function getTypes(
252+
node: TSESTree.Node,
253+
compositionType: CompositionType | null = null,
254+
): TypeWithLabel[] {
255+
if (
256+
node.type === AST_NODE_TYPES.TSUnionType ||
257+
node.type === AST_NODE_TYPES.TSIntersectionType
258+
) {
259+
return node.types.reduce<TypeWithLabel[]>((acc, type) => {
260+
acc.push(...getTypes(type, node.type));
261+
return acc;
262+
}, []);
263+
}
264+
if (node.type === AST_NODE_TYPES.TSParenthesizedType) {
265+
return getTypes(node.typeAnnotation, compositionType);
266+
}
267+
return [{ node, compositionType }];
288268
}
289269

290270
return {
291271
TSTypeAliasDeclaration(node) {
292-
if (isComposition(node.typeAnnotation)) {
293-
validateCompositions(node.typeAnnotation);
272+
const types = getTypes(node.typeAnnotation);
273+
if (types.length === 1) {
274+
// is a top level type annotation
275+
validateTypeAliases(types[0], true);
294276
} else {
295-
validateTypeAliases(node.typeAnnotation, true);
277+
// is a composition type
278+
types.forEach(type => {
279+
validateTypeAliases(type);
280+
});
296281
}
297282
},
298283
};

packages/eslint-plugin/tests/rules/no-type-alias.test.ts

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ const ruleTester = new RuleTester({
77

88
ruleTester.run('no-type-alias', rule, {
99
valid: [
10+
{
11+
code: "type A = 'a' & ('b' | 'c');",
12+
options: [{ allowAliases: 'always' }],
13+
},
1014
{
1115
code: "type Foo = 'a';",
1216
options: [{ allowAliases: 'always' }],

0 commit comments

Comments
 (0)