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; + } } `, }, 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