@@ -173,6 +173,38 @@ export default createRule<Options, MessageIds>({
173173 } ;
174174 }
175175
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+
176208 return {
177209 [ `${ functionScopeBoundaries } :exit` ] (
178210 node :
@@ -229,13 +261,62 @@ export default createRule<Options, MessageIds>({
229261 }
230262 } ) ( ) ;
231263
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+
232307 context . report ( {
233308 ...reportNodeOrLoc ,
234309 messageId : 'preferReadonly' ,
235310 data : {
236311 name : context . sourceCode . getText ( nameNode ) ,
237312 } ,
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+ } ,
239320 } ) ;
240321 }
241322 } ,
@@ -288,6 +369,8 @@ class ClassScope {
288369 private readonly classType : ts . Type ;
289370 private constructorScopeDepth = OUTSIDE_CONSTRUCTOR ;
290371 private readonly memberVariableModifications = new Set < string > ( ) ;
372+ private readonly memberVariableWithConstructorModifications =
373+ new Set < string > ( ) ;
291374 private readonly privateModifiableMembers = new Map <
292375 string ,
293376 ParameterOrPropertyDeclaration
@@ -358,6 +441,7 @@ class ClassScope {
358441 relationOfModifierTypeToClass === TypeToClassRelation . Instance &&
359442 this . constructorScopeDepth === DIRECTLY_INSIDE_CONSTRUCTOR
360443 ) {
444+ this . memberVariableWithConstructorModifications . add ( node . name . text ) ;
361445 return ;
362446 }
363447
@@ -465,4 +549,8 @@ class ClassScope {
465549
466550 return TypeToClassRelation . Instance ;
467551 }
552+
553+ public memberHasConstructorModifications ( name : string ) {
554+ return this . memberVariableWithConstructorModifications . has ( name ) ;
555+ }
468556}
0 commit comments