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' ;
310import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree' ;
411import { ClassReference , ClassReferenceLoaded , InterfaceLoaded } from './ClassIndex' ;
512import { 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
212309export 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+
244370export type ParameterRangeUnresolved = {
245371 type : 'raw' ;
246372 value : 'boolean' | 'number' | 'string' ;
0 commit comments