@@ -173,6 +173,38 @@ export default createRule<Options, MessageIds>({
173
173
} ;
174
174
}
175
175
176
+ function getTypeAnnotationForViolatingNode (
177
+ node : TSESTree . Node ,
178
+ type : ts . Type ,
179
+ initializerType : ts . Type ,
180
+ ) {
181
+ const annotation = checker . typeToString ( type ) ;
182
+
183
+ // verify the about-to-be-added type annotation is in-scope
184
+ if ( tsutils . isTypeFlagSet ( initializerType , ts . TypeFlags . EnumLiteral ) ) {
185
+ const scope = context . sourceCode . getScope ( node ) ;
186
+ const variable = ASTUtils . findVariable ( scope , annotation ) ;
187
+
188
+ if ( variable == null ) {
189
+ return null ;
190
+ }
191
+
192
+ const definition = variable . defs . find ( def => def . isTypeDefinition ) ;
193
+
194
+ if ( definition == null ) {
195
+ return null ;
196
+ }
197
+
198
+ const definitionType = services . getTypeAtLocation ( definition . node ) ;
199
+
200
+ if ( definitionType !== type ) {
201
+ return null ;
202
+ }
203
+ }
204
+
205
+ return annotation ;
206
+ }
207
+
176
208
return {
177
209
[ `${ functionScopeBoundaries } :exit` ] (
178
210
node :
@@ -229,13 +261,62 @@ export default createRule<Options, MessageIds>({
229
261
}
230
262
} ) ( ) ;
231
263
264
+ const typeAnnotation = ( ( ) => {
265
+ if ( esNode . type !== AST_NODE_TYPES . PropertyDefinition ) {
266
+ return null ;
267
+ }
268
+
269
+ if ( esNode . typeAnnotation || ! esNode . value ) {
270
+ return null ;
271
+ }
272
+
273
+ if ( nameNode . type !== AST_NODE_TYPES . Identifier ) {
274
+ return null ;
275
+ }
276
+
277
+ const hasConstructorModifications =
278
+ finalizedClassScope . memberHasConstructorModifications (
279
+ nameNode . name ,
280
+ ) ;
281
+
282
+ if ( ! hasConstructorModifications ) {
283
+ return null ;
284
+ }
285
+
286
+ const violatingType = services . getTypeAtLocation ( esNode ) ;
287
+ const initializerType = services . getTypeAtLocation ( esNode . value ) ;
288
+
289
+ // if the RHS is a literal, its type would be narrowed, while the
290
+ // type of the initializer (which isn't `readonly`) would be the
291
+ // widened type
292
+ if ( initializerType === violatingType ) {
293
+ return null ;
294
+ }
295
+
296
+ if ( ! tsutils . isLiteralType ( initializerType ) ) {
297
+ return null ;
298
+ }
299
+
300
+ return getTypeAnnotationForViolatingNode (
301
+ esNode ,
302
+ violatingType ,
303
+ initializerType ,
304
+ ) ;
305
+ } ) ( ) ;
306
+
232
307
context . report ( {
233
308
...reportNodeOrLoc ,
234
309
messageId : 'preferReadonly' ,
235
310
data : {
236
311
name : context . sourceCode . getText ( nameNode ) ,
237
312
} ,
238
- fix : fixer => fixer . insertTextBefore ( nameNode , 'readonly ' ) ,
313
+ * fix ( fixer ) {
314
+ yield fixer . insertTextBefore ( nameNode , 'readonly ' ) ;
315
+
316
+ if ( typeAnnotation ) {
317
+ yield fixer . insertTextAfter ( nameNode , `: ${ typeAnnotation } ` ) ;
318
+ }
319
+ } ,
239
320
} ) ;
240
321
}
241
322
} ,
@@ -288,6 +369,8 @@ class ClassScope {
288
369
private readonly classType : ts . Type ;
289
370
private constructorScopeDepth = OUTSIDE_CONSTRUCTOR ;
290
371
private readonly memberVariableModifications = new Set < string > ( ) ;
372
+ private readonly memberVariableWithConstructorModifications =
373
+ new Set < string > ( ) ;
291
374
private readonly privateModifiableMembers = new Map <
292
375
string ,
293
376
ParameterOrPropertyDeclaration
@@ -358,6 +441,7 @@ class ClassScope {
358
441
relationOfModifierTypeToClass === TypeToClassRelation . Instance &&
359
442
this . constructorScopeDepth === DIRECTLY_INSIDE_CONSTRUCTOR
360
443
) {
444
+ this . memberVariableWithConstructorModifications . add ( node . name . text ) ;
361
445
return ;
362
446
}
363
447
@@ -465,4 +549,8 @@ class ClassScope {
465
549
466
550
return TypeToClassRelation . Instance ;
467
551
}
552
+
553
+ public memberHasConstructorModifications ( name : string ) {
554
+ return this . memberVariableWithConstructorModifications . has ( name ) ;
555
+ }
468
556
}
0 commit comments