[d565449] | 1 | /**
|
---|
| 2 | * @fileoverview Rule to replace assignment expressions with logical operator assignment
|
---|
| 3 | * @author Daniel Martens
|
---|
| 4 | */
|
---|
| 5 | "use strict";
|
---|
| 6 |
|
---|
| 7 | //------------------------------------------------------------------------------
|
---|
| 8 | // Requirements
|
---|
| 9 | //------------------------------------------------------------------------------
|
---|
| 10 | const astUtils = require("./utils/ast-utils.js");
|
---|
| 11 |
|
---|
| 12 | //------------------------------------------------------------------------------
|
---|
| 13 | // Helpers
|
---|
| 14 | //------------------------------------------------------------------------------
|
---|
| 15 |
|
---|
| 16 | const baseTypes = new Set(["Identifier", "Super", "ThisExpression"]);
|
---|
| 17 |
|
---|
| 18 | /**
|
---|
| 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 ..."
|
---|
| 23 | */
|
---|
| 24 | function isUndefined(expression, scope) {
|
---|
| 25 | if (expression.type === "Identifier" && expression.name === "undefined") {
|
---|
| 26 | return astUtils.isReferenceToGlobalVariable(scope, expression);
|
---|
| 27 | }
|
---|
| 28 |
|
---|
| 29 | return expression.type === "UnaryExpression" &&
|
---|
| 30 | expression.operator === "void" &&
|
---|
| 31 | expression.argument.type === "Literal" &&
|
---|
| 32 | expression.argument.value === 0;
|
---|
| 33 | }
|
---|
| 34 |
|
---|
| 35 | /**
|
---|
| 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
|
---|
| 39 | */
|
---|
| 40 | function isReference(expression) {
|
---|
| 41 | return (expression.type === "Identifier" && expression.name !== "undefined") ||
|
---|
| 42 | expression.type === "MemberExpression";
|
---|
| 43 | }
|
---|
| 44 |
|
---|
| 45 | /**
|
---|
| 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
|
---|
| 51 | */
|
---|
| 52 | function isImplicitNullishComparison(expression, scope) {
|
---|
| 53 | if (expression.type !== "BinaryExpression" || expression.operator !== "==") {
|
---|
| 54 | return false;
|
---|
| 55 | }
|
---|
| 56 |
|
---|
| 57 | const reference = isReference(expression.left) ? "left" : "right";
|
---|
| 58 | const nullish = reference === "left" ? "right" : "left";
|
---|
| 59 |
|
---|
| 60 | return isReference(expression[reference]) &&
|
---|
| 61 | (astUtils.isNullLiteral(expression[nullish]) || isUndefined(expression[nullish], scope));
|
---|
| 62 | }
|
---|
| 63 |
|
---|
| 64 | /**
|
---|
| 65 | * Condition with two equal comparisons.
|
---|
| 66 | * @param {ASTNode} expression Condition
|
---|
| 67 | * @returns {boolean} True iff matches ? === ? || ? === ?
|
---|
| 68 | */
|
---|
| 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 === "===";
|
---|
| 76 | }
|
---|
| 77 |
|
---|
| 78 | /**
|
---|
| 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
|
---|
| 84 | */
|
---|
| 85 | function isExplicitNullishComparison(expression, scope) {
|
---|
| 86 | if (!isDoubleComparison(expression)) {
|
---|
| 87 | return false;
|
---|
| 88 | }
|
---|
| 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";
|
---|
| 93 |
|
---|
| 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])));
|
---|
| 97 | }
|
---|
| 98 |
|
---|
| 99 | /**
|
---|
| 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
|
---|
| 104 | */
|
---|
| 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);
|
---|
| 110 | }
|
---|
| 111 |
|
---|
| 112 | /**
|
---|
| 113 | * Returns true for:
|
---|
| 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
|
---|
| 120 | */
|
---|
| 121 | function getExistence(expression, scope) {
|
---|
| 122 | const isNegated = expression.type === "UnaryExpression" && expression.operator === "!";
|
---|
| 123 | const base = isNegated ? expression.argument : expression;
|
---|
| 124 |
|
---|
| 125 | switch (true) {
|
---|
| 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;
|
---|
| 137 | }
|
---|
| 138 | }
|
---|
| 139 |
|
---|
| 140 | /**
|
---|
| 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
|
---|
| 144 | */
|
---|
| 145 | function isInsideWithBlock(node) {
|
---|
| 146 | if (node.type === "Program") {
|
---|
| 147 | return false;
|
---|
| 148 | }
|
---|
| 149 |
|
---|
| 150 | return node.parent.type === "WithStatement" && node.parent.body === node ? true : isInsideWithBlock(node.parent);
|
---|
| 151 | }
|
---|
| 152 |
|
---|
| 153 | /**
|
---|
| 154 | * Gets the leftmost operand of a consecutive logical expression.
|
---|
| 155 | * @param {SourceCode} sourceCode The ESLint source code object
|
---|
| 156 | * @param {LogicalExpression} node LogicalExpression
|
---|
| 157 | * @returns {Expression} Leftmost operand
|
---|
| 158 | */
|
---|
| 159 | function getLeftmostOperand(sourceCode, node) {
|
---|
| 160 | let left = node.left;
|
---|
| 161 |
|
---|
| 162 | while (left.type === "LogicalExpression" && left.operator === node.operator) {
|
---|
| 163 |
|
---|
| 164 | if (astUtils.isParenthesised(sourceCode, left)) {
|
---|
| 165 |
|
---|
| 166 | /*
|
---|
| 167 | * It should have associativity,
|
---|
| 168 | * but ignore it if use parentheses to make the evaluation order clear.
|
---|
| 169 | */
|
---|
| 170 | return left;
|
---|
| 171 | }
|
---|
| 172 | left = left.left;
|
---|
| 173 | }
|
---|
| 174 | return left;
|
---|
| 175 |
|
---|
| 176 | }
|
---|
| 177 |
|
---|
| 178 | //------------------------------------------------------------------------------
|
---|
| 179 | // Rule Definition
|
---|
| 180 | //------------------------------------------------------------------------------
|
---|
| 181 | /** @type {import('../shared/types').Rule} */
|
---|
| 182 | module.exports = {
|
---|
| 183 | meta: {
|
---|
| 184 | type: "suggestion",
|
---|
| 185 |
|
---|
| 186 | docs: {
|
---|
| 187 | description: "Require or disallow logical assignment operator shorthand",
|
---|
| 188 | recommended: false,
|
---|
| 189 | url: "https://eslint.org/docs/latest/rules/logical-assignment-operators"
|
---|
| 190 | },
|
---|
| 191 |
|
---|
| 192 | schema: {
|
---|
| 193 | type: "array",
|
---|
| 194 | oneOf: [{
|
---|
| 195 | items: [
|
---|
| 196 | { const: "always" },
|
---|
| 197 | {
|
---|
| 198 | type: "object",
|
---|
| 199 | properties: {
|
---|
| 200 | enforceForIfStatements: {
|
---|
| 201 | type: "boolean"
|
---|
| 202 | }
|
---|
| 203 | },
|
---|
| 204 | additionalProperties: false
|
---|
| 205 | }
|
---|
| 206 | ],
|
---|
| 207 | minItems: 0, // 0 for allowing passing no options
|
---|
| 208 | maxItems: 2
|
---|
| 209 | }, {
|
---|
| 210 | items: [{ const: "never" }],
|
---|
| 211 | minItems: 1,
|
---|
| 212 | maxItems: 1
|
---|
| 213 | }]
|
---|
| 214 | },
|
---|
| 215 | fixable: "code",
|
---|
| 216 | hasSuggestions: true,
|
---|
| 217 | messages: {
|
---|
| 218 | assignment: "Assignment (=) can be replaced with operator assignment ({{operator}}).",
|
---|
| 219 | useLogicalOperator: "Convert this assignment to use the operator {{ operator }}.",
|
---|
| 220 | logical: "Logical expression can be replaced with an assignment ({{ operator }}).",
|
---|
| 221 | convertLogical: "Replace this logical expression with an assignment with the operator {{ operator }}.",
|
---|
| 222 | if: "'if' statement can be replaced with a logical operator assignment with operator {{ operator }}.",
|
---|
| 223 | convertIf: "Replace this 'if' statement with a logical assignment with operator {{ operator }}.",
|
---|
| 224 | unexpected: "Unexpected logical operator assignment ({{operator}}) shorthand.",
|
---|
| 225 | separate: "Separate the logical assignment into an assignment with a logical operator."
|
---|
| 226 | }
|
---|
| 227 | },
|
---|
| 228 |
|
---|
| 229 | create(context) {
|
---|
| 230 | const mode = context.options[0] === "never" ? "never" : "always";
|
---|
| 231 | const checkIf = mode === "always" && context.options.length > 1 && context.options[1].enforceForIfStatements;
|
---|
| 232 | const sourceCode = context.sourceCode;
|
---|
| 233 | const isStrict = sourceCode.getScope(sourceCode.ast).isStrict;
|
---|
| 234 |
|
---|
| 235 | /**
|
---|
| 236 | * Returns false if the access could be a getter
|
---|
| 237 | * @param {ASTNode} node Assignment expression
|
---|
| 238 | * @returns {boolean} True iff the fix is safe
|
---|
| 239 | */
|
---|
| 240 | function cannotBeGetter(node) {
|
---|
| 241 | return node.type === "Identifier" &&
|
---|
| 242 | (isStrict || !isInsideWithBlock(node));
|
---|
| 243 | }
|
---|
| 244 |
|
---|
| 245 | /**
|
---|
| 246 | * Check whether only a single property is accessed
|
---|
| 247 | * @param {ASTNode} node reference
|
---|
| 248 | * @returns {boolean} True iff a single property is accessed
|
---|
| 249 | */
|
---|
| 250 | function accessesSingleProperty(node) {
|
---|
| 251 | if (!isStrict && isInsideWithBlock(node)) {
|
---|
| 252 | return node.type === "Identifier";
|
---|
| 253 | }
|
---|
| 254 |
|
---|
| 255 | return node.type === "MemberExpression" &&
|
---|
| 256 | baseTypes.has(node.object.type) &&
|
---|
| 257 | (!node.computed || (node.property.type !== "MemberExpression" && node.property.type !== "ChainExpression"));
|
---|
| 258 | }
|
---|
| 259 |
|
---|
| 260 | /**
|
---|
| 261 | * Adds a fixer or suggestion whether on the fix is safe.
|
---|
| 262 | * @param {{ messageId: string, node: ASTNode }} descriptor Report descriptor without fix or suggest
|
---|
| 263 | * @param {{ messageId: string, fix: Function }} suggestion Adds the fix or the whole suggestion as only element in "suggest" to suggestion
|
---|
| 264 | * @param {boolean} shouldBeFixed Fix iff the condition is true
|
---|
| 265 | * @returns {Object} Descriptor with either an added fix or suggestion
|
---|
| 266 | */
|
---|
| 267 | function createConditionalFixer(descriptor, suggestion, shouldBeFixed) {
|
---|
| 268 | if (shouldBeFixed) {
|
---|
| 269 | return {
|
---|
| 270 | ...descriptor,
|
---|
| 271 | fix: suggestion.fix
|
---|
| 272 | };
|
---|
| 273 | }
|
---|
| 274 |
|
---|
| 275 | return {
|
---|
| 276 | ...descriptor,
|
---|
| 277 | suggest: [suggestion]
|
---|
| 278 | };
|
---|
| 279 | }
|
---|
| 280 |
|
---|
| 281 |
|
---|
| 282 | /**
|
---|
| 283 | * Returns the operator token for assignments and binary expressions
|
---|
| 284 | * @param {ASTNode} node AssignmentExpression or BinaryExpression
|
---|
| 285 | * @returns {import('eslint').AST.Token} Operator token between the left and right expression
|
---|
| 286 | */
|
---|
| 287 | function getOperatorToken(node) {
|
---|
| 288 | return sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator);
|
---|
| 289 | }
|
---|
| 290 |
|
---|
| 291 | if (mode === "never") {
|
---|
| 292 | return {
|
---|
| 293 |
|
---|
| 294 | // foo ||= bar
|
---|
| 295 | "AssignmentExpression"(assignment) {
|
---|
| 296 | if (!astUtils.isLogicalAssignmentOperator(assignment.operator)) {
|
---|
| 297 | return;
|
---|
| 298 | }
|
---|
| 299 |
|
---|
| 300 | const descriptor = {
|
---|
| 301 | messageId: "unexpected",
|
---|
| 302 | node: assignment,
|
---|
| 303 | data: { operator: assignment.operator }
|
---|
| 304 | };
|
---|
| 305 | const suggestion = {
|
---|
| 306 | messageId: "separate",
|
---|
| 307 | *fix(ruleFixer) {
|
---|
| 308 | if (sourceCode.getCommentsInside(assignment).length > 0) {
|
---|
| 309 | return;
|
---|
| 310 | }
|
---|
| 311 |
|
---|
| 312 | const operatorToken = getOperatorToken(assignment);
|
---|
| 313 |
|
---|
| 314 | // -> foo = bar
|
---|
| 315 | yield ruleFixer.replaceText(operatorToken, "=");
|
---|
| 316 |
|
---|
| 317 | const assignmentText = sourceCode.getText(assignment.left);
|
---|
| 318 | const operator = assignment.operator.slice(0, -1);
|
---|
| 319 |
|
---|
| 320 | // -> foo = foo || bar
|
---|
| 321 | yield ruleFixer.insertTextAfter(operatorToken, ` ${assignmentText} ${operator}`);
|
---|
| 322 |
|
---|
| 323 | const precedence = astUtils.getPrecedence(assignment.right) <= astUtils.getPrecedence({ type: "LogicalExpression", operator });
|
---|
| 324 |
|
---|
| 325 | // ?? and || / && cannot be mixed but have same precedence
|
---|
| 326 | const mixed = assignment.operator === "??=" && astUtils.isLogicalExpression(assignment.right);
|
---|
| 327 |
|
---|
| 328 | if (!astUtils.isParenthesised(sourceCode, assignment.right) && (precedence || mixed)) {
|
---|
| 329 |
|
---|
| 330 | // -> foo = foo || (bar)
|
---|
| 331 | yield ruleFixer.insertTextBefore(assignment.right, "(");
|
---|
| 332 | yield ruleFixer.insertTextAfter(assignment.right, ")");
|
---|
| 333 | }
|
---|
| 334 | }
|
---|
| 335 | };
|
---|
| 336 |
|
---|
| 337 | context.report(createConditionalFixer(descriptor, suggestion, cannotBeGetter(assignment.left)));
|
---|
| 338 | }
|
---|
| 339 | };
|
---|
| 340 | }
|
---|
| 341 |
|
---|
| 342 | return {
|
---|
| 343 |
|
---|
| 344 | // foo = foo || bar
|
---|
| 345 | "AssignmentExpression[operator='='][right.type='LogicalExpression']"(assignment) {
|
---|
| 346 | const leftOperand = getLeftmostOperand(sourceCode, assignment.right);
|
---|
| 347 |
|
---|
| 348 | if (!astUtils.isSameReference(assignment.left, leftOperand)
|
---|
| 349 | ) {
|
---|
| 350 | return;
|
---|
| 351 | }
|
---|
| 352 |
|
---|
| 353 | const descriptor = {
|
---|
| 354 | messageId: "assignment",
|
---|
| 355 | node: assignment,
|
---|
| 356 | data: { operator: `${assignment.right.operator}=` }
|
---|
| 357 | };
|
---|
| 358 | const suggestion = {
|
---|
| 359 | messageId: "useLogicalOperator",
|
---|
| 360 | data: { operator: `${assignment.right.operator}=` },
|
---|
| 361 | *fix(ruleFixer) {
|
---|
| 362 | if (sourceCode.getCommentsInside(assignment).length > 0) {
|
---|
| 363 | return;
|
---|
| 364 | }
|
---|
| 365 |
|
---|
| 366 | // No need for parenthesis around the assignment based on precedence as the precedence stays the same even with changed operator
|
---|
| 367 | const assignmentOperatorToken = getOperatorToken(assignment);
|
---|
| 368 |
|
---|
| 369 | // -> foo ||= foo || bar
|
---|
| 370 | yield ruleFixer.insertTextBefore(assignmentOperatorToken, assignment.right.operator);
|
---|
| 371 |
|
---|
| 372 | // -> foo ||= bar
|
---|
| 373 | const logicalOperatorToken = getOperatorToken(leftOperand.parent);
|
---|
| 374 | const firstRightOperandToken = sourceCode.getTokenAfter(logicalOperatorToken);
|
---|
| 375 |
|
---|
| 376 | yield ruleFixer.removeRange([leftOperand.parent.range[0], firstRightOperandToken.range[0]]);
|
---|
| 377 | }
|
---|
| 378 | };
|
---|
| 379 |
|
---|
| 380 | context.report(createConditionalFixer(descriptor, suggestion, cannotBeGetter(assignment.left)));
|
---|
| 381 | },
|
---|
| 382 |
|
---|
| 383 | // foo || (foo = bar)
|
---|
| 384 | 'LogicalExpression[right.type="AssignmentExpression"][right.operator="="]'(logical) {
|
---|
| 385 |
|
---|
| 386 | // Right side has to be parenthesized, otherwise would be parsed as (foo || foo) = bar which is illegal
|
---|
| 387 | if (isReference(logical.left) && astUtils.isSameReference(logical.left, logical.right.left)) {
|
---|
| 388 | const descriptor = {
|
---|
| 389 | messageId: "logical",
|
---|
| 390 | node: logical,
|
---|
| 391 | data: { operator: `${logical.operator}=` }
|
---|
| 392 | };
|
---|
| 393 | const suggestion = {
|
---|
| 394 | messageId: "convertLogical",
|
---|
| 395 | data: { operator: `${logical.operator}=` },
|
---|
| 396 | *fix(ruleFixer) {
|
---|
| 397 | if (sourceCode.getCommentsInside(logical).length > 0) {
|
---|
| 398 | return;
|
---|
| 399 | }
|
---|
| 400 |
|
---|
| 401 | const parentPrecedence = astUtils.getPrecedence(logical.parent);
|
---|
| 402 | const requiresOuterParenthesis = logical.parent.type !== "ExpressionStatement" && (
|
---|
| 403 | parentPrecedence === -1 ||
|
---|
| 404 | astUtils.getPrecedence({ type: "AssignmentExpression" }) < parentPrecedence
|
---|
| 405 | );
|
---|
| 406 |
|
---|
| 407 | if (!astUtils.isParenthesised(sourceCode, logical) && requiresOuterParenthesis) {
|
---|
| 408 | yield ruleFixer.insertTextBefore(logical, "(");
|
---|
| 409 | yield ruleFixer.insertTextAfter(logical, ")");
|
---|
| 410 | }
|
---|
| 411 |
|
---|
| 412 | // Also removes all opening parenthesis
|
---|
| 413 | yield ruleFixer.removeRange([logical.range[0], logical.right.range[0]]); // -> foo = bar)
|
---|
| 414 |
|
---|
| 415 | // Also removes all ending parenthesis
|
---|
| 416 | yield ruleFixer.removeRange([logical.right.range[1], logical.range[1]]); // -> foo = bar
|
---|
| 417 |
|
---|
| 418 | const operatorToken = getOperatorToken(logical.right);
|
---|
| 419 |
|
---|
| 420 | yield ruleFixer.insertTextBefore(operatorToken, logical.operator); // -> foo ||= bar
|
---|
| 421 | }
|
---|
| 422 | };
|
---|
| 423 | const fix = cannotBeGetter(logical.left) || accessesSingleProperty(logical.left);
|
---|
| 424 |
|
---|
| 425 | context.report(createConditionalFixer(descriptor, suggestion, fix));
|
---|
| 426 | }
|
---|
| 427 | },
|
---|
| 428 |
|
---|
| 429 | // if (foo) foo = bar
|
---|
| 430 | "IfStatement[alternate=null]"(ifNode) {
|
---|
| 431 | if (!checkIf) {
|
---|
| 432 | return;
|
---|
| 433 | }
|
---|
| 434 |
|
---|
| 435 | const hasBody = ifNode.consequent.type === "BlockStatement";
|
---|
| 436 |
|
---|
| 437 | if (hasBody && ifNode.consequent.body.length !== 1) {
|
---|
| 438 | return;
|
---|
| 439 | }
|
---|
| 440 |
|
---|
| 441 | const body = hasBody ? ifNode.consequent.body[0] : ifNode.consequent;
|
---|
| 442 | const scope = sourceCode.getScope(ifNode);
|
---|
| 443 | const existence = getExistence(ifNode.test, scope);
|
---|
| 444 |
|
---|
| 445 | if (
|
---|
| 446 | body.type === "ExpressionStatement" &&
|
---|
| 447 | body.expression.type === "AssignmentExpression" &&
|
---|
| 448 | body.expression.operator === "=" &&
|
---|
| 449 | existence !== null &&
|
---|
| 450 | astUtils.isSameReference(existence.reference, body.expression.left)
|
---|
| 451 | ) {
|
---|
| 452 | const descriptor = {
|
---|
| 453 | messageId: "if",
|
---|
| 454 | node: ifNode,
|
---|
| 455 | data: { operator: `${existence.operator}=` }
|
---|
| 456 | };
|
---|
| 457 | const suggestion = {
|
---|
| 458 | messageId: "convertIf",
|
---|
| 459 | data: { operator: `${existence.operator}=` },
|
---|
| 460 | *fix(ruleFixer) {
|
---|
| 461 | if (sourceCode.getCommentsInside(ifNode).length > 0) {
|
---|
| 462 | return;
|
---|
| 463 | }
|
---|
| 464 |
|
---|
| 465 | const firstBodyToken = sourceCode.getFirstToken(body);
|
---|
| 466 | const prevToken = sourceCode.getTokenBefore(ifNode);
|
---|
| 467 |
|
---|
| 468 | if (
|
---|
| 469 | prevToken !== null &&
|
---|
| 470 | prevToken.value !== ";" &&
|
---|
| 471 | prevToken.value !== "{" &&
|
---|
| 472 | firstBodyToken.type !== "Identifier" &&
|
---|
| 473 | firstBodyToken.type !== "Keyword"
|
---|
| 474 | ) {
|
---|
| 475 |
|
---|
| 476 | // Do not fix if the fixed statement could be part of the previous statement (eg. fn() if (a == null) (a) = b --> fn()(a) ??= b)
|
---|
| 477 | return;
|
---|
| 478 | }
|
---|
| 479 |
|
---|
| 480 |
|
---|
| 481 | const operatorToken = getOperatorToken(body.expression);
|
---|
| 482 |
|
---|
| 483 | yield ruleFixer.insertTextBefore(operatorToken, existence.operator); // -> if (foo) foo ||= bar
|
---|
| 484 |
|
---|
| 485 | yield ruleFixer.removeRange([ifNode.range[0], body.range[0]]); // -> foo ||= bar
|
---|
| 486 |
|
---|
| 487 | yield ruleFixer.removeRange([body.range[1], ifNode.range[1]]); // -> foo ||= bar, only present if "if" had a body
|
---|
| 488 |
|
---|
| 489 | const nextToken = sourceCode.getTokenAfter(body.expression);
|
---|
| 490 |
|
---|
| 491 | if (hasBody && (nextToken !== null && nextToken.value !== ";")) {
|
---|
| 492 | yield ruleFixer.insertTextAfter(ifNode, ";");
|
---|
| 493 | }
|
---|
| 494 | }
|
---|
| 495 | };
|
---|
| 496 | const shouldBeFixed = cannotBeGetter(existence.reference) ||
|
---|
| 497 | (ifNode.test.type !== "LogicalExpression" && accessesSingleProperty(existence.reference));
|
---|
| 498 |
|
---|
| 499 | context.report(createConditionalFixer(descriptor, suggestion, shouldBeFixed));
|
---|
| 500 | }
|
---|
| 501 | }
|
---|
| 502 | };
|
---|
| 503 | }
|
---|
| 504 | };
|
---|