Skip to content

Commit 9848655

Browse files
committed
Handle indexed hashes as collectEntries parameters, Closes #43
1 parent caaaf38 commit 9848655

10 files changed

Lines changed: 1048 additions & 58 deletions

lib/parse/CommentLoader.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ClassDeclaration, TSInterfaceDeclaration,
2-
MethodDefinition, TSPropertySignature, BaseNode } from '@typescript-eslint/types/dist/ts-estree';
2+
MethodDefinition, TSPropertySignature, TSIndexSignature, BaseNode } from '@typescript-eslint/types/dist/ts-estree';
33
import * as commentParse from 'comment-parser';
44
import { ClassReference, ClassReferenceLoaded } from './ClassIndex';
55
import { ParameterRangeUnresolved } from './ParameterLoader';
@@ -51,7 +51,7 @@ export class CommentLoader {
5151
* Extract comment data from the given field.
5252
* @param field A field.
5353
*/
54-
public getCommentDataFromField(field: TSPropertySignature): CommentData {
54+
public getCommentDataFromField(field: TSPropertySignature | TSIndexSignature): CommentData {
5555
const comment = this.getCommentRaw(field);
5656
if (comment) {
5757
return CommentLoader.getCommentDataFromComment(comment, this.classLoaded);

lib/parse/ConstructorLoader.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ClassDeclaration, MethodDefinition } from '@typescript-eslint/types/dist/ts-estree';
22
import { AST, TSESTreeOptions, AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
33
import { ClassIndex, ClassLoaded } from './ClassIndex';
4-
import { ParameterData, ParameterLoader, ParameterRangeUnresolved } from './ParameterLoader';
4+
import { ParameterDataField, ParameterLoader, ParameterRangeUnresolved } from './ParameterLoader';
55

66
/**
77
* Loads the constructor data of classes.
@@ -89,5 +89,5 @@ export class ConstructorLoader {
8989
* Constructor parameter information
9090
*/
9191
export interface ConstructorData<R> {
92-
parameters: ParameterData<R>[];
92+
parameters: ParameterDataField<R>[];
9393
}

lib/parse/ParameterLoader.ts

Lines changed: 144 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
import { MethodDefinition, TypeElement,
2-
Identifier, TSTypeLiteral, TSPropertySignature, TypeNode } from '@typescript-eslint/types/dist/ts-estree';
1+
import {
2+
Identifier,
3+
MethodDefinition,
4+
TSPropertySignature,
5+
TSTypeLiteral,
6+
TypeElement,
7+
TypeNode,
8+
TSIndexSignature,
9+
} from '@typescript-eslint/types/dist/ts-estree';
310
import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
411
import { ClassReference, ClassReferenceLoaded, InterfaceLoaded } from './ClassIndex';
512
import { CommentData, CommentLoader } from './CommentLoader';
@@ -26,7 +33,7 @@ export class ParameterLoader {
2633
const constructorCommentData = this.commentLoader.getCommentDataFromConstructor(constructor);
2734

2835
// Load all constructor parameters
29-
const parameters: ParameterData<ParameterRangeUnresolved>[] = [];
36+
const parameters: ParameterDataField<ParameterRangeUnresolved>[] = [];
3037
for (const field of constructor.value.params) {
3138
if (field.type === AST_NODE_TYPES.Identifier) {
3239
const commentData = constructorCommentData[field.name] || {};
@@ -74,6 +81,12 @@ export class ParameterLoader {
7481
return this.loadField(typeElement, commentData);
7582
}
7683
return undefined;
84+
case AST_NODE_TYPES.TSIndexSignature:
85+
commentData = this.commentLoader.getCommentDataFromField(typeElement);
86+
if (!commentData.ignored) {
87+
return this.loadIndex(typeElement, commentData);
88+
}
89+
return undefined;
7790
default:
7891
throw new Error(`Unsupported field type ${typeElement.type} in ${this.classLoaded.localName} in ${this.classLoaded.fileName}`);
7992
}
@@ -85,9 +98,10 @@ export class ParameterLoader {
8598
* @param commentData Comment data about the given field.
8699
*/
87100
public loadField(field: Identifier | TSPropertySignature, commentData: CommentData):
88-
ParameterData<ParameterRangeUnresolved> {
101+
ParameterDataField<ParameterRangeUnresolved> {
89102
// Required data
90-
const parameterData: ParameterData<ParameterRangeUnresolved> = {
103+
const parameterData: ParameterDataField<ParameterRangeUnresolved> = {
104+
type: 'field',
91105
name: this.getFieldName(field),
92106
unique: this.isFieldUnique(field),
93107
required: this.isFieldRequired(field),
@@ -120,19 +134,37 @@ export class ParameterLoader {
120134
throw new Error(`Unsupported field key type ${field.key.type} in interface ${this.classLoaded.localName} in ${this.classLoaded.fileName}`);
121135
}
122136

137+
public isFieldIndexedHash(field: Identifier | TSPropertySignature): boolean {
138+
return Boolean(field.typeAnnotation &&
139+
field.typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSTypeLiteral &&
140+
field.typeAnnotation.typeAnnotation.members.some(member => member.type === AST_NODE_TYPES.TSIndexSignature));
141+
}
142+
123143
public isFieldUnique(field: Identifier | TSPropertySignature): boolean {
124-
return !(field.typeAnnotation && field.typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSArrayType);
144+
return !(field.typeAnnotation && field.typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSArrayType) &&
145+
!this.isFieldIndexedHash(field);
125146
}
126147

127148
public isFieldRequired(field: Identifier | TSPropertySignature): boolean {
128-
return !field.optional;
149+
return !field.optional && !this.isFieldIndexedHash(field);
150+
}
151+
152+
public getErrorIdentifierField(field: Identifier | TSPropertySignature): string {
153+
return `field ${this.getFieldName(field)}`;
129154
}
130155

131-
public getRangeFromTypeNode(typeNode: TypeNode, field: Identifier | TSPropertySignature, nestedArrays = 0):
132-
ParameterRangeUnresolved {
156+
public getErrorIdentifierIndex(): string {
157+
return `an index signature`;
158+
}
159+
160+
public getRangeFromTypeNode(
161+
typeNode: TypeNode,
162+
errorIdentifier: string,
163+
nestedArrays = 0,
164+
): ParameterRangeUnresolved {
133165
// Don't allow arrays to be nested
134166
if (nestedArrays > 1) {
135-
throw new Error(`Detected illegal nested array type for field ${this.getFieldName(field)
167+
throw new Error(`Detected illegal nested array type for ${errorIdentifier
136168
} in ${this.classLoaded.localName} at ${this.classLoaded.fileName}`);
137169
}
138170

@@ -150,19 +182,19 @@ export class ParameterLoader {
150182
return { type: 'raw', value: 'string' };
151183
case 'Array':
152184
if (typeNode.typeParameters && typeNode.typeParameters.params.length === 1) {
153-
return this.getRangeFromTypeNode(typeNode.typeParameters.params[0], field, nestedArrays + 1);
185+
return this.getRangeFromTypeNode(typeNode.typeParameters.params[0], errorIdentifier, nestedArrays + 1);
154186
}
155-
throw new Error(`Found invalid Array field type at ${this.getFieldName(field)
187+
throw new Error(`Found invalid Array field type at ${errorIdentifier
156188
} in ${this.classLoaded.localName} at ${this.classLoaded.fileName}`);
157189
default:
158190
// First check if the type is be a generic type
159191
if (typeNode.typeName.name in this.classLoaded.generics) {
160192
const genericProperties = this.classLoaded.generics[typeNode.typeName.name];
161193
if (!genericProperties.type) {
162-
throw new Error(`Found untyped generic field type at ${this.getFieldName(field)
194+
throw new Error(`Found untyped generic field type at ${errorIdentifier
163195
} in ${this.classLoaded.localName} at ${this.classLoaded.fileName}`);
164196
}
165-
return this.getRangeFromTypeNode(genericProperties.type, field);
197+
return this.getRangeFromTypeNode(genericProperties.type, errorIdentifier);
166198
}
167199

168200
// Otherwise, assume we have an interface/class parameter
@@ -171,7 +203,7 @@ export class ParameterLoader {
171203
}
172204
break;
173205
case AST_NODE_TYPES.TSArrayType:
174-
return this.getRangeFromTypeNode(typeNode.elementType, field, nestedArrays + 1);
206+
return this.getRangeFromTypeNode(typeNode.elementType, errorIdentifier, nestedArrays + 1);
175207
case AST_NODE_TYPES.TSBooleanKeyword:
176208
return { type: 'raw', value: 'boolean' };
177209
case AST_NODE_TYPES.TSNumberKeyword:
@@ -181,7 +213,7 @@ export class ParameterLoader {
181213
case AST_NODE_TYPES.TSTypeLiteral:
182214
return { type: 'hash', value: typeNode };
183215
}
184-
throw new Error(`Could not understand parameter type ${typeNode.type} of field ${this.getFieldName(field)
216+
throw new Error(`Could not understand parameter type ${typeNode.type} of ${errorIdentifier
185217
} in ${this.classLoaded.localName} at ${this.classLoaded.fileName}`);
186218
}
187219

@@ -193,7 +225,7 @@ export class ParameterLoader {
193225

194226
// Check the typescript raw field type
195227
if (field.typeAnnotation) {
196-
return this.getRangeFromTypeNode(field.typeAnnotation.typeAnnotation, field);
228+
return this.getRangeFromTypeNode(field.typeAnnotation.typeAnnotation, this.getErrorIdentifierField(field));
197229
}
198230

199231
throw new Error(`Missing field type on ${this.getFieldName(field)
@@ -207,13 +239,84 @@ export class ParameterLoader {
207239
public getFieldComment(commentData: CommentData): string | undefined {
208240
return commentData.description;
209241
}
242+
243+
/**
244+
* Load the parameter data from the given index signature.
245+
* @param indexSignature An index signature.
246+
* @param commentData Comment data about the given field.
247+
*/
248+
public loadIndex(indexSignature: TSIndexSignature, commentData: CommentData):
249+
ParameterDataIndex<ParameterRangeUnresolved> {
250+
// Required data
251+
const parameterData: ParameterDataIndex<ParameterRangeUnresolved> = {
252+
type: 'index',
253+
domain: this.getIndexDomain(indexSignature),
254+
range: this.getIndexRange(indexSignature, commentData),
255+
};
256+
257+
// Optional data
258+
const defaultValue = this.getFieldDefault(commentData);
259+
if (defaultValue) {
260+
parameterData.default = defaultValue;
261+
}
262+
263+
const comment = this.getFieldComment(commentData);
264+
if (comment) {
265+
parameterData.comment = comment;
266+
}
267+
268+
return parameterData;
269+
}
270+
271+
public getIndexDomain(indexSignature: TSIndexSignature): 'string' | 'number' | 'boolean' {
272+
if (indexSignature.parameters.length !== 1) {
273+
throw new Error(`Expected exactly one key in index signature in ${
274+
this.classLoaded.localName} at ${this.classLoaded.fileName}`);
275+
}
276+
if (indexSignature.parameters[0].type !== 'Identifier') {
277+
throw new Error(`Only identifier-based index signatures are allowed in ${
278+
this.classLoaded.localName} at ${this.classLoaded.fileName}`);
279+
}
280+
if (!indexSignature.parameters[0].typeAnnotation) {
281+
throw new Error(`Missing key type annotation in index signature in ${
282+
this.classLoaded.localName} at ${this.classLoaded.fileName}`);
283+
}
284+
const type = this.getRangeFromTypeNode(indexSignature.parameters[0].typeAnnotation.typeAnnotation,
285+
this.getErrorIdentifierIndex());
286+
if (type.type !== 'raw') {
287+
throw new Error(`Only raw types are allowed in index signature keys in ${
288+
this.classLoaded.localName} at ${this.classLoaded.fileName}`);
289+
}
290+
return type.value;
291+
}
292+
293+
public getIndexRange(indexSignature: TSIndexSignature, commentData: CommentData): ParameterRangeUnresolved {
294+
// Check comment data
295+
if (commentData.range) {
296+
return commentData.range;
297+
}
298+
299+
// Check the typescript raw field type
300+
if (indexSignature.typeAnnotation) {
301+
return this.getRangeFromTypeNode(indexSignature.typeAnnotation.typeAnnotation, this.getErrorIdentifierIndex());
302+
}
303+
304+
throw new Error(`Missing field type on ${this.getErrorIdentifierIndex()
305+
} in ${this.classLoaded.localName} at ${this.classLoaded.fileName}`);
306+
}
210307
}
211308

212309
export interface ParameterLoaderArgs {
213310
classLoaded: ClassReferenceLoaded;
214311
}
215312

216-
export interface ParameterData<R> {
313+
export type ParameterData<R> = ParameterDataField<R> | ParameterDataIndex<R>;
314+
315+
export interface ParameterDataField<R> {
316+
/**
317+
* The data type.
318+
*/
319+
type: 'field';
217320
/**
218321
* The parameter name.
219322
*/
@@ -241,6 +344,29 @@ export interface ParameterData<R> {
241344
comment?: string;
242345
}
243346

347+
export interface ParameterDataIndex<R> {
348+
/**
349+
* The data type.
350+
*/
351+
type: 'index';
352+
/**
353+
* The domain of the parameter keys.
354+
*/
355+
domain: 'string' | 'number' | 'boolean';
356+
/**
357+
* The range of the parameter values.
358+
*/
359+
range: R;
360+
/**
361+
* The default value.
362+
*/
363+
default?: string;
364+
/**
365+
* The human-readable description of this parameter.
366+
*/
367+
comment?: string;
368+
}
369+
244370
export type ParameterRangeUnresolved = {
245371
type: 'raw';
246372
value: 'boolean' | 'number' | 'string';

lib/parse/ParameterResolver.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
33
import { ClassIndex, ClassLoaded, ClassReference, ClassReferenceLoaded, InterfaceLoaded } from './ClassIndex';
44
import { ClassLoader } from './ClassLoader';
55
import { ConstructorData } from './ConstructorLoader';
6-
import { ParameterData, ParameterLoader, ParameterRangeResolved, ParameterRangeUnresolved } from './ParameterLoader';
6+
import {
7+
ParameterData,
8+
ParameterDataField,
9+
ParameterLoader,
10+
ParameterRangeResolved,
11+
ParameterRangeUnresolved,
12+
} from './ParameterLoader';
713

814
export class ParameterResolver {
915
private readonly classLoader: ClassLoader;
@@ -54,7 +60,7 @@ export class ParameterResolver {
5460
public async resolveParameterData(
5561
parameters: ParameterData<ParameterRangeUnresolved>[],
5662
owningClass: ClassReferenceLoaded,
57-
): Promise<ParameterData<ParameterRangeResolved>[]> {
63+
): Promise<ParameterDataField<ParameterRangeResolved>[]> {
5864
return await Promise.all(parameters
5965
.map(async parameter => ({ ...parameter, range: await this.resolveRange(parameter.range, owningClass) })));
6066
}

0 commit comments

Comments
 (0)