UNPKG

eslint-plugin-unicorn-x

Version:
169 lines (145 loc) 4.45 kB
import {isSemicolonToken} from '@eslint-community/eslint-utils'; import getIndentString from './utils/get-indent-string.js'; const MESSAGE_ID_ERROR = 'prefer-class-fields/error'; const MESSAGE_ID_SUGGESTION = 'prefer-class-fields/suggestion'; const messages = { [MESSAGE_ID_ERROR]: 'Prefer class field declaration over `this` assignment in constructor for static values.', [MESSAGE_ID_SUGGESTION]: 'Encountered same-named class field declaration and `this` assignment in constructor. Replace the class field declaration with the value from `this` assignment.', }; /** @param {import('eslint').Rule.Node} node @param {import('eslint').Rule.RuleContext['sourceCode']} sourceCode @param {import('eslint').Rule.RuleFixer} fixer */ const removeFieldAssignment = (node, sourceCode, fixer) => { const {line} = sourceCode.getLoc(node).start; const nodeText = sourceCode.getText(node); const lineText = sourceCode.lines[line - 1]; const isOnlyNodeOnLine = lineText.trim() === nodeText; return isOnlyNodeOnLine ? fixer.removeRange([ sourceCode.getIndexFromLoc({line, column: 0}), sourceCode.getIndexFromLoc({line: line + 1, column: 0}), ]) : fixer.remove(node); }; /** @type {import('eslint').Rule.RuleModule['create']} */ const create = (context) => { const {sourceCode} = context; return { ClassBody(classBody) { const constructor = classBody.body.find( (node) => node.kind === 'constructor' && !node.computed && !node.static && node.type === 'MethodDefinition' && node.value.type === 'FunctionExpression', ); if (!constructor) { return; } const node = constructor.value.body.body.find( (node) => node.type !== 'EmptyStatement', ); if ( !( node?.type === 'ExpressionStatement' && node.expression.type === 'AssignmentExpression' && node.expression.operator === '=' && node.expression.left.type === 'MemberExpression' && node.expression.left.object.type === 'ThisExpression' && !node.expression.left.computed && ['Identifier', 'PrivateIdentifier'].includes( node.expression.left.property.type, ) && node.expression.right.type === 'Literal' ) ) { return; } const propertyName = node.expression.left.property.name; const propertyValue = node.expression.right.raw; const propertyType = node.expression.left.property.type; const existingProperty = classBody.body.find( (node) => node.type === 'PropertyDefinition' && !node.computed && !node.static && node.key.type === propertyType && node.key.name === propertyName, ); const problem = { node, messageId: MESSAGE_ID_ERROR, }; /** @param {import('eslint').Rule.RuleFixer} fixer */ function* fix(fixer) { yield removeFieldAssignment(node, sourceCode, fixer); if (existingProperty) { if (existingProperty.value) { yield fixer.replaceText(existingProperty.value, propertyValue); return; } const text = ` = ${propertyValue}`; const lastToken = sourceCode.getLastToken(existingProperty); if (isSemicolonToken(lastToken)) { yield fixer.insertTextBefore(lastToken, text); return; } yield fixer.insertTextAfter(existingProperty, `${text};`); return; } const closingBrace = sourceCode.getLastToken(classBody); const indent = getIndentString(constructor, sourceCode); let text = `${indent}${propertyName} = ${propertyValue};\n`; const characterBefore = sourceCode.getText()[sourceCode.getRange(closingBrace)[0] - 1]; if (characterBefore !== '\n') { text = `\n${text}`; } const lastProperty = classBody.body.at(-1); if ( lastProperty.type === 'PropertyDefinition' && sourceCode.getLastToken(lastProperty).value !== ';' ) { text = `;${text}`; } yield fixer.insertTextBefore(closingBrace, text); } if (existingProperty?.value) { problem.suggest = [ { messageId: MESSAGE_ID_SUGGESTION, fix, }, ]; return problem; } problem.fix = fix; return problem; }, }; }; /** @type {import('eslint').Rule.RuleModule} */ const config = { create, meta: { type: 'suggestion', docs: { description: 'Prefer class field declarations over `this` assignments in constructors.', recommended: true, }, fixable: 'code', hasSuggestions: true, messages, }, }; export default config;