From 7993076a19dd85c525415387cfb2ebcd563a242f Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Thu, 26 Dec 2024 19:35:32 +0200 Subject: [PATCH 1/4] initial implementation --- .../src/rules/prefer-readonly.ts | 43 ++++++++++++++++++- .../tests/rules/prefer-readonly.test.ts | 38 ++++++++-------- 2 files changed, 61 insertions(+), 20 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-readonly.ts b/packages/eslint-plugin/src/rules/prefer-readonly.ts index 15256502ec20..f2ee7e577f37 100644 --- a/packages/eslint-plugin/src/rules/prefer-readonly.ts +++ b/packages/eslint-plugin/src/rules/prefer-readonly.ts @@ -229,13 +229,54 @@ export default createRule({ } })(); + // add an explicit type annotation in cases in which adding a `readonly` + // modifier would change the type of the property + const typeAnnotation = (() => { + if (violatingNode.type) { + return null; + } + + if (!violatingNode.initializer) { + return null; + } + + if (esNode.type !== AST_NODE_TYPES.PropertyDefinition) { + return null; + } + + const initializerType = checker.getTypeAtLocation( + violatingNode.initializer, + ); + + const violatingType = checker.getTypeAtLocation(violatingNode); + + if (initializerType === violatingType) { + return null; + } + + if ( + !tsutils.isLiteralType(initializerType) && + !tsutils.isEnumType(initializerType) + ) { + return null; + } + + return checker.typeToString(violatingType); + })(); + 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}`); + } + }, }); } }, diff --git a/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts b/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts index 1f0d7383c1db..2a48c97f2a4a 100644 --- a/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts @@ -764,7 +764,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiableStatic { - private static readonly incorrectlyModifiableStatic = 7; + private static readonly incorrectlyModifiableStatic: number = 7; } `, }, @@ -788,7 +788,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiableStatic { - static readonly #incorrectlyModifiableStatic = 7; + static readonly #incorrectlyModifiableStatic: number = 7; } `, }, @@ -876,11 +876,11 @@ class Foo { ], output: ` class TestIncorrectlyModifiableInline { - private readonly incorrectlyModifiableInline = 7; + private readonly incorrectlyModifiableInline: number = 7; public createConfusingChildClass() { return class { - private readonly incorrectlyModifiableInline = 7; + private readonly incorrectlyModifiableInline: number = 7; }; } } @@ -922,11 +922,11 @@ class Foo { ], output: ` class TestIncorrectlyModifiableInline { - readonly #incorrectlyModifiableInline = 7; + readonly #incorrectlyModifiableInline: number = 7; public createConfusingChildClass() { return class { - readonly #incorrectlyModifiableInline = 7; + readonly #incorrectlyModifiableInline: number = 7; }; } } @@ -956,7 +956,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiableDelayed { - private readonly incorrectlyModifiableDelayed = 7; + private readonly incorrectlyModifiableDelayed: number = 7; public constructor() { this.incorrectlyModifiableDelayed = 7; @@ -988,7 +988,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiableDelayed { - readonly #incorrectlyModifiableDelayed = 7; + readonly #incorrectlyModifiableDelayed: number = 7; public constructor() { this.#incorrectlyModifiableDelayed = 7; @@ -1026,7 +1026,7 @@ class Foo { ], output: ` class TestChildClassExpressionModifiable { - private readonly childClassExpressionModifiable = 7; + private readonly childClassExpressionModifiable: number = 7; public createConfusingChildClass() { return class { @@ -1070,7 +1070,7 @@ class Foo { ], output: ` class TestChildClassExpressionModifiable { - readonly #childClassExpressionModifiable = 7; + readonly #childClassExpressionModifiable: number = 7; public createConfusingChildClass() { return class { @@ -1109,7 +1109,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiablePostMinus { - private readonly incorrectlyModifiablePostMinus = 7; + private readonly incorrectlyModifiablePostMinus: number = 7; public mutate() { this.incorrectlyModifiablePostMinus - 1; @@ -1141,7 +1141,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiablePostMinus { - readonly #incorrectlyModifiablePostMinus = 7; + readonly #incorrectlyModifiablePostMinus: number = 7; public mutate() { this.#incorrectlyModifiablePostMinus - 1; @@ -1174,7 +1174,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiablePostPlus { - private readonly incorrectlyModifiablePostPlus = 7; + private readonly incorrectlyModifiablePostPlus: number = 7; public mutate() { this.incorrectlyModifiablePostPlus + 1; @@ -1207,7 +1207,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiablePostPlus { - readonly #incorrectlyModifiablePostPlus = 7; + readonly #incorrectlyModifiablePostPlus: number = 7; public mutate() { this.#incorrectlyModifiablePostPlus + 1; @@ -1239,7 +1239,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiablePreMinus { - private readonly incorrectlyModifiablePreMinus = 7; + private readonly incorrectlyModifiablePreMinus: number = 7; public mutate() { -this.incorrectlyModifiablePreMinus; @@ -1272,7 +1272,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiablePreMinus { - readonly #incorrectlyModifiablePreMinus = 7; + readonly #incorrectlyModifiablePreMinus: number = 7; public mutate() { -this.#incorrectlyModifiablePreMinus; @@ -1305,7 +1305,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiablePrePlus { - private readonly incorrectlyModifiablePrePlus = 7; + private readonly incorrectlyModifiablePrePlus: number = 7; public mutate() { +this.incorrectlyModifiablePrePlus; @@ -1338,7 +1338,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiablePrePlus { - readonly #incorrectlyModifiablePrePlus = 7; + readonly #incorrectlyModifiablePrePlus: number = 7; public mutate() { +this.#incorrectlyModifiablePrePlus; @@ -1375,7 +1375,7 @@ class Foo { ], output: ` class TestOverlappingClassVariable { - private readonly overlappingClassVariable = 7; + private readonly overlappingClassVariable: number = 7; public workWithSimilarClass(other: SimilarClass) { other.overlappingClassVariable = 7; From f0446152b68362a37e54ab10bb2935968c4f6d1b Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Thu, 26 Dec 2024 19:35:48 +0200 Subject: [PATCH 2/4] add tests --- .../src/rules/prefer-readonly.ts | 16 +- .../tests/rules/prefer-readonly.test.ts | 606 ++++++++++++++++++ 2 files changed, 611 insertions(+), 11 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-readonly.ts b/packages/eslint-plugin/src/rules/prefer-readonly.ts index f2ee7e577f37..2edcddf37ce5 100644 --- a/packages/eslint-plugin/src/rules/prefer-readonly.ts +++ b/packages/eslint-plugin/src/rules/prefer-readonly.ts @@ -229,14 +229,8 @@ export default createRule({ } })(); - // add an explicit type annotation in cases in which adding a `readonly` - // modifier would change the type of the property const typeAnnotation = (() => { - if (violatingNode.type) { - return null; - } - - if (!violatingNode.initializer) { + if (violatingNode.type || !violatingNode.initializer) { return null; } @@ -250,14 +244,14 @@ export default createRule({ const violatingType = checker.getTypeAtLocation(violatingNode); + // 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) && - !tsutils.isEnumType(initializerType) - ) { + if (!tsutils.isLiteralType(initializerType)) { return null; } diff --git a/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts b/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts index 2a48c97f2a4a..fb4450420455 100644 --- a/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts @@ -2333,5 +2333,611 @@ function ClassWithName {}>(Base: TBase) { } `, }, + { + 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: string = 'hello'; + } + `, + }, + { + 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; + } + `, + errors: [ + { + column: 11, + data: { + name: 'prop', + }, + endColumn: 23, + endLine: 3, + line: 3, + messageId: 'preferReadonly', + }, + ], + output: ` + class Test { + private readonly prop: number = 10; + } + `, + }, + { + code: ` + declare const hello: 10; + + class Test { + private prop = hello; + } + `, + 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; + } + `, + }, + { + 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: boolean = true; + } + `, + }, + { + code: ` + declare const hello: true; + + class Test { + private prop = hello; + } + `, + 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; + } + `, + }, + { + 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 = Foo.Bar; + } + `, + }, + { + 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 = 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: ` + 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 = [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 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; + } + `, + 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; + } + `, + }, + { + 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 = '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'); + } + `, + }, ], }); From a63f3350e311dff6d8a50b81c51344b5cd929fe2 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Mon, 30 Dec 2024 18:33:16 +0200 Subject: [PATCH 3/4] verify the about-to-be-added type annotation is in-scope --- .../src/rules/prefer-readonly.ts | 49 ++++++- .../tests/rules/prefer-readonly.test.ts | 138 ++++++++++++++++++ 2 files changed, 179 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-readonly.ts b/packages/eslint-plugin/src/rules/prefer-readonly.ts index 2edcddf37ce5..a7ac2cc0ec4a 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: @@ -230,19 +262,16 @@ export default createRule({ })(); const typeAnnotation = (() => { - if (violatingNode.type || !violatingNode.initializer) { + if (esNode.type !== AST_NODE_TYPES.PropertyDefinition) { return null; } - if (esNode.type !== AST_NODE_TYPES.PropertyDefinition) { + if (esNode.typeAnnotation || !esNode.value) { return null; } - const initializerType = checker.getTypeAtLocation( - violatingNode.initializer, - ); - - const violatingType = checker.getTypeAtLocation(violatingNode); + 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 @@ -255,7 +284,11 @@ export default createRule({ return null; } - return checker.typeToString(violatingType); + return getTypeAnnotationForViolatingNode( + esNode, + violatingType, + initializerType, + ); })(); context.report({ diff --git a/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts b/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts index fb4450420455..4b27dc3eb530 100644 --- a/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts @@ -2599,6 +2599,144 @@ function ClassWithName {}>(Base: TBase) { } `, }, + { + code: ` + enum Foo { + Bar, + Bazz, + } + + const bar = Foo.Bar; + + function wrapper() { + const Foo = 10; + + class Test { + private 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; + } + } + `, + }, + { + code: ` + enum Foo { + Bar, + Bazz, + } + + const bar = Foo.Bar; + + function wrapper() { + type Foo = 10; + + class Test { + private 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; + } + } + `, + }, + { + code: ` + const Bar = (function () { + enum Foo { + Bar, + Bazz, + } + + return Foo; + })(); + + const bar = Bar.Bar; + + class Test { + private 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; + } + `, + }, { code: ` class Test { From 125b56ebddb699b9b4c150651d98a6f0f4818a2a Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Tue, 14 Jan 2025 21:28:11 +0200 Subject: [PATCH 4/4] auto fix only literals that have been modified in the class constructor --- .../src/rules/prefer-readonly.ts | 20 + .../tests/rules/prefer-readonly.test.ts | 400 +++++++++++++++++- 2 files changed, 402 insertions(+), 18 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-readonly.ts b/packages/eslint-plugin/src/rules/prefer-readonly.ts index a7ac2cc0ec4a..fc1fca5978e6 100644 --- a/packages/eslint-plugin/src/rules/prefer-readonly.ts +++ b/packages/eslint-plugin/src/rules/prefer-readonly.ts @@ -270,6 +270,19 @@ export default createRule({ 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); @@ -356,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 @@ -426,6 +441,7 @@ class ClassScope { relationOfModifierTypeToClass === TypeToClassRelation.Instance && this.constructorScopeDepth === DIRECTLY_INSIDE_CONSTRUCTOR ) { + this.memberVariableWithConstructorModifications.add(node.name.text); return; } @@ -533,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 4b27dc3eb530..1e2289d65d1e 100644 --- a/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-readonly.test.ts @@ -764,7 +764,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiableStatic { - private static readonly incorrectlyModifiableStatic: number = 7; + private static readonly incorrectlyModifiableStatic = 7; } `, }, @@ -788,7 +788,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiableStatic { - static readonly #incorrectlyModifiableStatic: number = 7; + static readonly #incorrectlyModifiableStatic = 7; } `, }, @@ -876,11 +876,11 @@ class Foo { ], output: ` class TestIncorrectlyModifiableInline { - private readonly incorrectlyModifiableInline: number = 7; + private readonly incorrectlyModifiableInline = 7; public createConfusingChildClass() { return class { - private readonly incorrectlyModifiableInline: number = 7; + private readonly incorrectlyModifiableInline = 7; }; } } @@ -922,11 +922,11 @@ class Foo { ], output: ` class TestIncorrectlyModifiableInline { - readonly #incorrectlyModifiableInline: number = 7; + readonly #incorrectlyModifiableInline = 7; public createConfusingChildClass() { return class { - readonly #incorrectlyModifiableInline: number = 7; + readonly #incorrectlyModifiableInline = 7; }; } } @@ -988,7 +988,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiableDelayed { - readonly #incorrectlyModifiableDelayed: number = 7; + readonly #incorrectlyModifiableDelayed = 7; public constructor() { this.#incorrectlyModifiableDelayed = 7; @@ -1026,7 +1026,7 @@ class Foo { ], output: ` class TestChildClassExpressionModifiable { - private readonly childClassExpressionModifiable: number = 7; + private readonly childClassExpressionModifiable = 7; public createConfusingChildClass() { return class { @@ -1070,7 +1070,7 @@ class Foo { ], output: ` class TestChildClassExpressionModifiable { - readonly #childClassExpressionModifiable: number = 7; + readonly #childClassExpressionModifiable = 7; public createConfusingChildClass() { return class { @@ -1109,7 +1109,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiablePostMinus { - private readonly incorrectlyModifiablePostMinus: number = 7; + private readonly incorrectlyModifiablePostMinus = 7; public mutate() { this.incorrectlyModifiablePostMinus - 1; @@ -1141,7 +1141,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiablePostMinus { - readonly #incorrectlyModifiablePostMinus: number = 7; + readonly #incorrectlyModifiablePostMinus = 7; public mutate() { this.#incorrectlyModifiablePostMinus - 1; @@ -1174,7 +1174,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiablePostPlus { - private readonly incorrectlyModifiablePostPlus: number = 7; + private readonly incorrectlyModifiablePostPlus = 7; public mutate() { this.incorrectlyModifiablePostPlus + 1; @@ -1207,7 +1207,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiablePostPlus { - readonly #incorrectlyModifiablePostPlus: number = 7; + readonly #incorrectlyModifiablePostPlus = 7; public mutate() { this.#incorrectlyModifiablePostPlus + 1; @@ -1239,7 +1239,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiablePreMinus { - private readonly incorrectlyModifiablePreMinus: number = 7; + private readonly incorrectlyModifiablePreMinus = 7; public mutate() { -this.incorrectlyModifiablePreMinus; @@ -1272,7 +1272,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiablePreMinus { - readonly #incorrectlyModifiablePreMinus: number = 7; + readonly #incorrectlyModifiablePreMinus = 7; public mutate() { -this.#incorrectlyModifiablePreMinus; @@ -1305,7 +1305,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiablePrePlus { - private readonly incorrectlyModifiablePrePlus: number = 7; + private readonly incorrectlyModifiablePrePlus = 7; public mutate() { +this.incorrectlyModifiablePrePlus; @@ -1338,7 +1338,7 @@ class Foo { ], output: ` class TestIncorrectlyModifiablePrePlus { - readonly #incorrectlyModifiablePrePlus: number = 7; + readonly #incorrectlyModifiablePrePlus = 7; public mutate() { +this.#incorrectlyModifiablePrePlus; @@ -1375,7 +1375,7 @@ class Foo { ], output: ` class TestOverlappingClassVariable { - private readonly overlappingClassVariable: number = 7; + private readonly overlappingClassVariable = 7; public workWithSimilarClass(other: SimilarClass) { other.overlappingClassVariable = 7; @@ -2337,6 +2337,10 @@ function ClassWithName {}>(Base: TBase) { code: ` class Test { private prop = 'hello'; + + constructor() { + this.prop = 'world'; + } } `, errors: [ @@ -2354,6 +2358,70 @@ function ClassWithName {}>(Base: TBase) { 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'; + } } `, }, @@ -2389,6 +2457,10 @@ function ClassWithName {}>(Base: TBase) { code: ` class Test { private prop = 10; + + constructor() { + this.prop = 11; + } } `, errors: [ @@ -2406,6 +2478,34 @@ function ClassWithName {}>(Base: TBase) { 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; } `, }, @@ -2415,6 +2515,10 @@ function ClassWithName {}>(Base: TBase) { class Test { private prop = hello; + + constructor() { + this.prop = 11; + } } `, errors: [ @@ -2434,6 +2538,10 @@ function ClassWithName {}>(Base: TBase) { class Test { private readonly prop = hello; + + constructor() { + this.prop = 11; + } } `, }, @@ -2441,6 +2549,10 @@ function ClassWithName {}>(Base: TBase) { code: ` class Test { private prop = true; + + constructor() { + this.prop = false; + } } `, errors: [ @@ -2458,6 +2570,34 @@ function ClassWithName {}>(Base: TBase) { 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; } `, }, @@ -2467,6 +2607,10 @@ function ClassWithName {}>(Base: TBase) { class Test { private prop = hello; + + constructor() { + this.prop = false; + } } `, errors: [ @@ -2486,6 +2630,10 @@ function ClassWithName {}>(Base: TBase) { class Test { private readonly prop = hello; + + constructor() { + this.prop = false; + } } `, }, @@ -2498,6 +2646,10 @@ function ClassWithName {}>(Base: TBase) { class Test { private prop = Foo.Bar; + + constructor() { + this.prop = Foo.Bazz; + } } `, errors: [ @@ -2520,6 +2672,44 @@ function ClassWithName {}>(Base: TBase) { 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; } `, }, @@ -2534,6 +2724,10 @@ function ClassWithName {}>(Base: TBase) { class Test { private prop = foo; + + constructor() { + this.prop = foo; + } } `, errors: [ @@ -2558,6 +2752,48 @@ function ClassWithName {}>(Base: TBase) { 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; } `, }, @@ -2613,6 +2849,10 @@ function ClassWithName {}>(Base: TBase) { class Test { private prop = bar; + + constructor() { + this.prop = bar; + } } } `, @@ -2641,6 +2881,10 @@ function ClassWithName {}>(Base: TBase) { class Test { private readonly prop = bar; + + constructor() { + this.prop = bar; + } } } `, @@ -2659,6 +2903,10 @@ function ClassWithName {}>(Base: TBase) { class Test { private prop = bar; + + constructor() { + this.prop = bar; + } } } `, @@ -2687,6 +2935,10 @@ function ClassWithName {}>(Base: TBase) { class Test { private readonly prop = bar; + + constructor() { + this.prop = bar; + } } } `, @@ -2706,6 +2958,10 @@ function ClassWithName {}>(Base: TBase) { class Test { private prop = bar; + + constructor() { + this.prop = bar; + } } `, errors: [ @@ -2734,6 +2990,10 @@ function ClassWithName {}>(Base: TBase) { class Test { private readonly prop = bar; + + constructor() { + this.prop = bar; + } } `, }, @@ -2761,6 +3021,38 @@ function ClassWithName {}>(Base: TBase) { } `, }, + { + 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 { @@ -2785,6 +3077,38 @@ function ClassWithName {}>(Base: TBase) { } `, }, + { + 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 { @@ -2983,6 +3307,10 @@ function ClassWithName {}>(Base: TBase) { class Test { private prop = hello; + + constructor() { + this.prop = 10; + } } `, errors: [ @@ -3002,6 +3330,34 @@ function ClassWithName {}>(Base: TBase) { 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; } `, }, @@ -3009,6 +3365,10 @@ function ClassWithName {}>(Base: TBase) { code: ` class Test { private prop = null; + + constructor() { + this.prop = null; + } } `, errors: [ @@ -3026,6 +3386,10 @@ function ClassWithName {}>(Base: TBase) { output: ` class Test { private readonly prop = null; + + constructor() { + this.prop = null; + } } `, },