eslint-plugin-unicorn-x
Version:
More than 100 powerful ESLint rules
169 lines (145 loc) • 4.45 kB
JavaScript
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;