routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+778
@@ -0,0 +1,778 @@
|
||||
import {
|
||||
hasSideEffect,
|
||||
isCommaToken,
|
||||
isSemicolonToken,
|
||||
findVariable,
|
||||
} from '@eslint-community/eslint-utils';
|
||||
import {
|
||||
isMethodCall,
|
||||
isMemberExpression,
|
||||
isNewExpression,
|
||||
} from './ast/index.js';
|
||||
import {
|
||||
removeExpressionStatement,
|
||||
removeArgument,
|
||||
} from './fix/index.js';
|
||||
import {
|
||||
getNextNode,
|
||||
getCallExpressionArgumentsText,
|
||||
getParenthesizedText,
|
||||
getVariableIdentifiers,
|
||||
getNewExpressionTokens,
|
||||
isNewExpressionWithParentheses,
|
||||
} from './utils/index.js';
|
||||
|
||||
/**
|
||||
@import {TSESTree as ESTree} from '@typescript-eslint/types';
|
||||
@import * as ESLint from 'eslint';
|
||||
*/
|
||||
|
||||
const MESSAGE_ID_ERROR = 'error';
|
||||
const MESSAGE_ID_SUGGESTION_ARRAY = 'suggestion/array';
|
||||
const MESSAGE_ID_SUGGESTION_OBJECT = 'suggestion/object';
|
||||
const MESSAGE_ID_SUGGESTION_OBJECT_ASSIGN = 'suggestion/object-assign';
|
||||
const MESSAGE_ID_SUGGESTION_SET = 'suggestion/set';
|
||||
const MESSAGE_ID_SUGGESTION_MAP = 'suggestion/map';
|
||||
const messages = {
|
||||
[MESSAGE_ID_ERROR]: 'Immediate mutation on {{objectType}} is not allowed.',
|
||||
[MESSAGE_ID_SUGGESTION_ARRAY]: '{{operation}} the elements to the {{assignType}}.',
|
||||
[MESSAGE_ID_SUGGESTION_OBJECT]: 'Move this property to the {{assignType}}.',
|
||||
[MESSAGE_ID_SUGGESTION_OBJECT_ASSIGN]: '{{description}} the {{assignType}}.',
|
||||
[MESSAGE_ID_SUGGESTION_SET]: 'Move the element to the {{assignType}}.',
|
||||
[MESSAGE_ID_SUGGESTION_MAP]: 'Move the entry to the {{assignType}}.',
|
||||
};
|
||||
|
||||
const hasVariableInNodes = (variable, nodes, context) => {
|
||||
const {sourceCode} = context;
|
||||
const identifiers = getVariableIdentifiers(variable);
|
||||
return nodes.some(node => {
|
||||
const range = sourceCode.getRange(node);
|
||||
return identifiers.some(identifier => {
|
||||
const [start, end] = sourceCode.getRange(identifier);
|
||||
return start >= range[0] && end <= range[1];
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function isCallExpressionWithOptionalArrayExpression(newExpression, names) {
|
||||
if (!isNewExpression(
|
||||
newExpression,
|
||||
{names, maximumArguments: 1},
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// `new Set();` and `new Set([]);`
|
||||
const [iterable] = newExpression.arguments;
|
||||
return (!iterable || iterable.type === 'ArrayExpression');
|
||||
}
|
||||
|
||||
function * removeExpressionStatementAfterAssign(expressionStatement, context, fixer) {
|
||||
const tokenBefore = context.sourceCode.getTokenBefore(expressionStatement);
|
||||
const shouldPreserveSemiColon = !isSemicolonToken(tokenBefore);
|
||||
yield removeExpressionStatement(expressionStatement, context, fixer, shouldPreserveSemiColon);
|
||||
}
|
||||
|
||||
function appendListTextToArrayExpressionOrObjectExpression(
|
||||
context,
|
||||
fixer,
|
||||
arrayOrObjectExpression,
|
||||
listText,
|
||||
) {
|
||||
const {sourceCode} = context;
|
||||
const [
|
||||
penultimateToken,
|
||||
closingBracketToken,
|
||||
] = sourceCode.getLastTokens(arrayOrObjectExpression, 2);
|
||||
const list = arrayOrObjectExpression.type === 'ArrayExpression'
|
||||
? arrayOrObjectExpression.elements
|
||||
: arrayOrObjectExpression.properties;
|
||||
const shouldInsertComma = list.length > 0 && !isCommaToken(penultimateToken);
|
||||
|
||||
return fixer.insertTextBefore(
|
||||
closingBracketToken,
|
||||
`${shouldInsertComma ? ',' : ''} ${listText}`,
|
||||
);
|
||||
}
|
||||
|
||||
function * appendElementsTextToSetConstructor({
|
||||
context,
|
||||
fixer,
|
||||
newExpression,
|
||||
elementsText,
|
||||
nextExpressionStatement,
|
||||
}) {
|
||||
if (isNewExpressionWithParentheses(newExpression, context)) {
|
||||
const [setInitialValue] = newExpression.arguments;
|
||||
if (setInitialValue) {
|
||||
yield appendListTextToArrayExpressionOrObjectExpression(context, fixer, setInitialValue, elementsText);
|
||||
} else {
|
||||
const {
|
||||
openingParenthesisToken,
|
||||
} = getNewExpressionTokens(newExpression, context);
|
||||
yield fixer.insertTextAfter(openingParenthesisToken, `[${elementsText}]`);
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
The new expression doesn't have parentheses
|
||||
```
|
||||
const set = (( new (( Set )) ));
|
||||
set.add(1);
|
||||
```
|
||||
*/
|
||||
yield fixer.insertTextAfter(newExpression, `([${elementsText}])`);
|
||||
}
|
||||
|
||||
yield * removeExpressionStatementAfterAssign(nextExpressionStatement, context, fixer);
|
||||
}
|
||||
|
||||
function getObjectExpressionPropertiesText(objectExpression, context) {
|
||||
const {sourceCode} = context;
|
||||
const openingBraceToken = sourceCode.getFirstToken(objectExpression);
|
||||
const [penultimateToken, closingBraceToken] = sourceCode.getLastTokens(objectExpression, 2);
|
||||
const [, start] = sourceCode.getRange(openingBraceToken);
|
||||
const [end] = sourceCode.getRange(isCommaToken(penultimateToken) ? penultimateToken : closingBraceToken);
|
||||
return sourceCode.text.slice(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
@typedef {ESTree.VariableDeclarator['init'] | ESTree.AssignmentExpression['right']} ValueNode
|
||||
@typedef {(information: ViolationCaseInformation, arguments: any)} GetFix
|
||||
@typedef {Parameters<ESLint.Rule.RuleContext['report']>[0]} Problem
|
||||
@typedef {(information: ViolationCaseInformation) => ESTree.Node} GetProblematicNode
|
||||
@typedef {{
|
||||
context: ESLint.Rule.RuleContext,
|
||||
variable: ESLint.Scope.Variable,
|
||||
variableNode: ESTree.Identifier,
|
||||
valueNode: ValueNode,
|
||||
statement: ESTree.VariableDeclaration | ESTree.ExpressionStatement,
|
||||
nextExpressionStatement: ESTree.ExpressionStatement,
|
||||
assignType: 'assignment' | 'declaration',
|
||||
getFix: GetFix,
|
||||
}} ViolationCaseInformation
|
||||
@typedef {{
|
||||
testValue: (value: ValueNode) => boolean,
|
||||
getProblematicNode: GetProblematicNode,
|
||||
getProblem: (node: ReturnType<GetProblematicNode>, information: ViolationCaseInformation) => Problem,
|
||||
getFix: GetFix,
|
||||
}} ViolationCase
|
||||
*/
|
||||
|
||||
// `Array`
|
||||
/** @type {ViolationCase} */
|
||||
const arrayMutationSettings = {
|
||||
testValue: value => value?.type === 'ArrayExpression',
|
||||
getProblematicNode({
|
||||
context,
|
||||
variable,
|
||||
nextExpressionStatement,
|
||||
}) {
|
||||
const callExpression = nextExpressionStatement.expression;
|
||||
|
||||
if (!(
|
||||
isMethodCall(callExpression, {
|
||||
object: variable.name,
|
||||
methods: ['push', 'unshift'],
|
||||
optionalMember: false,
|
||||
optionalCall: false,
|
||||
})
|
||||
&& callExpression.arguments.length > 0
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasVariableInNodes(variable, callExpression.arguments, context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return callExpression;
|
||||
},
|
||||
getProblem(callExpression, information) {
|
||||
const {
|
||||
context,
|
||||
assignType,
|
||||
getFix,
|
||||
} = information;
|
||||
const {sourceCode} = context;
|
||||
const memberExpression = callExpression.callee;
|
||||
const method = memberExpression.property;
|
||||
const problem = {
|
||||
node: memberExpression,
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
data: {objectType: 'array'},
|
||||
};
|
||||
|
||||
const isPrepend = method.name === 'unshift';
|
||||
const fix = getFix(information, {
|
||||
callExpression,
|
||||
isPrepend,
|
||||
});
|
||||
|
||||
if (callExpression.arguments.some(element => hasSideEffect(element, sourceCode))) {
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: MESSAGE_ID_SUGGESTION_ARRAY,
|
||||
fix,
|
||||
data: {operation: isPrepend ? 'Prepend' : 'Append', assignType},
|
||||
},
|
||||
];
|
||||
} else {
|
||||
problem.fix = fix;
|
||||
}
|
||||
|
||||
return problem;
|
||||
},
|
||||
getFix: (
|
||||
{
|
||||
context,
|
||||
valueNode: arrayExpression,
|
||||
nextExpressionStatement,
|
||||
},
|
||||
{
|
||||
callExpression,
|
||||
isPrepend,
|
||||
},
|
||||
) => function * (fixer) {
|
||||
const text = getCallExpressionArgumentsText(context, callExpression, /* includeTrailingComma */ false);
|
||||
|
||||
yield (
|
||||
isPrepend
|
||||
? fixer.insertTextAfter(
|
||||
context.sourceCode.getFirstToken(arrayExpression),
|
||||
`${text}, `,
|
||||
)
|
||||
: appendListTextToArrayExpressionOrObjectExpression(context, fixer, arrayExpression, text)
|
||||
);
|
||||
|
||||
yield removeExpressionStatementAfterAssign(
|
||||
nextExpressionStatement,
|
||||
context,
|
||||
fixer,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// `Object` + `AssignmentExpression`
|
||||
/** @type {ViolationCase} */
|
||||
const objectWithAssignmentExpressionSettings = {
|
||||
testValue: value => value?.type === 'ObjectExpression',
|
||||
getProblematicNode({
|
||||
context,
|
||||
variable,
|
||||
nextExpressionStatement,
|
||||
}) {
|
||||
const assignmentExpression = nextExpressionStatement.expression;
|
||||
if (!(
|
||||
assignmentExpression.type === 'AssignmentExpression'
|
||||
&& assignmentExpression.operator === '='
|
||||
&& isMemberExpression(assignmentExpression.left, {object: variable.name, optional: false})
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = assignmentExpression.right;
|
||||
const memberExpression = assignmentExpression.left;
|
||||
const {property} = memberExpression;
|
||||
|
||||
if (
|
||||
hasVariableInNodes(
|
||||
variable,
|
||||
memberExpression.computed ? [property, value] : [value],
|
||||
context,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return assignmentExpression;
|
||||
},
|
||||
getProblem(assignmentExpression, information) {
|
||||
const {
|
||||
context,
|
||||
assignType,
|
||||
getFix,
|
||||
} = information;
|
||||
const {sourceCode} = context;
|
||||
const {
|
||||
left: memberExpression,
|
||||
right: value,
|
||||
} = assignmentExpression;
|
||||
|
||||
const {property} = memberExpression;
|
||||
const operatorToken = sourceCode.getTokenAfter(memberExpression, token => token.type === 'Punctuator' && token.value === assignmentExpression.operator);
|
||||
|
||||
const problem = {
|
||||
node: assignmentExpression,
|
||||
loc: {
|
||||
start: sourceCode.getLoc(assignmentExpression).start,
|
||||
end: sourceCode.getLoc(operatorToken).end,
|
||||
},
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
data: {objectType: 'object'},
|
||||
};
|
||||
const fix = getFix(information, {
|
||||
assignmentExpression,
|
||||
memberExpression,
|
||||
property,
|
||||
value,
|
||||
});
|
||||
|
||||
if (
|
||||
(memberExpression.computed && hasSideEffect(property, sourceCode))
|
||||
|| hasSideEffect(value, sourceCode)
|
||||
) {
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: MESSAGE_ID_SUGGESTION_OBJECT,
|
||||
data: {assignType},
|
||||
fix,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
problem.fix = fix;
|
||||
}
|
||||
|
||||
return problem;
|
||||
},
|
||||
getFix: (
|
||||
{
|
||||
context,
|
||||
valueNode: objectExpression,
|
||||
nextExpressionStatement,
|
||||
},
|
||||
{
|
||||
memberExpression,
|
||||
property,
|
||||
value,
|
||||
},
|
||||
) => function * (fixer) {
|
||||
let propertyText = getParenthesizedText(property, context);
|
||||
if (memberExpression.computed) {
|
||||
propertyText = `[${propertyText}]`;
|
||||
}
|
||||
|
||||
const valueText = getParenthesizedText(value, context);
|
||||
|
||||
const text = `${propertyText}: ${valueText},`;
|
||||
const [
|
||||
penultimateToken,
|
||||
closingBraceToken,
|
||||
] = context.sourceCode.getLastTokens(objectExpression, 2);
|
||||
const shouldInsertComma = objectExpression.properties.length > 0 && !isCommaToken(penultimateToken);
|
||||
|
||||
yield fixer.insertTextBefore(
|
||||
closingBraceToken,
|
||||
`${shouldInsertComma ? ',' : ''} ${text}`,
|
||||
);
|
||||
|
||||
yield removeExpressionStatementAfterAssign(
|
||||
nextExpressionStatement,
|
||||
context,
|
||||
fixer,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// `Object` + `Object.assign()`
|
||||
/** @type {ViolationCase} */
|
||||
const objectWithObjectAssignSettings = {
|
||||
testValue: value => value?.type === 'ObjectExpression',
|
||||
getProblematicNode({
|
||||
context,
|
||||
variable,
|
||||
nextExpressionStatement,
|
||||
}) {
|
||||
const callExpression = nextExpressionStatement.expression;
|
||||
|
||||
if (!isMethodCall(callExpression, {
|
||||
object: 'Object',
|
||||
method: 'assign',
|
||||
minimumArguments: 2,
|
||||
optionalMember: false,
|
||||
optionalCall: false,
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [object, firstValue] = callExpression.arguments;
|
||||
|
||||
if (
|
||||
!(object.type === 'Identifier' && object.name === variable.name)
|
||||
|| firstValue.type === 'SpreadElement'
|
||||
|| hasVariableInNodes(variable, [firstValue], context)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return callExpression;
|
||||
},
|
||||
getProblem(callExpression, information) {
|
||||
const {
|
||||
context,
|
||||
assignType,
|
||||
getFix,
|
||||
} = information;
|
||||
const {sourceCode} = context;
|
||||
const [, firstValue] = callExpression.arguments;
|
||||
|
||||
const problem = {
|
||||
node: callExpression.callee,
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
data: {objectType: 'object'},
|
||||
};
|
||||
const fix = getFix(information, {
|
||||
callExpression,
|
||||
firstValue,
|
||||
});
|
||||
|
||||
if (hasSideEffect(firstValue, sourceCode)) {
|
||||
const description = firstValue.type === 'ObjectExpression'
|
||||
? 'Move properties to'
|
||||
: 'Spread properties in';
|
||||
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: MESSAGE_ID_SUGGESTION_OBJECT_ASSIGN,
|
||||
data: {description, assignType},
|
||||
fix,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
problem.fix = fix;
|
||||
}
|
||||
|
||||
return problem;
|
||||
},
|
||||
getFix: (
|
||||
{
|
||||
context,
|
||||
valueNode: objectExpression,
|
||||
nextExpressionStatement,
|
||||
},
|
||||
{
|
||||
callExpression,
|
||||
firstValue,
|
||||
},
|
||||
) => function * (fixer) {
|
||||
let text;
|
||||
if (firstValue.type === 'ObjectExpression') {
|
||||
if (firstValue.properties.length > 0) {
|
||||
text = getObjectExpressionPropertiesText(firstValue, context);
|
||||
}
|
||||
} else {
|
||||
text = `...${getParenthesizedText(firstValue, context)}`;
|
||||
}
|
||||
|
||||
if (text) {
|
||||
yield appendListTextToArrayExpressionOrObjectExpression(context, fixer, objectExpression, text);
|
||||
}
|
||||
|
||||
if (callExpression.arguments.length !== 2) {
|
||||
yield removeArgument(fixer, firstValue, context);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
yield removeExpressionStatementAfterAssign(
|
||||
nextExpressionStatement,
|
||||
context,
|
||||
fixer,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// `Set` and `WeakSet`
|
||||
/** @type {ViolationCase} */
|
||||
const setMutationSettings = {
|
||||
testValue: value => isCallExpressionWithOptionalArrayExpression(value, ['Set', 'WeakSet']),
|
||||
getProblematicNode({
|
||||
context,
|
||||
variable,
|
||||
nextExpressionStatement,
|
||||
}) {
|
||||
let callExpression = nextExpressionStatement.expression;
|
||||
if (callExpression.type === 'ChainExpression') {
|
||||
callExpression = callExpression.expression;
|
||||
}
|
||||
|
||||
if (!isMethodCall(callExpression, {
|
||||
object: variable.name,
|
||||
method: 'add',
|
||||
argumentsLength: 1,
|
||||
optionalMember: false,
|
||||
optionalCall: false,
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasVariableInNodes(variable, callExpression.arguments, context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return callExpression;
|
||||
},
|
||||
getProblem(callExpression, information) {
|
||||
const {
|
||||
context,
|
||||
assignType,
|
||||
valueNode: newExpression,
|
||||
getFix,
|
||||
} = information;
|
||||
const {sourceCode} = context;
|
||||
const memberExpression = callExpression.callee;
|
||||
const problem = {
|
||||
node: memberExpression,
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
data: {objectType: `\`${newExpression.callee.name}\``},
|
||||
};
|
||||
|
||||
const fix = getFix(information, {
|
||||
callExpression,
|
||||
newExpression,
|
||||
});
|
||||
|
||||
if (callExpression.arguments.some(element => hasSideEffect(element, sourceCode))) {
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: MESSAGE_ID_SUGGESTION_SET,
|
||||
data: {assignType},
|
||||
fix,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
problem.fix = fix;
|
||||
}
|
||||
|
||||
return problem;
|
||||
},
|
||||
getFix: (
|
||||
{
|
||||
context,
|
||||
nextExpressionStatement,
|
||||
},
|
||||
{
|
||||
callExpression,
|
||||
newExpression,
|
||||
},
|
||||
) => fixer => {
|
||||
const elementsText = getCallExpressionArgumentsText(
|
||||
context,
|
||||
callExpression,
|
||||
/* IncludeTrailingComma */ false,
|
||||
);
|
||||
return appendElementsTextToSetConstructor({
|
||||
context,
|
||||
fixer,
|
||||
newExpression,
|
||||
elementsText,
|
||||
nextExpressionStatement,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// `Map` and `WeakMap`
|
||||
/** @type {ViolationCase} */
|
||||
const mapMutationSettings = {
|
||||
testValue: value => isCallExpressionWithOptionalArrayExpression(value, ['Map', 'WeakMap']),
|
||||
getProblematicNode({
|
||||
context,
|
||||
variable,
|
||||
nextExpressionStatement,
|
||||
}) {
|
||||
const callExpression = nextExpressionStatement.expression;
|
||||
|
||||
if (!isMethodCall(callExpression, {
|
||||
object: variable.name,
|
||||
method: 'set',
|
||||
argumentsLength: 2,
|
||||
optionalCall: false,
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasVariableInNodes(variable, callExpression.arguments, context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return callExpression;
|
||||
},
|
||||
getProblem(callExpression, information) {
|
||||
const {
|
||||
context,
|
||||
assignType,
|
||||
valueNode: newExpression,
|
||||
getFix,
|
||||
} = information;
|
||||
const {sourceCode} = context;
|
||||
const memberExpression = callExpression.callee;
|
||||
const problem = {
|
||||
node: memberExpression,
|
||||
messageId: MESSAGE_ID_ERROR,
|
||||
data: {objectType: `\`${newExpression.callee.name}\``},
|
||||
};
|
||||
|
||||
const fix = getFix(information, {
|
||||
callExpression,
|
||||
newExpression,
|
||||
});
|
||||
|
||||
if (callExpression.arguments.some(element => hasSideEffect(element, sourceCode))) {
|
||||
problem.suggest = [
|
||||
{
|
||||
messageId: MESSAGE_ID_SUGGESTION_MAP,
|
||||
data: {assignType},
|
||||
fix,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
problem.fix = fix;
|
||||
}
|
||||
|
||||
return problem;
|
||||
},
|
||||
getFix: (
|
||||
{
|
||||
context,
|
||||
nextExpressionStatement,
|
||||
},
|
||||
{
|
||||
callExpression,
|
||||
newExpression,
|
||||
},
|
||||
) => fixer => {
|
||||
const argumentsText = getCallExpressionArgumentsText(
|
||||
context,
|
||||
callExpression,
|
||||
/* IncludeTrailingComma */ false,
|
||||
);
|
||||
const entryText = `[${argumentsText}]`;
|
||||
return appendElementsTextToSetConstructor({
|
||||
context,
|
||||
fixer,
|
||||
newExpression,
|
||||
elementsText: entryText,
|
||||
nextExpressionStatement,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const cases = [
|
||||
arrayMutationSettings,
|
||||
objectWithAssignmentExpressionSettings,
|
||||
objectWithObjectAssignSettings,
|
||||
setMutationSettings,
|
||||
mapMutationSettings,
|
||||
];
|
||||
|
||||
function isLastDeclarator(variableDeclarator) {
|
||||
const variableDeclaration = variableDeclarator.parent;
|
||||
return (
|
||||
variableDeclaration.type === 'VariableDeclaration'
|
||||
&& variableDeclaration.declarations.at(-1) === variableDeclarator
|
||||
);
|
||||
}
|
||||
|
||||
const getVariable = (node, context) => {
|
||||
if (node.type === 'VariableDeclarator') {
|
||||
return context.sourceCode.getDeclaredVariables(node)
|
||||
.find(variable => variable.defs.length === 1 && variable.defs[0].name === node.id);
|
||||
}
|
||||
|
||||
return findVariable(context.sourceCode.getScope(node), node.left.name);
|
||||
};
|
||||
|
||||
function getCaseProblem(
|
||||
context,
|
||||
assignNode,
|
||||
{
|
||||
testValue,
|
||||
getProblematicNode,
|
||||
getProblem,
|
||||
getFix,
|
||||
},
|
||||
) {
|
||||
const isAssignment = assignNode.type === 'AssignmentExpression';
|
||||
const [variableNode, valueNode] = (isAssignment ? ['left', 'right'] : ['id', 'init'])
|
||||
.map(property => assignNode[property]);
|
||||
|
||||
// eslint-disable-next-line no-warning-comments
|
||||
// TODO[@fisker]: `AssignmentExpression` should not limit to `Identifier`
|
||||
if (!(variableNode.type === 'Identifier' && testValue(valueNode))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const statement = assignNode.parent;
|
||||
|
||||
if (!(
|
||||
// eslint-disable-next-line no-warning-comments
|
||||
// TODO[@fisker]: `AssignmentExpression` should support `a = b = c` too
|
||||
(
|
||||
isAssignment
|
||||
&& assignNode.operator === '='
|
||||
&& statement.type === 'ExpressionStatement'
|
||||
&& statement.expression === assignNode)
|
||||
|| (!isAssignment && isLastDeclarator(assignNode))
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextExpressionStatement = getNextNode(statement, context);
|
||||
if (nextExpressionStatement?.type !== 'ExpressionStatement') {
|
||||
return;
|
||||
}
|
||||
|
||||
const variable = getVariable(assignNode, context);
|
||||
/* c8 ignore next */
|
||||
if (!variable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const information = {
|
||||
context,
|
||||
variable,
|
||||
variableNode,
|
||||
valueNode,
|
||||
statement,
|
||||
nextExpressionStatement,
|
||||
assignType: isAssignment ? 'assignment' : 'declaration',
|
||||
getFix,
|
||||
};
|
||||
|
||||
const problematicNode = getProblematicNode(information);
|
||||
|
||||
if (!problematicNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getProblem(problematicNode, information);
|
||||
}
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
for (const caseSettings of cases) {
|
||||
context.on(
|
||||
[
|
||||
'VariableDeclarator',
|
||||
'AssignmentExpression',
|
||||
],
|
||||
assignNode => getCaseProblem(context, assignNode, caseSettings),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const config = {
|
||||
create,
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow immediate mutation after variable assignment.',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
messages,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Reference in New Issue
Block a user