Skip to content

Commit 0543b66

Browse files
committed
Translate Record type aliases into hashes, Closes #47
1 parent 76f9929 commit 0543b66

5 files changed

Lines changed: 274 additions & 1 deletion

File tree

lib/parse/ParameterLoader.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,23 @@ import {
66
TypeElement,
77
TypeNode,
88
TSIndexSignature,
9+
TSTypeReference,
910
} from '@typescript-eslint/types/dist/ts-estree';
1011
import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
1112
import { ClassReference, ClassReferenceLoaded, InterfaceLoaded } from './ClassIndex';
1213
import { CommentData, CommentLoader } from './CommentLoader';
1314
import { ConstructorData } from './ConstructorLoader';
15+
import { TypeReferenceOverride } from './typereferenceoverride/TypeReferenceOverride';
16+
import { TypeReferenceOverrideAliasRecord } from './typereferenceoverride/TypeReferenceOverrideAliasRecord';
1417

1518
/**
1619
* Interprets class parameters of a given class.
1720
*/
1821
export class ParameterLoader {
22+
private static readonly typeReferenceOverrides: TypeReferenceOverride[] = [
23+
new TypeReferenceOverrideAliasRecord(),
24+
];
25+
1926
private readonly classLoaded: ClassReferenceLoaded;
2027
private readonly commentLoader: CommentLoader;
2128

@@ -168,6 +175,7 @@ export class ParameterLoader {
168175
} in ${this.classLoaded.localName} at ${this.classLoaded.fileName}`);
169176
}
170177

178+
let typeAliasOverride: ParameterRangeUnresolved | undefined;
171179
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
172180
switch (typeNode.type) {
173181
case AST_NODE_TYPES.TSTypeReference:
@@ -197,6 +205,12 @@ export class ParameterLoader {
197205
return this.getRangeFromTypeNode(genericProperties.type, errorIdentifier);
198206
}
199207

208+
// Check if this node is a predefined type alias
209+
typeAliasOverride = this.handleTypeOverride(typeNode);
210+
if (typeAliasOverride) {
211+
return typeAliasOverride;
212+
}
213+
200214
// Otherwise, assume we have an interface/class parameter
201215
return { type: 'interface', value: typeNode.typeName.name };
202216
}
@@ -304,6 +318,19 @@ export class ParameterLoader {
304318
throw new Error(`Missing field type on ${this.getErrorIdentifierIndex()
305319
} in ${this.classLoaded.localName} at ${this.classLoaded.fileName}`);
306320
}
321+
322+
/**
323+
* Iterate over all type reference override handler to see if one of them overrides the given type.
324+
* @param typeNode A type reference node.
325+
*/
326+
public handleTypeOverride(typeNode: TSTypeReference): ParameterRangeUnresolved | undefined {
327+
for (const typeReferenceOverride of ParameterLoader.typeReferenceOverrides) {
328+
const handled = typeReferenceOverride.handle(typeNode);
329+
if (handled) {
330+
return handled;
331+
}
332+
}
333+
}
307334
}
308335

309336
export interface ParameterLoaderArgs {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { TSTypeReference } from '@typescript-eslint/types/dist/ts-estree';
2+
import { ParameterRangeUnresolved } from '../ParameterLoader';
3+
4+
/**
5+
* Overrides how types should be converted to parameter ranges.
6+
*/
7+
export interface TypeReferenceOverride {
8+
/**
9+
* Convert a type node.
10+
* Returns undefined if this handler is not applicable.
11+
* @param typeNode A type node.
12+
*/
13+
handle(typeNode: TSTypeReference): ParameterRangeUnresolved | undefined;
14+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Range, SourceLocation, TSTypeLiteral, TSTypeReference } from '@typescript-eslint/types/dist/ts-estree';
2+
import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
3+
import { ParameterRangeUnresolved } from '../ParameterLoader';
4+
import { TypeReferenceOverride } from './TypeReferenceOverride';
5+
6+
/**
7+
* Converts type aliases of the form `Record<K, V>` into `{[k: K]: V}`.
8+
*/
9+
export class TypeReferenceOverrideAliasRecord implements TypeReferenceOverride {
10+
public handle(typeNode: TSTypeReference): ParameterRangeUnresolved | undefined {
11+
if (typeNode.typeName.type === AST_NODE_TYPES.Identifier &&
12+
typeNode.typeName.name === 'Record' &&
13+
typeNode.typeParameters &&
14+
typeNode.typeParameters.params.length === 2) {
15+
const loc: SourceLocation = { start: { line: 0, column: 0 }, end: { line: 0, column: 7 }};
16+
const range: Range = [ 0, 0 ];
17+
const typeLiteral: TSTypeLiteral = {
18+
type: AST_NODE_TYPES.TSTypeLiteral,
19+
members: [
20+
{
21+
type: AST_NODE_TYPES.TSIndexSignature,
22+
parameters: [
23+
{
24+
type: AST_NODE_TYPES.Identifier,
25+
name: 'key',
26+
typeAnnotation: {
27+
type: AST_NODE_TYPES.TSTypeAnnotation,
28+
typeAnnotation: typeNode.typeParameters.params[0],
29+
loc,
30+
range,
31+
},
32+
loc,
33+
range,
34+
},
35+
],
36+
typeAnnotation: {
37+
type: AST_NODE_TYPES.TSTypeAnnotation,
38+
typeAnnotation: typeNode.typeParameters.params[1],
39+
loc,
40+
range,
41+
},
42+
loc,
43+
range,
44+
},
45+
],
46+
loc,
47+
range,
48+
};
49+
return { type: 'hash', value: typeLiteral };
50+
}
51+
}
52+
}

test/parse/ParameterLoader.test.ts

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { MethodDefinition, TSTypeLiteral, Identifier, TSIndexSignature } from '@typescript-eslint/types/dist/ts-estree';
1+
import { MethodDefinition, TSTypeLiteral, Identifier, TSIndexSignature,
2+
TSTypeAnnotation, TypeNode, TSTypeReference } from '@typescript-eslint/types/dist/ts-estree';
23
import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
34
import { ClassReference, InterfaceLoaded } from '../../lib/parse/ClassIndex';
45
import { ClassLoader } from '../../lib/parse/ClassLoader';
@@ -962,6 +963,39 @@ export interface A{
962963
expect(() => parameterLoader.getFieldRange(field, {}))
963964
.toThrow(new Error('Found untyped generic field type at field fieldA in A at file'));
964965
});
966+
967+
it('should get the range of a Record', async() => {
968+
expect(await getFieldRange('fieldA: Record<string, number>', {}))
969+
.toMatchObject({
970+
type: 'hash',
971+
value: {
972+
members: [
973+
{
974+
parameters: [
975+
{
976+
name: 'key',
977+
type: 'Identifier',
978+
typeAnnotation: {
979+
type: 'TSTypeAnnotation',
980+
typeAnnotation: {
981+
type: 'TSStringKeyword',
982+
},
983+
},
984+
},
985+
],
986+
type: 'TSIndexSignature',
987+
typeAnnotation: {
988+
type: 'TSTypeAnnotation',
989+
typeAnnotation: {
990+
type: 'TSNumberKeyword',
991+
},
992+
},
993+
},
994+
],
995+
type: 'TSTypeLiteral',
996+
},
997+
});
998+
});
965999
});
9661000

9671001
describe('getFieldDefault', () => {
@@ -1077,4 +1111,58 @@ export interface A{
10771111
.rejects.toThrow(new Error('Missing field type on an index signature in A at file'));
10781112
});
10791113
});
1114+
1115+
describe('handleTypeOverride', () => {
1116+
const clazz: ClassReference = { localName: 'A', fileName: 'file' };
1117+
1118+
async function handleTypeOverride(type: string): Promise<ParameterRangeUnresolved | undefined> {
1119+
resolutionContext.contentsOverrides = {
1120+
'file.d.ts': `export class A{
1121+
constructor(a: ${type}) {}
1122+
}`,
1123+
};
1124+
const classLoaded = await classLoader.loadClassDeclaration(clazz, false);
1125+
const field: Identifier = <any> (<MethodDefinition> constructorLoader.getConstructor(classLoaded))
1126+
.value.params[0];
1127+
const parameterLoader = new ParameterLoader({ classLoaded });
1128+
const typeNode: TypeNode = (<TSTypeAnnotation> field.typeAnnotation).typeAnnotation;
1129+
return parameterLoader.handleTypeOverride(<TSTypeReference> typeNode);
1130+
}
1131+
1132+
it('should do nothing on an unsupported type', async() => {
1133+
expect(await handleTypeOverride('String')).toBeUndefined();
1134+
});
1135+
1136+
it('handle a Record type alias', async() => {
1137+
expect(await handleTypeOverride('Record<string, number>')).toMatchObject({
1138+
type: 'hash',
1139+
value: {
1140+
members: [
1141+
{
1142+
parameters: [
1143+
{
1144+
name: 'key',
1145+
type: 'Identifier',
1146+
typeAnnotation: {
1147+
type: 'TSTypeAnnotation',
1148+
typeAnnotation: {
1149+
type: 'TSStringKeyword',
1150+
},
1151+
},
1152+
},
1153+
],
1154+
type: 'TSIndexSignature',
1155+
typeAnnotation: {
1156+
type: 'TSTypeAnnotation',
1157+
typeAnnotation: {
1158+
type: 'TSNumberKeyword',
1159+
},
1160+
},
1161+
},
1162+
],
1163+
type: 'TSTypeLiteral',
1164+
},
1165+
});
1166+
});
1167+
});
10801168
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
2+
import {
3+
TypeReferenceOverrideAliasRecord,
4+
} from '../../../lib/parse/typereferenceoverride/TypeReferenceOverrideAliasRecord';
5+
6+
describe('TypeReferenceOverrideAliasRecord', () => {
7+
const handler: TypeReferenceOverrideAliasRecord = new TypeReferenceOverrideAliasRecord();
8+
9+
describe('handle', () => {
10+
it('should ignore non-identifiers', () => {
11+
const typeNode: any = {
12+
typeName: {
13+
type: 'unknown',
14+
},
15+
};
16+
expect(handler.handle(typeNode)).toBeUndefined();
17+
});
18+
19+
it('should ignore non-Record', () => {
20+
const typeNode: any = {
21+
typeName: {
22+
type: AST_NODE_TYPES.Identifier,
23+
name: 'NonRecord',
24+
},
25+
};
26+
expect(handler.handle(typeNode)).toBeUndefined();
27+
});
28+
29+
it('should ignore Record without typeParameters', () => {
30+
const typeNode: any = {
31+
typeName: {
32+
type: AST_NODE_TYPES.Identifier,
33+
name: 'Record',
34+
},
35+
};
36+
expect(handler.handle(typeNode)).toBeUndefined();
37+
});
38+
39+
it('should ignore Record with typeParameters of wrong length', () => {
40+
const typeNode: any = {
41+
typeName: {
42+
type: AST_NODE_TYPES.Identifier,
43+
name: 'Record',
44+
},
45+
typeParameters: {
46+
params: [],
47+
},
48+
};
49+
expect(handler.handle(typeNode)).toBeUndefined();
50+
});
51+
52+
it('should handle Record with typeParameters of length 2', () => {
53+
const typeNode: any = {
54+
typeName: {
55+
type: AST_NODE_TYPES.Identifier,
56+
name: 'Record',
57+
},
58+
typeParameters: {
59+
params: [
60+
'TYPE0',
61+
'TYPE1',
62+
],
63+
},
64+
};
65+
expect(handler.handle(typeNode)).toMatchObject({
66+
type: 'hash',
67+
value: {
68+
type: AST_NODE_TYPES.TSTypeLiteral,
69+
members: [
70+
{
71+
type: AST_NODE_TYPES.TSIndexSignature,
72+
parameters: [
73+
{
74+
type: AST_NODE_TYPES.Identifier,
75+
name: 'key',
76+
typeAnnotation: {
77+
type: AST_NODE_TYPES.TSTypeAnnotation,
78+
typeAnnotation: 'TYPE0',
79+
},
80+
},
81+
],
82+
typeAnnotation: {
83+
type: AST_NODE_TYPES.TSTypeAnnotation,
84+
typeAnnotation: 'TYPE1',
85+
},
86+
},
87+
],
88+
},
89+
});
90+
});
91+
});
92+
});

0 commit comments

Comments
 (0)