158 lines
4.3 KiB
JavaScript
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;
|