diff --git a/rules/prefer-class-fields.js b/rules/prefer-class-fields.js index f93dcad002..201e448467 100644 --- a/rules/prefer-class-fields.js +++ b/rules/prefer-class-fields.js @@ -27,27 +27,6 @@ const isThisAssignmentExpression = node => { return true; }; -/** -@param {import('estree').Expression | import('estree').PrivateIdentifier} node -*/ -const getPropertyName = node => { - if (node.type === 'Identifier') { - return node.name; - } - - if (node.type === 'Literal') { - return `[${node.raw}]`; - } - - if ( - node.type === 'TemplateLiteral' - && node.expressions.length === 0 - && node.quasis.length === 1 - ) { - return `[\`${node.quasis[0].value.raw}\`]`; - } -}; - /** @param {import('eslint').Rule.Node} node @param {import('eslint').Rule.RuleContext['sourceCode']} sourceCode @@ -67,6 +46,58 @@ const removeFieldAssignment = (node, sourceCode, fixer) => { : fixer.remove(node); }; +/** +@param {string} propertyName +@param {import('estree').ClassBody} classBody +*/ +const findClassFieldNamed = (propertyName, classBody) => { + for (const classBodyChild of classBody.body) { + if ( + classBodyChild.type === 'PropertyDefinition' + && classBodyChild.key.type === 'Identifier' + && classBodyChild.key.name === propertyName + ) { + return classBodyChild; + } + } +}; + +/** +@param {string} propertyName +@param {string} propertyValue +@param {import('estree').ClassBody} classBody +@param {import('estree').MethodDefinition} constructor +@param {import('eslint').Rule.RuleContext['sourceCode']} sourceCode +@param {import('eslint').Rule.RuleFixer} fixer +*/ +const addOrReplaceClassFieldDeclaration = ( + propertyName, + propertyValue, + classBody, + constructor, + sourceCode, + fixer, +) => { + const alreadyExistingDeclaration = findClassFieldNamed( + propertyName, + classBody, + ); + + if (alreadyExistingDeclaration) { + return fixer.replaceText( + alreadyExistingDeclaration, + `${propertyName} = ${propertyValue}`, + ); + } + + const classBodyStartRange = [classBody.range[0], classBody.range[0] + 1]; + const indent = getIndentString(constructor, sourceCode); + return fixer.insertTextAfterRange( + classBodyStartRange, + `\n${indent}${propertyName} = ${propertyValue};`, + ); +}; + /** @type {import('eslint').Rule.RuleModule['create']} */ @@ -87,39 +118,34 @@ const create = context => { return; } - const classBodyStartRange = [classBody.range[0], classBody.range[0] + 1]; - const indent = getIndentString(constructor, sourceCode); - for (let i = constructorBody.length - 1; i >= 0; i--) { const node = constructorBody[i]; if ( - !isThisAssignmentExpression(node) - || node.expression.right?.type !== 'Literal' - || node.expression.operator !== '=' + isThisAssignmentExpression(node) + && node.expression.right?.type === 'Literal' + && node.expression.operator === '=' + && node.expression.left.property.type === 'Identifier' ) { - continue; + return { + node, + messageId: MESSAGE_ID, + + /** + @param {import('eslint').Rule.RuleFixer} fixer + */ + * fix(fixer) { + yield removeFieldAssignment(node, sourceCode, fixer); + yield addOrReplaceClassFieldDeclaration( + node.expression.left.property.name, + node.expression.right.raw, + classBody, + constructor, + sourceCode, + fixer, + ); + }, + }; } - - const propertyName = getPropertyName(node.expression.left.property); - if (!propertyName) { - continue; - } - - return { - node, - messageId: MESSAGE_ID, - - /** - @param {import('eslint').Rule.RuleFixer} fixer - */ - * fix(fixer) { - yield removeFieldAssignment(node, sourceCode, fixer); - yield fixer.insertTextAfterRange( - classBodyStartRange, - `\n${indent}${propertyName} = ${node.expression.right.raw};`, - ); - }, - }; } }, }; diff --git a/test/prefer-class-fields.mjs b/test/prefer-class-fields.mjs index 52beae2b7f..823e55919d 100644 --- a/test/prefer-class-fields.mjs +++ b/test/prefer-class-fields.mjs @@ -75,15 +75,9 @@ test.snapshot({ `, outdent` class Foo { + foo = 'test'; constructor() { - this['foo'] = 'foo'; - } - } - `, - outdent` - class Foo { - constructor() { - this[\`foo\`] = 'foo'; + this.foo = 'foo'; } } `, diff --git a/test/snapshots/prefer-class-fields.mjs.md b/test/snapshots/prefer-class-fields.mjs.md index d3b6e6ab0e..c21d297e0e 100644 --- a/test/snapshots/prefer-class-fields.mjs.md +++ b/test/snapshots/prefer-class-fields.mjs.md @@ -148,56 +148,24 @@ Generated by [AVA](https://avajs.dev). 6 | }␊ ` -## invalid(5): class Foo { constructor() { this['foo'] = 'foo'; } } +## invalid(5): class Foo { foo = 'test'; constructor() { this.foo = 'foo'; } } > Input `␊ 1 | class Foo {␊ - 2 | constructor() {␊ - 3 | this['foo'] = 'foo';␊ - 4 | }␊ - 5 | }␊ - ` - -> Output - - `␊ - 1 | class Foo {␊ - 2 | ['foo'] = 'foo';␊ + 2 | foo = 'test';␊ 3 | constructor() {␊ - 4 | }␊ - 5 | }␊ - ` - -> Error 1/1 - - `␊ - 1 | class Foo {␊ - 2 | constructor() {␊ - > 3 | this['foo'] = 'foo';␊ - | ^^^^^^^^^^^^^^^^^^^^ Prefer class field declaration over \`this\` assignment in constructor for static values.␊ - 4 | }␊ - 5 | }␊ - ` - -## invalid(6): class Foo { constructor() { this[`foo`] = 'foo'; } } - -> Input - - `␊ - 1 | class Foo {␊ - 2 | constructor() {␊ - 3 | this[\`foo\`] = 'foo';␊ - 4 | }␊ - 5 | }␊ + 4 | this.foo = 'foo';␊ + 5 | }␊ + 6 | }␊ ` > Output `␊ 1 | class Foo {␊ - 2 | [\`foo\`] = 'foo';␊ + 2 | foo = 'foo'␊ 3 | constructor() {␊ 4 | }␊ 5 | }␊ @@ -207,9 +175,10 @@ Generated by [AVA](https://avajs.dev). `␊ 1 | class Foo {␊ - 2 | constructor() {␊ - > 3 | this[\`foo\`] = 'foo';␊ - | ^^^^^^^^^^^^^^^^^^^^ Prefer class field declaration over \`this\` assignment in constructor for static values.␊ - 4 | }␊ - 5 | }␊ + 2 | foo = 'test';␊ + 3 | constructor() {␊ + > 4 | this.foo = 'foo';␊ + | ^^^^^^^^^^^^^^^^^ Prefer class field declaration over \`this\` assignment in constructor for static values.␊ + 5 | }␊ + 6 | }␊ ` diff --git a/test/snapshots/prefer-class-fields.mjs.snap b/test/snapshots/prefer-class-fields.mjs.snap index fa6d236595..445d2abffe 100644 Binary files a/test/snapshots/prefer-class-fields.mjs.snap and b/test/snapshots/prefer-class-fields.mjs.snap differ