| 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 | };
|
|---|