diff --git a/packages/eslint-plugin/src/rules/prefer-readonly.ts b/packages/eslint-plugin/src/rules/prefer-readonly.ts index a393888926cf..ce3f75ebc22d 100644 --- a/packages/eslint-plugin/src/rules/prefer-readonly.ts +++ b/packages/eslint-plugin/src/rules/prefer-readonly.ts @@ -173,6 +173,38 @@ export default createRule({ }; } + function getTypeAnnotationForViolatingNode( + node: TSESTree.Node, + type: ts.Type, + initializerType: ts.Type, + ) { + const annotation = checker.typeToString(type); + + // verify the about-to-be-added type annotation is in-scope + if (tsutils.isTypeFlagSet(initializerType, ts.TypeFlags.EnumLiteral)) { + const scope = context.sourceCode.getScope(node); + const variable = ASTUtils.findVariable(scope, annotation); + + if (variable == null) { + return null; + } + + const definition = variable.defs.find(def => def.isTypeDefinition); + + if (definition == null) { + return null; + } + + const definitionType = services.getTypeAtLocation(definition.node); + + if (definitionType !== type) { + return null; + } + } + + return annotation; + } + return { [`${functionScopeBoundaries}:exit`]( node: @@ -229,13 +261,62 @@ export default createRule({ } })(); + const typeAnnotation = (() => { + if (esNode.type !== AST_NODE_TYPES.PropertyDefinition) { + return null; + } + + if (esNode.typeAnnotation || !esNode.value) { + return null; + } + + if (nameNode.type !== AST_NODE_TYPES.Identifier) { + return null; + } + + const hasConstructorModifications = + finalizedClassScope.memberHasConstructorModifications( + nameNode.name, + ); + + if (!hasConstructorModifications) { + return null; + } + + const violatingType = services.getTypeAtLocation(esNode); + const initializerType = services.getTypeAtLocation(esNode.value); + + // if the RHS is a literal, its type would be narrowed, while the + // type of the initializer (which isn't `readonly`) would be the + // widened type + if (initializerType === violatingType) { + return null; + } + + if (!tsutils.isLiteralType(initializerType)) { + return null; + } + + return getTypeAnnotationForViolatingNode( + esNode, + violatingType, + initializerType, + ); + })(); + context.report({ ...reportNodeOrLoc, messageId: 'preferReadonly', data: { name: context.sourceCode.getText(nameNode), }, - fix: fixer => fixer.insertTextBefore(nameNode, 'readonly '), + *fix(fixer) { + yield fixer.insertTextBefore(nameNode, 'readonly '); + + if (typeAnnotation) { + yield fixer.insertTextAfter(nameNode, `: ${typeAnnotation}`); + } + }, }); } }, @@ -288,6 +369,8 @@ class ClassScope { private readonly classType: ts.Type; private constructorScopeDepth = OUTSIDE_CONSTRUCTOR; private readonly memberVariableModifications = new Set(); + private readonly memberVariableWithConstructorModifications = + new Set(); private readonly privateModifiableMembers = new Map< string, ParameterOrPropertyDeclaration @@ -358,6 +441,7 @@ class ClassScope { relationOfModifierTypeToClass === TypeToClassRelation.Instance && this.constructorScopeDepth === DIRECTLY_INSIDE_CONSTRUCTOR ) { + this.memberVariableWithConstructorModifications.add(node.name.text); return; } @@ -465,4 +549,8 @@ class ClassScope { return TypeToClassRelation.Instance; } + + public memberHasConstructorModifications(name: string) { + return this.memberVariableWithConstructorModifications.has(name); + } } diff --git a/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts b/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts index 1f0d7383c1db..1e2289d65d1e 100644 --- a/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts @@ -956,7 +956,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiableDelayed { - private readonly incorrectlyModifiableDelayed = 7; + private readonly incorrectlyModifiableDelayed: number = 7; public constructor() { this.incorrectlyModifiableDelayed = 7; @@ -2333,5 +2333,1113 @@ function ClassWithName {}>(Base: TBase) { } `, }, + { + code: ` + class Test { + private prop = 'hello'; + + constructor() { + this.prop = 'world'; + } + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class Test { + private readonly prop: string = 'hello'; + + constructor() { + this.prop = 'world'; + } + } + `, + }, + { + code: ` + class Test { + private prop = 'hello'; + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class Test { + private readonly prop = 'hello'; + } + `, + }, + { + code: ` + declare const hello: 'hello'; + + class Test { + private prop = hello; + + constructor() { + this.prop = 'world'; + } + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 5, + line: 5, + messageId: 'preferReadonly', + }, + ], + output: ` + declare const hello: 'hello'; + + class Test { + private readonly prop = hello; + + constructor() { + this.prop = 'world'; + } + } + `, + }, + { + code: ` + declare const hello: 'hello'; + + class Test { + private prop = hello; + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 5, + line: 5, + messageId: 'preferReadonly', + }, + ], + output: ` + declare const hello: 'hello'; + + class Test { + private readonly prop = hello; + } + `, + }, + { + code: ` + class Test { + private prop = 10; + + constructor() { + this.prop = 11; + } + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class Test { + private readonly prop: number = 10; + + constructor() { + this.prop = 11; + } + } + `, + }, + { + code: ` + class Test { + private prop = 10; + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class Test { + private readonly prop = 10; + } + `, + }, + { + code: ` + declare const hello: 10; + + class Test { + private prop = hello; + + constructor() { + this.prop = 11; + } + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 5, + line: 5, + messageId: 'preferReadonly', + }, + ], + output: ` + declare const hello: 10; + + class Test { + private readonly prop = hello; + + constructor() { + this.prop = 11; + } + } + `, + }, + { + code: ` + class Test { + private prop = true; + + constructor() { + this.prop = false; + } + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class Test { + private readonly prop: boolean = true; + + constructor() { + this.prop = false; + } + } + `, + }, + { + code: ` + class Test { + private prop = true; + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class Test { + private readonly prop = true; + } + `, + }, + { + code: ` + declare const hello: true; + + class Test { + private prop = hello; + + constructor() { + this.prop = false; + } + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 5, + line: 5, + messageId: 'preferReadonly', + }, + ], + output: ` + declare const hello: true; + + class Test { + private readonly prop = hello; + + constructor() { + this.prop = false; + } + } + `, + }, + { + code: ` + enum Foo { + Bar, + Bazz, + } + + class Test { + private prop = Foo.Bar; + + constructor() { + this.prop = Foo.Bazz; + } + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 8, + line: 8, + messageId: 'preferReadonly', + }, + ], + output: ` + enum Foo { + Bar, + Bazz, + } + + class Test { + private readonly prop: Foo = Foo.Bar; + + constructor() { + this.prop = Foo.Bazz; + } + } + `, + }, + { + code: ` + enum Foo { + Bar, + Bazz, + } + + class Test { + private prop = Foo.Bar; + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 8, + line: 8, + messageId: 'preferReadonly', + }, + ], + output: ` + enum Foo { + Bar, + Bazz, + } + + class Test { + private readonly prop = Foo.Bar; + } + `, + }, + { + code: ` + enum Foo { + Bar, + Bazz, + } + + const foo = Foo.Bar; + + class Test { + private prop = foo; + + constructor() { + this.prop = foo; + } + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 10, + line: 10, + messageId: 'preferReadonly', + }, + ], + output: ` + enum Foo { + Bar, + Bazz, + } + + const foo = Foo.Bar; + + class Test { + private readonly prop: Foo = foo; + + constructor() { + this.prop = foo; + } + } + `, + }, + { + code: ` + enum Foo { + Bar, + Bazz, + } + + const foo = Foo.Bar; + + class Test { + private prop = foo; + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 10, + line: 10, + messageId: 'preferReadonly', + }, + ], + output: ` + enum Foo { + Bar, + Bazz, + } + + const foo = Foo.Bar; + + class Test { + private readonly prop = foo; + } + `, + }, + { + code: ` + enum Foo { + Bar, + Bazz, + } + + declare const foo: Foo; + + class Test { + private prop = foo; + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 10, + line: 10, + messageId: 'preferReadonly', + }, + ], + output: ` + enum Foo { + Bar, + Bazz, + } + + declare const foo: Foo; + + class Test { + private readonly prop = foo; + } + `, + }, + { + code: ` + enum Foo { + Bar, + Bazz, + } + + const bar = Foo.Bar; + + function wrapper() { + const Foo = 10; + + class Test { + private prop = bar; + + constructor() { + this.prop = bar; + } + } + } + `, + errors: [ + { + column: 13, + data: { + name: 'prop', + }, + endColumn: 25, + endLine: 13, + line: 13, + messageId: 'preferReadonly', + }, + ], + output: ` + enum Foo { + Bar, + Bazz, + } + + const bar = Foo.Bar; + + function wrapper() { + const Foo = 10; + + class Test { + private readonly prop = bar; + + constructor() { + this.prop = bar; + } + } + } + `, + }, + { + code: ` + enum Foo { + Bar, + Bazz, + } + + const bar = Foo.Bar; + + function wrapper() { + type Foo = 10; + + class Test { + private prop = bar; + + constructor() { + this.prop = bar; + } + } + } + `, + errors: [ + { + column: 13, + data: { + name: 'prop', + }, + endColumn: 25, + endLine: 13, + line: 13, + messageId: 'preferReadonly', + }, + ], + output: ` + enum Foo { + Bar, + Bazz, + } + + const bar = Foo.Bar; + + function wrapper() { + type Foo = 10; + + class Test { + private readonly prop = bar; + + constructor() { + this.prop = bar; + } + } + } + `, + }, + { + code: ` + const Bar = (function () { + enum Foo { + Bar, + Bazz, + } + + return Foo; + })(); + + const bar = Bar.Bar; + + class Test { + private prop = bar; + + constructor() { + this.prop = bar; + } + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 14, + line: 14, + messageId: 'preferReadonly', + }, + ], + output: ` + const Bar = (function () { + enum Foo { + Bar, + Bazz, + } + + return Foo; + })(); + + const bar = Bar.Bar; + + class Test { + private readonly prop = bar; + + constructor() { + this.prop = bar; + } + } + `, + }, + { + code: ` + class Test { + private prop = { foo: 'bar' }; + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class Test { + private readonly prop = { foo: 'bar' }; + } + `, + }, + { + code: ` + class Test { + private prop = { foo: 'bar' }; + + constructor() { + this.prop = { foo: 'bazz' }; + } + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class Test { + private readonly prop = { foo: 'bar' }; + + constructor() { + this.prop = { foo: 'bazz' }; + } + } + `, + }, + { + code: ` + class Test { + private prop = [1, 2, 'three']; + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class Test { + private readonly prop = [1, 2, 'three']; + } + `, + }, + { + code: ` + class Test { + private prop = [1, 2, 'three']; + + constructor() { + this.prop = [1, 2, 'four']; + } + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class Test { + private readonly prop = [1, 2, 'three']; + + constructor() { + this.prop = [1, 2, 'four']; + } + } + `, + }, + { + code: ` + class X { + private _isValid = true; + + getIsValid = () => this._isValid; + + constructor(data?: {}) { + if (!data) { + this._isValid = false; + } + } + } + `, + errors: [ + { + column: 11, + data: { + name: '_isValid', + }, + endColumn: 27, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class X { + private readonly _isValid: boolean = true; + + getIsValid = () => this._isValid; + + constructor(data?: {}) { + if (!data) { + this._isValid = false; + } + } + } + `, + }, + { + code: ` + class Test { + private prop: string = 'hello'; + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class Test { + private readonly prop: string = 'hello'; + } + `, + }, + { + code: ` + class Test { + private prop: string | number = 'hello'; + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class Test { + private readonly prop: string | number = 'hello'; + } + `, + }, + { + code: ` + class Test { + private prop: string; + + constructor() { + this.prop = 'hello'; + } + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class Test { + private readonly prop: string; + + constructor() { + this.prop = 'hello'; + } + } + `, + }, + { + code: ` + class Test { + private prop; + + constructor() { + this.prop = 'hello'; + } + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class Test { + private readonly prop; + + constructor() { + this.prop = 'hello'; + } + } + `, + }, + { + code: ` + class Test { + private prop; + + constructor(x: boolean) { + if (x) { + this.prop = 'hello'; + } else { + this.prop = 10; + } + } + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class Test { + private readonly prop; + + constructor(x: boolean) { + if (x) { + this.prop = 'hello'; + } else { + this.prop = 10; + } + } + } + `, + }, + { + code: ` + declare const hello: 'hello' | 10; + + class Test { + private prop = hello; + + constructor() { + this.prop = 10; + } + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 5, + line: 5, + messageId: 'preferReadonly', + }, + ], + output: ` + declare const hello: 'hello' | 10; + + class Test { + private readonly prop = hello; + + constructor() { + this.prop = 10; + } + } + `, + }, + { + code: ` + class Test { + private prop = null; + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class Test { + private readonly prop = null; + } + `, + }, + { + code: ` + class Test { + private prop = null; + + constructor() { + this.prop = null; + } + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class Test { + private readonly prop = null; + + constructor() { + this.prop = null; + } + } + `, + }, + { + code: ` + class Test { + private prop = 'hello' as string; + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class Test { + private readonly prop = 'hello' as string; + } + `, + }, + { + code: ` + class Test { + private prop = Promise.resolve('hello'); + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class Test { + private readonly prop = Promise.resolve('hello'); + } + `, + }, ], }); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy