elearning/Frontend-Learner/node_modules/eslint-plugin-unicorn/rules/prefer-class-fields.js
2026-01-13 10:48:02 +07:00

158 lines
4.3 KiB
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, context);
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: 'unopinionated',
},
fixable: 'code',
hasSuggestions: true,
messages,
},
};
export default config;