]>
git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/logical-assignment-operators.js
2 * @fileoverview Rule to replace assignment expressions with logical operator assignment
3 * @author Daniel Martens
7 //------------------------------------------------------------------------------
9 //------------------------------------------------------------------------------
10 const astUtils
= require("./utils/ast-utils.js");
12 //------------------------------------------------------------------------------
14 //------------------------------------------------------------------------------
16 const baseTypes
= new Set(["Identifier", "Super", "ThisExpression"]);
19 * Returns true iff either "undefined" or a void expression (eg. "void 0")
20 * @param {ASTNode} expression Expression to check
21 * @param {import('eslint-scope').Scope} scope Scope of the expression
22 * @returns {boolean} True iff "undefined" or "void ..."
24 function isUndefined(expression
, scope
) {
25 if (expression
.type
=== "Identifier" && expression
.name
=== "undefined") {
26 return astUtils
.isReferenceToGlobalVariable(scope
, expression
);
29 return expression
.type
=== "UnaryExpression" &&
30 expression
.operator
=== "void" &&
31 expression
.argument
.type
=== "Literal" &&
32 expression
.argument
.value
=== 0;
36 * Returns true iff the reference is either an identifier or member expression
37 * @param {ASTNode} expression Expression to check
38 * @returns {boolean} True for identifiers and member expressions
40 function isReference(expression
) {
41 return (expression
.type
=== "Identifier" && expression
.name
!== "undefined") ||
42 expression
.type
=== "MemberExpression";
46 * Returns true iff the expression checks for nullish with loose equals.
47 * Examples: value == null, value == void 0
48 * @param {ASTNode} expression Test condition
49 * @param {import('eslint-scope').Scope} scope Scope of the expression
50 * @returns {boolean} True iff implicit nullish comparison
52 function isImplicitNullishComparison(expression
, scope
) {
53 if (expression
.type
!== "BinaryExpression" || expression
.operator
!== "==") {
57 const reference
= isReference(expression
.left
) ? "left" : "right";
58 const nullish
= reference
=== "left" ? "right" : "left";
60 return isReference(expression
[reference
]) &&
61 (astUtils
.isNullLiteral(expression
[nullish
]) || isUndefined(expression
[nullish
], scope
));
65 * Condition with two equal comparisons.
66 * @param {ASTNode} expression Condition
67 * @returns {boolean} True iff matches ? === ? || ? === ?
69 function isDoubleComparison(expression
) {
70 return expression
.type
=== "LogicalExpression" &&
71 expression
.operator
=== "||" &&
72 expression
.left
.type
=== "BinaryExpression" &&
73 expression
.left
.operator
=== "===" &&
74 expression
.right
.type
=== "BinaryExpression" &&
75 expression
.right
.operator
=== "===";
79 * Returns true iff the expression checks for undefined and null.
80 * Example: value === null || value === undefined
81 * @param {ASTNode} expression Test condition
82 * @param {import('eslint-scope').Scope} scope Scope of the expression
83 * @returns {boolean} True iff explicit nullish comparison
85 function isExplicitNullishComparison(expression
, scope
) {
86 if (!isDoubleComparison(expression
)) {
89 const leftReference
= isReference(expression
.left
.left
) ? "left" : "right";
90 const leftNullish
= leftReference
=== "left" ? "right" : "left";
91 const rightReference
= isReference(expression
.right
.left
) ? "left" : "right";
92 const rightNullish
= rightReference
=== "left" ? "right" : "left";
94 return astUtils
.isSameReference(expression
.left
[leftReference
], expression
.right
[rightReference
]) &&
95 ((astUtils
.isNullLiteral(expression
.left
[leftNullish
]) && isUndefined(expression
.right
[rightNullish
], scope
)) ||
96 (isUndefined(expression
.left
[leftNullish
], scope
) && astUtils
.isNullLiteral(expression
.right
[rightNullish
])));
100 * Returns true for Boolean(arg) calls
101 * @param {ASTNode} expression Test condition
102 * @param {import('eslint-scope').Scope} scope Scope of the expression
103 * @returns {boolean} Whether the expression is a boolean cast
105 function isBooleanCast(expression
, scope
) {
106 return expression
.type
=== "CallExpression" &&
107 expression
.callee
.name
=== "Boolean" &&
108 expression
.arguments
.length
=== 1 &&
109 astUtils
.isReferenceToGlobalVariable(scope
, expression
.callee
);
114 * truthiness checks: value, Boolean(value), !!value
115 * falsiness checks: !value, !Boolean(value)
116 * nullish checks: value == null, value === undefined || value === null
117 * @param {ASTNode} expression Test condition
118 * @param {import('eslint-scope').Scope} scope Scope of the expression
119 * @returns {?{ reference: ASTNode, operator: '??'|'||'|'&&'}} Null if not a known existence
121 function getExistence(expression
, scope
) {
122 const isNegated
= expression
.type
=== "UnaryExpression" && expression
.operator
=== "!";
123 const base
= isNegated
? expression
.argument
: expression
;
126 case isReference(base
):
127 return { reference
: base
, operator
: isNegated
? "||" : "&&" };
128 case base
.type
=== "UnaryExpression" && base
.operator
=== "!" && isReference(base
.argument
):
129 return { reference
: base
.argument
, operator
: "&&" };
130 case isBooleanCast(base
, scope
) && isReference(base
.arguments
[0]):
131 return { reference
: base
.arguments
[0], operator
: isNegated
? "||" : "&&" };
132 case isImplicitNullishComparison(expression
, scope
):
133 return { reference
: isReference(expression
.left
) ? expression
.left
: expression
.right
, operator
: "??" };
134 case isExplicitNullishComparison(expression
, scope
):
135 return { reference
: isReference(expression
.left
.left
) ? expression
.left
.left
: expression
.left
.right
, operator
: "??" };
136 default: return null;
141 * Returns true iff the node is inside a with block
142 * @param {ASTNode} node Node to check
143 * @returns {boolean} True iff passed node is inside a with block
145 function isInsideWithBlock(node
) {
146 if (node
.type
=== "Program") {
150 return node
.parent
.type
=== "WithStatement" && node
.parent
.body
=== node
? true : isInsideWithBlock(node
.parent
);
153 //------------------------------------------------------------------------------
155 //------------------------------------------------------------------------------
156 /** @type {import('../shared/types').Rule} */
162 description
: "Require or disallow logical assignment operator shorthand",
164 url
: "https://eslint.org/docs/latest/rules/logical-assignment-operators"
175 enforceForIfStatements
: {
179 additionalProperties
: false
182 minItems
: 0, // 0 for allowing passing no options
185 items
: [{ const: "never" }],
191 // eslint-disable-next-line eslint-plugin/require-meta-has-suggestions -- Does not detect conditional suggestions
192 hasSuggestions
: true,
194 assignment
: "Assignment (=) can be replaced with operator assignment ({{operator}}).",
195 useLogicalOperator
: "Convert this assignment to use the operator {{ operator }}.",
196 logical
: "Logical expression can be replaced with an assignment ({{ operator }}).",
197 convertLogical
: "Replace this logical expression with an assignment with the operator {{ operator }}.",
198 if: "'if' statement can be replaced with a logical operator assignment with operator {{ operator }}.",
199 convertIf
: "Replace this 'if' statement with a logical assignment with operator {{ operator }}.",
200 unexpected
: "Unexpected logical operator assignment ({{operator}}) shorthand.",
201 separate
: "Separate the logical assignment into an assignment with a logical operator."
206 const mode
= context
.options
[0] === "never" ? "never" : "always";
207 const checkIf
= mode
=== "always" && context
.options
.length
> 1 && context
.options
[1].enforceForIfStatements
;
208 const sourceCode
= context
.sourceCode
;
209 const isStrict
= sourceCode
.getScope(sourceCode
.ast
).isStrict
;
212 * Returns false if the access could be a getter
213 * @param {ASTNode} node Assignment expression
214 * @returns {boolean} True iff the fix is safe
216 function cannotBeGetter(node
) {
217 return node
.type
=== "Identifier" &&
218 (isStrict
|| !isInsideWithBlock(node
));
222 * Check whether only a single property is accessed
223 * @param {ASTNode} node reference
224 * @returns {boolean} True iff a single property is accessed
226 function accessesSingleProperty(node
) {
227 if (!isStrict
&& isInsideWithBlock(node
)) {
228 return node
.type
=== "Identifier";
231 return node
.type
=== "MemberExpression" &&
232 baseTypes
.has(node
.object
.type
) &&
233 (!node
.computed
|| (node
.property
.type
!== "MemberExpression" && node
.property
.type
!== "ChainExpression"));
237 * Adds a fixer or suggestion whether on the fix is safe.
238 * @param {{ messageId: string, node: ASTNode }} descriptor Report descriptor without fix or suggest
239 * @param {{ messageId: string, fix: Function }} suggestion Adds the fix or the whole suggestion as only element in "suggest" to suggestion
240 * @param {boolean} shouldBeFixed Fix iff the condition is true
241 * @returns {Object} Descriptor with either an added fix or suggestion
243 function createConditionalFixer(descriptor
, suggestion
, shouldBeFixed
) {
253 suggest
: [suggestion
]
259 * Returns the operator token for assignments and binary expressions
260 * @param {ASTNode} node AssignmentExpression or BinaryExpression
261 * @returns {import('eslint').AST.Token} Operator token between the left and right expression
263 function getOperatorToken(node
) {
264 return sourceCode
.getFirstTokenBetween(node
.left
, node
.right
, token
=> token
.value
=== node
.operator
);
267 if (mode
=== "never") {
271 "AssignmentExpression"(assignment
) {
272 if (!astUtils
.isLogicalAssignmentOperator(assignment
.operator
)) {
277 messageId
: "unexpected",
279 data
: { operator
: assignment
.operator
}
282 messageId
: "separate",
284 if (sourceCode
.getCommentsInside(assignment
).length
> 0) {
288 const operatorToken
= getOperatorToken(assignment
);
291 yield ruleFixer
.replaceText(operatorToken
, "=");
293 const assignmentText
= sourceCode
.getText(assignment
.left
);
294 const operator
= assignment
.operator
.slice(0, -1);
296 // -> foo = foo || bar
297 yield ruleFixer
.insertTextAfter(operatorToken
, ` ${assignmentText} ${operator}`);
299 const precedence
= astUtils
.getPrecedence(assignment
.right
) <= astUtils
.getPrecedence({ type
: "LogicalExpression", operator
});
301 // ?? and || / && cannot be mixed but have same precedence
302 const mixed
= assignment
.operator
=== "??=" && astUtils
.isLogicalExpression(assignment
.right
);
304 if (!astUtils
.isParenthesised(sourceCode
, assignment
.right
) && (precedence
|| mixed
)) {
306 // -> foo = foo || (bar)
307 yield ruleFixer
.insertTextBefore(assignment
.right
, "(");
308 yield ruleFixer
.insertTextAfter(assignment
.right
, ")");
313 context
.report(createConditionalFixer(descriptor
, suggestion
, cannotBeGetter(assignment
.left
)));
321 "AssignmentExpression[operator='='][right.type='LogicalExpression']"(assignment
) {
322 if (!astUtils
.isSameReference(assignment
.left
, assignment
.right
.left
)) {
327 messageId
: "assignment",
329 data
: { operator
: `${assignment.right.operator}=` }
332 messageId
: "useLogicalOperator",
333 data
: { operator
: `${assignment.right.operator}=` },
335 if (sourceCode
.getCommentsInside(assignment
).length
> 0) {
339 // No need for parenthesis around the assignment based on precedence as the precedence stays the same even with changed operator
340 const assignmentOperatorToken
= getOperatorToken(assignment
);
342 // -> foo ||= foo || bar
343 yield ruleFixer
.insertTextBefore(assignmentOperatorToken
, assignment
.right
.operator
);
346 const logicalOperatorToken
= getOperatorToken(assignment
.right
);
347 const firstRightOperandToken
= sourceCode
.getTokenAfter(logicalOperatorToken
);
349 yield ruleFixer
.removeRange([assignment
.right
.range
[0], firstRightOperandToken
.range
[0]]);
353 context
.report(createConditionalFixer(descriptor
, suggestion
, cannotBeGetter(assignment
.left
)));
356 // foo || (foo = bar)
357 'LogicalExpression[right.type="AssignmentExpression"][right.operator="="]'(logical
) {
359 // Right side has to be parenthesized, otherwise would be parsed as (foo || foo) = bar which is illegal
360 if (isReference(logical
.left
) && astUtils
.isSameReference(logical
.left
, logical
.right
.left
)) {
362 messageId
: "logical",
364 data
: { operator
: `${logical.operator}=` }
367 messageId
: "convertLogical",
368 data
: { operator
: `${logical.operator}=` },
370 if (sourceCode
.getCommentsInside(logical
).length
> 0) {
374 const requiresOuterParenthesis
= logical
.parent
.type
!== "ExpressionStatement" &&
375 (astUtils
.getPrecedence({ type
: "AssignmentExpression" }) < astUtils
.getPrecedence(logical
.parent
));
377 if (!astUtils
.isParenthesised(sourceCode
, logical
) && requiresOuterParenthesis
) {
378 yield ruleFixer
.insertTextBefore(logical
, "(");
379 yield ruleFixer
.insertTextAfter(logical
, ")");
382 // Also removes all opening parenthesis
383 yield ruleFixer
.removeRange([logical
.range
[0], logical
.right
.range
[0]]); // -> foo = bar)
385 // Also removes all ending parenthesis
386 yield ruleFixer
.removeRange([logical
.right
.range
[1], logical
.range
[1]]); // -> foo = bar
388 const operatorToken
= getOperatorToken(logical
.right
);
390 yield ruleFixer
.insertTextBefore(operatorToken
, logical
.operator
); // -> foo ||= bar
393 const fix
= cannotBeGetter(logical
.left
) || accessesSingleProperty(logical
.left
);
395 context
.report(createConditionalFixer(descriptor
, suggestion
, fix
));
399 // if (foo) foo = bar
400 "IfStatement[alternate=null]"(ifNode
) {
405 const hasBody
= ifNode
.consequent
.type
=== "BlockStatement";
407 if (hasBody
&& ifNode
.consequent
.body
.length
!== 1) {
411 const body
= hasBody
? ifNode
.consequent
.body
[0] : ifNode
.consequent
;
412 const scope
= sourceCode
.getScope(ifNode
);
413 const existence
= getExistence(ifNode
.test
, scope
);
416 body
.type
=== "ExpressionStatement" &&
417 body
.expression
.type
=== "AssignmentExpression" &&
418 body
.expression
.operator
=== "=" &&
419 existence
!== null &&
420 astUtils
.isSameReference(existence
.reference
, body
.expression
.left
)
425 data
: { operator
: `${existence.operator}=` }
428 messageId
: "convertIf",
429 data
: { operator
: `${existence.operator}=` },
431 if (sourceCode
.getCommentsInside(ifNode
).length
> 0) {
435 const firstBodyToken
= sourceCode
.getFirstToken(body
);
436 const prevToken
= sourceCode
.getTokenBefore(ifNode
);
439 prevToken
!== null &&
440 prevToken
.value
!== ";" &&
441 prevToken
.value
!== "{" &&
442 firstBodyToken
.type
!== "Identifier" &&
443 firstBodyToken
.type
!== "Keyword"
446 // Do not fix if the fixed statement could be part of the previous statement (eg. fn() if (a == null) (a) = b --> fn()(a) ??= b)
451 const operatorToken
= getOperatorToken(body
.expression
);
453 yield ruleFixer
.insertTextBefore(operatorToken
, existence
.operator
); // -> if (foo) foo ||= bar
455 yield ruleFixer
.removeRange([ifNode
.range
[0], body
.range
[0]]); // -> foo ||= bar
457 yield ruleFixer
.removeRange([body
.range
[1], ifNode
.range
[1]]); // -> foo ||= bar, only present if "if" had a body
459 const nextToken
= sourceCode
.getTokenAfter(body
.expression
);
461 if (hasBody
&& (nextToken
!== null && nextToken
.value
!== ";")) {
462 yield ruleFixer
.insertTextAfter(ifNode
, ";");
466 const shouldBeFixed
= cannotBeGetter(existence
.reference
) ||
467 (ifNode
.test
.type
!== "LogicalExpression" && accessesSingleProperty(existence
.reference
));
469 context
.report(createConditionalFixer(descriptor
, suggestion
, shouldBeFixed
));