| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent f32b412 commit 5755712
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -161,6 +161,7 @@ export default [ | |||
| 161 | 161 | 'node-core/require-common-first': 'error', | |
| 162 | 162 | 'node-core/no-duplicate-requires': 'off', | |
| 163 | 163 | 'node-core/must-call-assert': 'error', | |
| 164 | + 'node-core/prefer-abort-signal-abort': 'error', | ||
| 164 | 165 | }, | |
| 165 | 166 | }, | |
| 166 | 167 | { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -161,6 +161,7 @@ test('AbortController inspection depth 1 or null works', () => { | |||
| 161 | 161 | ||
| 162 | 162 | test('AbortSignal reason is set correctly', () => { | |
| 163 | 163 | // Test AbortSignal.reason | |
| 164 | + // eslint-disable-next-line node-core/prefer-abort-signal-abort | ||
| 164 | 165 | const ac = new AbortController(); | |
| 165 | 166 | ac.abort('reason'); | |
| 166 | 167 | assert.strictEqual(ac.signal.reason, 'reason'); | |
@@ -235,6 +236,7 @@ test('AbortSignal.reason should default', () => { | |||
| 235 | 236 | assert.ok(signal.reason instanceof DOMException); | |
| 236 | 237 | assert.strictEqual(signal.reason.code, 20); | |
| 237 | 238 | ||
| 239 | + // eslint-disable-next-line node-core/prefer-abort-signal-abort | ||
| 238 | 240 | const ac = new AbortController(); | |
| 239 | 241 | ac.abort(); | |
| 240 | 242 | assert.ok(ac.signal.reason instanceof DOMException); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,118 @@ | |||
| 1 | + 'use strict'; | ||
| 2 | + | ||
| 3 | + const common = require('../common'); | ||
| 4 | + if ((!common.hasCrypto) || (!common.hasIntl)) { | ||
| 5 | + common.skip('ESLint tests require crypto and Intl'); | ||
| 6 | + } | ||
| 7 | + | ||
| 8 | + common.skipIfEslintMissing(); | ||
| 9 | + | ||
| 10 | + const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; | ||
| 11 | + const rule = require('../../tools/eslint-rules/prefer-abort-signal-abort'); | ||
| 12 | + | ||
| 13 | + const message = 'Use AbortSignal.abort() instead of creating and aborting an AbortController.'; | ||
| 14 | + | ||
| 15 | + new RuleTester().run('prefer-abort-signal-abort', rule, { | ||
| 16 | + valid: [ | ||
| 17 | + 'const signal = AbortSignal.abort();', | ||
| 18 | + ` | ||
| 19 | + const controller = new AbortController(); | ||
| 20 | + controller.abort(); | ||
| 21 | + controller.abort(); | ||
| 22 | + fn(controller.signal); | ||
| 23 | + `, | ||
| 24 | + ` | ||
| 25 | + const controller = new AbortController(); | ||
| 26 | + controller.abort(); | ||
| 27 | + console.log(controller); | ||
| 28 | + fn(controller.signal); | ||
| 29 | + `, | ||
| 30 | + ` | ||
| 31 | + const controller = new AbortController(); | ||
| 32 | + // This comment should not be removed. | ||
| 33 | + controller.abort(); | ||
| 34 | + fn(controller.signal); | ||
| 35 | + `, | ||
| 36 | + ` | ||
| 37 | + const controller = new AbortController(); | ||
| 38 | + setImmediate(() => controller.abort()); | ||
| 39 | + fn(controller.signal); | ||
| 40 | + `, | ||
| 41 | + ` | ||
| 42 | + const controller = new AbortController(); | ||
| 43 | + controller.abort('reason', 'extra'); | ||
| 44 | + fn(controller.signal); | ||
| 45 | + `, | ||
| 46 | + ], | ||
| 47 | + invalid: [ | ||
| 48 | + { | ||
| 49 | + code: ` | ||
| 50 | + const controller = new AbortController(); | ||
| 51 | + controller.abort(); | ||
| 52 | + fn(controller.signal); | ||
| 53 | + `, | ||
| 54 | + errors: [{ message }], | ||
| 55 | + output: ` | ||
| 56 | + fn(AbortSignal.abort()); | ||
| 57 | + `, | ||
| 58 | + }, | ||
| 59 | + { | ||
| 60 | + code: ` | ||
| 61 | + const abortController = new AbortController(); | ||
| 62 | + abortController.abort(new Error('aborted')); | ||
| 63 | + fn({ signal: abortController.signal }); | ||
| 64 | + `, | ||
| 65 | + errors: [{ message }], | ||
| 66 | + output: ` | ||
| 67 | + fn({ signal: AbortSignal.abort(new Error('aborted')) }); | ||
| 68 | + `, | ||
| 69 | + }, | ||
| 70 | + { | ||
| 71 | + code: ` | ||
| 72 | + { | ||
| 73 | + const ac = new AbortController(); | ||
| 74 | + ac.abort(); | ||
| 75 | + await wait({ signal: ac.signal }); | ||
| 76 | + } | ||
| 77 | + `, | ||
| 78 | + errors: [{ message }], | ||
| 79 | + output: ` | ||
| 80 | + { | ||
| 81 | + await wait({ signal: AbortSignal.abort() }); | ||
| 82 | + } | ||
| 83 | + `, | ||
| 84 | + }, | ||
| 85 | + { | ||
| 86 | + code: ` | ||
| 87 | + { | ||
| 88 | + const controller = new AbortController(); | ||
| 89 | + controller.abort(); | ||
| 90 | + fn(controller.signal, controller.signal); | ||
| 91 | + } | ||
| 92 | + `, | ||
| 93 | + errors: [{ message }], | ||
| 94 | + output: ` | ||
| 95 | + { | ||
| 96 | + const controller = AbortSignal.abort(); | ||
| 97 | + fn(controller, controller); | ||
| 98 | + } | ||
| 99 | + `, | ||
| 100 | + }, | ||
| 101 | + { | ||
| 102 | + code: ` | ||
| 103 | + { | ||
| 104 | + const controller = new AbortController(); | ||
| 105 | + controller.abort("reason"); | ||
| 106 | + fn(controller.signal, controller.signal); | ||
| 107 | + } | ||
| 108 | + `, | ||
| 109 | + errors: [{ message }], | ||
| 110 | + output: ` | ||
| 111 | + { | ||
| 112 | + const controller = AbortSignal.abort("reason"); | ||
| 113 | + fn(controller, controller); | ||
| 114 | + } | ||
| 115 | + `, | ||
| 116 | + }, | ||
| 117 | + ] | ||
| 118 | + }); | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -33,12 +33,11 @@ const stream = await clientSession.createBidirectionalStream(); | |||
| 33 | 33 | const w = stream.writer; | |
| 34 | 34 | ||
| 35 | 35 | // Create an already-aborted signal. | |
| 36 | - const ac = new AbortController(); | ||
| 37 | - ac.abort(new Error('already aborted')); | ||
| 36 | + const signal = AbortSignal.abort(new Error('already aborted')); | ||
| 38 | 37 | ||
| 39 | 38 | // write() with an already-aborted signal should reject immediately. | |
| 40 | 39 | await rejects( | |
| 41 | - w.write(encoder.encode('data'), { signal: ac.signal }), | ||
| 40 | + w.write(encoder.encode('data'), { signal }), | ||
| 42 | 41 | { message: 'already aborted' }, | |
| 43 | 42 | ); | |
| 44 | 43 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,154 @@ | |||
| 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 !== (1 + signalReferences.length) || | ||
| 122 | + abortReferences.length !== 1) { | ||
| 123 | + continue; | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + const signalNode = signalReferences[0].identifier.parent; | ||
| 127 | + const abortArguments = abortStatement.expression.arguments; | ||
| 128 | + const abortReason = abortArguments.length === 0 ? | ||
| 129 | + '' : sourceCode.getText(abortArguments[0]); | ||
| 130 | + | ||
| 131 | + context.report({ | ||
| 132 | + node: declarator, | ||
| 133 | + message, | ||
| 134 | + fix(fixer) { | ||
| 135 | + const abortSignalCreationCall = `AbortSignal.abort(${abortReason})`; | ||
| 136 | + if (signalReferences.length > 1) { | ||
| 137 | + return [ | ||
| 138 | + fixer.replaceText(declarator.init, abortSignalCreationCall), | ||
| 139 | + fixer.removeRange(rangeIncludingTrailingLine(abortStatement)), | ||
| 140 | + ...signalReferences.map(({ identifier: { parent } }) => fixer.replaceText(parent, name)), | ||
| 141 | + ]; | ||
| 142 | + } | ||
| 143 | + return [ | ||
| 144 | + fixer.removeRange(rangeIncludingTrailingLine(variableDeclaration)), | ||
| 145 | + fixer.removeRange(rangeIncludingTrailingLine(abortStatement)), | ||
| 146 | + fixer.replaceText(signalNode, abortSignalCreationCall), | ||
| 147 | + ]; | ||
| 148 | + }, | ||
| 149 | + }); | ||
| 150 | + } | ||
| 151 | + }, | ||
| 152 | + }; | ||
| 153 | + }, | ||
| 154 | + }; | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments