Skip to content

Commit bc15b19

Browse files
committed
Support keyof typeof enum as union of enum keys
1 parent 458d2de commit bc15b19

7 files changed

Lines changed: 181 additions & 2 deletions

File tree

lib/parse/ParameterLoader.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,22 @@ export class ParameterLoader {
423423
value: this.getRangeFromTypeNode(classLoaded, typeNode.typeAnnotation, errorIdentifier),
424424
};
425425
}
426+
break;
427+
case AST_NODE_TYPES.TSTypeQuery:
428+
if (typeNode.exprName.type === AST_NODE_TYPES.Identifier) {
429+
return {
430+
type: 'typeof',
431+
value: typeNode.exprName.name,
432+
origin: classLoaded,
433+
};
434+
}
435+
// Otherwise we have a qualified name: AST_NODE_TYPES.TSQualifiedName
436+
return {
437+
type: 'typeof',
438+
value: typeNode.exprName.right.name,
439+
qualifiedPath: this.getQualifiedPath(typeNode.exprName.left),
440+
origin: classLoaded,
441+
};
426442
}
427443
this.throwOrWarn(new Error(`Could not understand parameter type ${typeNode.type} of ${errorIdentifier
428444
} in ${classLoaded.localName} at ${classLoaded.fileName}`));
@@ -514,6 +530,7 @@ export class ParameterLoader {
514530
case 'hash':
515531
case 'interface':
516532
case 'genericTypeReference':
533+
case 'typeof':
517534
// Replace these types
518535
return override;
519536
case 'undefined':
@@ -775,6 +792,17 @@ export type ParameterRangeUnresolved = {
775792
} | {
776793
type: 'genericTypeReference';
777794
value: string;
795+
} | {
796+
type: 'typeof';
797+
value: string;
798+
/**
799+
* For qualified names, this array contains the path segments.
800+
*/
801+
qualifiedPath?: string[];
802+
/**
803+
* The place from which the interface was referenced.
804+
*/
805+
origin: ClassReferenceLoaded;
778806
};
779807

780808
export type ParameterRangeResolved = {
@@ -822,6 +850,9 @@ export type ParameterRangeResolved = {
822850
* The place in which the generic type was defined.
823851
*/
824852
origin: ClassReferenceLoaded;
853+
} | {
854+
type: 'typeof';
855+
value: ParameterRangeResolved;
825856
};
826857

827858
/**

lib/parse/ParameterResolver.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { TSTypeLiteral } from '@typescript-eslint/types/dist/ts-estree';
1+
import type { TSTypeLiteral, PropertyNameNonComputed } from '@typescript-eslint/types/dist/ts-estree';
2+
23
import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
34
import * as LRUCache from 'lru-cache';
45
import type {
@@ -260,6 +261,35 @@ export class ParameterResolver {
260261
case 'array':
261262
case 'rest':
262263
case 'keyof':
264+
// Special case: if we have a `keyof typeof Enum`, return a union of the keys of the enum
265+
if (range.value.type === 'typeof') {
266+
const classOrInterface = await this.loadClassOrInterfacesChain({
267+
packageName: owningClass.packageName,
268+
localName: range.value.value,
269+
qualifiedPath: range.value.qualifiedPath,
270+
fileName: owningClass.fileName,
271+
fileNameReferenced: owningClass.fileNameReferenced,
272+
});
273+
274+
if (classOrInterface.type === 'enum') {
275+
const enumRangeTypes: ParameterRangeResolved[] = await Promise.all(classOrInterface.declaration.members
276+
// eslint-disable-next-line array-callback-return
277+
.map((enumMember, i) => {
278+
const key = <PropertyNameNonComputed> enumMember.id;
279+
switch (key.type) {
280+
case AST_NODE_TYPES.Literal:
281+
return { type: 'literal', value: key.value };
282+
case AST_NODE_TYPES.Identifier:
283+
return { type: 'literal', value: key.name };
284+
}
285+
}));
286+
return {
287+
type: 'union',
288+
elements: enumRangeTypes,
289+
};
290+
}
291+
}
292+
263293
return {
264294
type: range.type,
265295
// TODO: remove the following any cast when TS bug is fixed
@@ -281,6 +311,8 @@ export class ParameterResolver {
281311
value: range.value,
282312
origin: owningClass,
283313
};
314+
case 'typeof':
315+
throw new Error(`Detected typeof of unsupported value ${range.value} in ${owningClass.fileName}`);
284316
}
285317
}
286318

@@ -299,6 +331,7 @@ export class ParameterResolver {
299331
case 'raw':
300332
case 'literal':
301333
case 'override':
334+
case 'typeof':
302335
return `${range.type}:${range.value}`;
303336
case 'union':
304337
case 'intersection':

lib/resolution/ExternalModulesLoader.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export class ExternalModulesLoader {
117117
case 'rest':
118118
case 'array':
119119
case 'keyof':
120+
case 'typeof':
120121
this.indexParameterRangeInExternalPackage(parameterRange.value, externalPackages);
121122
break;
122123
}

lib/serialize/ComponentConstructor.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,7 @@ export class ComponentConstructor {
763763
case 'rest':
764764
case 'array':
765765
case 'keyof':
766+
case 'typeof':
766767
switch (range.type) {
767768
case 'rest':
768769
type = 'ParameterRangeRest';
@@ -773,6 +774,9 @@ export class ComponentConstructor {
773774
case 'keyof':
774775
type = 'ParameterRangeKeyof';
775776
break;
777+
case 'typeof':
778+
type = 'ParameterRangeTypeof';
779+
break;
776780
}
777781
return {
778782
'@type': <any> type,

test/parse/ParameterLoader.test.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1981,7 +1981,24 @@ export interface A{
19811981
.rejects.toThrow(new Error(`Could not understand parameter type TSTypeOperator of field fieldA in A at file`));
19821982
});
19831983

1984-
// TODO
1984+
it('should get the range of a typeof field type', async() => {
1985+
expect(await getFieldRange('fieldA: typeof MyClass', {}))
1986+
.toEqual({
1987+
type: 'typeof',
1988+
value: 'MyClass',
1989+
origin: expect.anything(),
1990+
});
1991+
});
1992+
1993+
it('should get the range of a typeof field type with qualified path', async() => {
1994+
expect(await getFieldRange('fieldA: typeof A.B.MyClass', {}))
1995+
.toEqual({
1996+
type: 'typeof',
1997+
value: 'MyClass',
1998+
qualifiedPath: [ 'A', 'B' ],
1999+
origin: expect.anything(),
2000+
});
2001+
});
19852002
});
19862003

19872004
describe('overrideRawRange', () => {
@@ -2065,6 +2082,23 @@ export interface A{
20652082
});
20662083
});
20672084

2085+
it('should override a typeof range', () => {
2086+
expect(loader.overrideRawRange(
2087+
{
2088+
type: 'typeof',
2089+
value: 'T',
2090+
origin: classLoadedDummy,
2091+
},
2092+
{
2093+
type: 'raw',
2094+
value: 'boolean',
2095+
},
2096+
)).toEqual({
2097+
type: 'raw',
2098+
value: 'boolean',
2099+
});
2100+
});
2101+
20682102
it('should override a hash range', () => {
20692103
expect(loader.overrideRawRange(
20702104
{

test/parse/ParameterResolver.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,14 @@ describe('ParameterResolver', () => {
631631
.toEqual('keyof:[raw:boolean]');
632632
});
633633

634+
it('should hash typeof', () => {
635+
expect(loader.hashParameterRangeUnresolved(<any> {
636+
type: 'typeof',
637+
value: 'CLASS',
638+
}))
639+
.toEqual('typeof:CLASS');
640+
});
641+
634642
it('should hash hash', () => {
635643
expect(loader.hashParameterRangeUnresolved({
636644
type: 'hash',
@@ -1301,6 +1309,62 @@ class MyInnerClass<AInner, BInner> {
13011309
});
13021310
});
13031311

1312+
it('should handle a keyof range over a typeof over an enum', async() => {
1313+
resolutionContext.contentsOverrides = {
1314+
'A.d.ts': `export * from './MyEnum'`,
1315+
'MyEnum.d.ts': `export enum MyEnum {
1316+
keya = 'valuea',
1317+
'keyb' = 'valueb',
1318+
}`,
1319+
};
1320+
1321+
expect(await loader.resolveRange({
1322+
type: 'keyof',
1323+
value: {
1324+
type: 'typeof',
1325+
value: 'MyEnum',
1326+
origin: classReference,
1327+
},
1328+
}, classReference, {}, true, new Set())).toMatchObject({
1329+
type: 'union',
1330+
elements: [
1331+
{ type: 'literal', value: 'keya' },
1332+
{ type: 'literal', value: 'keyb' },
1333+
],
1334+
});
1335+
});
1336+
1337+
it('should throw on a keyof range over a typeof over a non-enum', async() => {
1338+
resolutionContext.contentsOverrides = {
1339+
'A.d.ts': `export * from './MyClass'`,
1340+
'MyClass.d.ts': `export class MyClass {}`,
1341+
};
1342+
1343+
await expect(loader.resolveRange({
1344+
type: 'keyof',
1345+
value: {
1346+
type: 'typeof',
1347+
value: 'MyClass',
1348+
origin: classReference,
1349+
},
1350+
}, classReference, {}, true, new Set())).rejects
1351+
.toThrowError(`Detected typeof of unsupported value MyClass in A`);
1352+
});
1353+
1354+
it('should throw on a typeof range', async() => {
1355+
resolutionContext.contentsOverrides = {
1356+
'A.d.ts': `export * from './MyClass'`,
1357+
'MyClass.d.ts': `export class MyClass{}`,
1358+
};
1359+
1360+
await expect(loader.resolveRange({
1361+
type: 'typeof',
1362+
value: 'MyClass',
1363+
origin: classReference,
1364+
}, classReference, {}, true, new Set())).rejects
1365+
.toThrowError(`Detected typeof of unsupported value MyClass in A`);
1366+
});
1367+
13041368
it('should handle an interface recursively pointing to itself', async() => {
13051369
resolutionContext.contentsOverrides = {
13061370
'A.d.ts': `export interface MyInterface { field: MyInterface; }`,

test/serialize/ComponentConstructor.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2636,6 +2636,18 @@ describe('ComponentConstructor', () => {
26362636
parameterRangeValue: 'xsd:boolean',
26372637
});
26382638
});
2639+
2640+
it('should construct a typeof parameter range', async() => {
2641+
expect(await ctor.constructParameterRange(
2642+
{ type: 'typeof', value: { type: 'raw', value: 'boolean' }},
2643+
context,
2644+
externalContextsCallback,
2645+
'mp:a/b/file-param#MyClass_field',
2646+
)).toEqual({
2647+
'@type': 'ParameterRangeTypeof',
2648+
parameterRangeValue: 'xsd:boolean',
2649+
});
2650+
});
26392651
});
26402652

26412653
describe('populateOptionalParameterFields', () => {

0 commit comments

Comments
 (0)