|
| 1 | +/** |
| 2 | + * @file Prefer AbortSignal.abort() for already-aborted signals. |
| 3 | + */ |
| 4 | +'use strict'; |
| 5 | + |
| 6 | +const message = 'Use AbortSignal.abort() instead of creating and aborting an AbortController.'; |
| 7 | + |
| 8 | +function isAbortControllerConstruction(node) { |
| 9 | + return node?.type === 'NewExpression' && |
| 10 | + node.callee.type === 'Identifier' && |
| 11 | + node.callee.name === 'AbortController' && |
| 12 | + node.arguments.length === 0; |
| 13 | +} |
| 14 | + |
| 15 | +function isIdentifier(node, name) { |
| 16 | + return node?.type === 'Identifier' && node.name === name; |
| 17 | +} |
| 18 | + |
| 19 | +function isProperty(node, name) { |
| 20 | + return !node.computed && isIdentifier(node.property, name); |
| 21 | +} |
| 22 | + |
| 23 | +function isAbortCallStatement(node, name) { |
| 24 | + const expression = node?.expression; |
| 25 | + const callee = expression?.callee; |
| 26 | + return node?.type === 'ExpressionStatement' && |
| 27 | + expression.type === 'CallExpression' && |
| 28 | + callee.type === 'MemberExpression' && |
| 29 | + isIdentifier(callee.object, name) && |
| 30 | + isProperty(callee, 'abort') && |
| 31 | + expression.arguments.length <= 1; |
| 32 | +} |
| 33 | + |
| 34 | +function isSignalReference(reference, name) { |
| 35 | + const { identifier } = reference; |
| 36 | + const parent = identifier.parent; |
| 37 | + return isIdentifier(identifier, name) && |
| 38 | + parent?.type === 'MemberExpression' && |
| 39 | + parent.object === identifier && |
| 40 | + isProperty(parent, 'signal'); |
| 41 | +} |
| 42 | + |
| 43 | +function isAbortReference(reference, abortStatement, name) { |
| 44 | + const { identifier } = reference; |
| 45 | + const parent = identifier.parent; |
| 46 | + return isIdentifier(identifier, name) && |
| 47 | + parent?.type === 'MemberExpression' && |
| 48 | + parent.object === identifier && |
| 49 | + isProperty(parent, 'abort') && |
| 50 | + parent.parent === abortStatement.expression; |
| 51 | +} |
| 52 | + |
| 53 | +module.exports = { |
| 54 | + meta: { |
| 55 | + fixable: 'code', |
| 56 | + }, |
| 57 | + |
| 58 | + create(context) { |
| 59 | + const sourceCode = context.sourceCode; |
| 60 | + const candidates = []; |
| 61 | + |
| 62 | + function hasCommentsBetween(left, right) { |
| 63 | + return sourceCode.getCommentsBefore(right) |
| 64 | + .some((comment) => comment.range[0] > left.range[1]); |
| 65 | + } |
| 66 | + |
| 67 | + function rangeIncludingTrailingLine(statement) { |
| 68 | + const tokenAfter = sourceCode.getTokenAfter(statement, { includeComments: true }); |
| 69 | + if (tokenAfter && tokenAfter.loc.start.line > statement.loc.end.line) { |
| 70 | + return [statement.range[0], tokenAfter.range[0]]; |
| 71 | + } |
| 72 | + return statement.range; |
| 73 | + } |
| 74 | + |
| 75 | + return { |
| 76 | + VariableDeclarator(node) { |
| 77 | + if (node.id.type !== 'Identifier' || |
| 78 | + !isAbortControllerConstruction(node.init) || |
| 79 | + node.parent.declarations.length !== 1) { |
| 80 | + return; |
| 81 | + } |
| 82 | + |
| 83 | + const variableDeclaration = node.parent; |
| 84 | + const parent = variableDeclaration.parent; |
| 85 | + if (parent.type !== 'BlockStatement' && parent.type !== 'Program') { |
| 86 | + return; |
| 87 | + } |
| 88 | + |
| 89 | + const index = parent.body.indexOf(variableDeclaration); |
| 90 | + const abortStatement = parent.body[index + 1]; |
| 91 | + if (!isAbortCallStatement(abortStatement, node.id.name) || |
| 92 | + hasCommentsBetween(variableDeclaration, abortStatement)) { |
| 93 | + return; |
| 94 | + } |
| 95 | + |
| 96 | + candidates.push({ |
| 97 | + abortStatement, |
| 98 | + declarator: node, |
| 99 | + variableDeclaration, |
| 100 | + }); |
| 101 | + }, |
| 102 | + |
| 103 | + 'Program:exit'() { |
| 104 | + for (const { abortStatement, declarator, variableDeclaration } of candidates) { |
| 105 | + const [variable] = sourceCode.scopeManager.getDeclaredVariables(declarator); |
| 106 | + if (!variable) { |
| 107 | + continue; |
| 108 | + } |
| 109 | + |
| 110 | + const name = declarator.id.name; |
| 111 | + const references = variable.references.filter((reference) => { |
| 112 | + return reference.identifier !== declarator.id; |
| 113 | + }); |
| 114 | + const signalReferences = references.filter((reference) => { |
| 115 | + return isSignalReference(reference, name); |
| 116 | + }); |
| 117 | + const abortReferences = references.filter((reference) => { |
| 118 | + return isAbortReference(reference, abortStatement, name); |
| 119 | + }); |
| 120 | + |
| 121 | + if (references.length !== 2 || |
| 122 | + signalReferences.length !== 1 || |
| 123 | + abortReferences.length !== 1) { |
| 124 | + continue; |
| 125 | + } |
| 126 | + |
| 127 | + const signalNode = signalReferences[0].identifier.parent; |
| 128 | + const abortArguments = abortStatement.expression.arguments; |
| 129 | + const abortReason = abortArguments.length === 0 ? |
| 130 | + '' : sourceCode.getText(abortArguments[0]); |
| 131 | + |
| 132 | + context.report({ |
| 133 | + node: signalNode, |
| 134 | + message, |
| 135 | + fix(fixer) { |
| 136 | + return [ |
| 137 | + fixer.removeRange(rangeIncludingTrailingLine(variableDeclaration)), |
| 138 | + fixer.removeRange(rangeIncludingTrailingLine(abortStatement)), |
| 139 | + fixer.replaceText(signalNode, `AbortSignal.abort(${abortReason})`), |
| 140 | + ]; |
| 141 | + }, |
| 142 | + }); |
| 143 | + } |
| 144 | + }, |
| 145 | + }; |
| 146 | + }, |
| 147 | +}; |
0 commit comments