diff --git a/packages/eslint-plugin/src/rules/no-misused-spread.ts b/packages/eslint-plugin/src/rules/no-misused-spread.ts index f6fd7610706a..74b2ddb6abf5 100644 --- a/packages/eslint-plugin/src/rules/no-misused-spread.ts +++ b/packages/eslint-plugin/src/rules/no-misused-spread.ts @@ -1,5 +1,6 @@ -import type { TSESTree } from '@typescript-eslint/utils'; +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -9,11 +10,13 @@ import { createRule, getConstrainedTypeAtLocation, getParserServices, + getWrappingFixer, isBuiltinSymbolLike, isPromiseLike, isTypeFlagSet, readonlynessOptionsSchema, typeMatchesSomeSpecifier, + isHigherPrecedenceThanAwait, } from '../util'; type Options = [ @@ -23,6 +26,7 @@ type Options = [ ]; type MessageIds = + | 'addAwait' | 'noArraySpreadInObject' | 'noClassDeclarationSpreadInObject' | 'noClassInstanceSpreadInObject' @@ -30,7 +34,8 @@ type MessageIds = | 'noIterableSpreadInObject' | 'noMapSpreadInObject' | 'noPromiseSpreadInObject' - | 'noStringSpread'; + | 'noStringSpread' + | 'replaceMapSpreadInObject'; export default createRule({ name: 'no-misused-spread', @@ -42,7 +47,9 @@ export default createRule({ recommended: 'strict', requiresTypeChecking: true, }, + hasSuggestions: true, messages: { + addAwait: 'Add await operator.', noArraySpreadInObject: 'Using the spread operator on an array in an object will result in a list of indices.', noClassDeclarationSpreadInObject: @@ -64,6 +71,8 @@ export default createRule({ 'Consider using `Intl.Segmenter` for locale-aware string decomposition.', "Otherwise, if you don't need to preserve emojis or other non-Ascii characters, disable this lint rule on this line or configure the 'allow' rule option.", ].join('\n'), + replaceMapSpreadInObject: + 'Replace map spread in object with `Object.fromEntries()`', }, schema: [ { @@ -104,6 +113,65 @@ export default createRule({ } } + function getMapSpreadSuggestions( + node: TSESTree.JSXSpreadAttribute | TSESTree.SpreadElement, + type: ts.Type, + ): TSESLint.ReportSuggestionArray | null { + const types = tsutils.unionTypeParts(type); + if (types.some(t => !isMap(services.program, t))) { + return null; + } + + if ( + node.parent.type === AST_NODE_TYPES.ObjectExpression && + node.parent.properties.length === 1 + ) { + return [ + { + messageId: 'replaceMapSpreadInObject', + fix: getWrappingFixer({ + node: node.parent, + innerNode: node.argument, + sourceCode: context.sourceCode, + wrap: code => `Object.fromEntries(${code})`, + }), + }, + ]; + } + + return [ + { + messageId: 'replaceMapSpreadInObject', + fix: getWrappingFixer({ + node: node.argument, + sourceCode: context.sourceCode, + wrap: code => `Object.fromEntries(${code})`, + }), + }, + ]; + } + + function getPromiseSpreadSuggestions( + node: TSESTree.Expression, + ): TSESLint.ReportSuggestionArray { + const isHighPrecendence = isHigherPrecedenceThanAwait( + services.esTreeNodeToTSNodeMap.get(node), + ); + + return [ + { + messageId: 'addAwait', + fix: fixer => + isHighPrecendence + ? fixer.insertTextBefore(node, 'await ') + : [ + fixer.insertTextBefore(node, 'await ('), + fixer.insertTextAfter(node, ')'), + ], + }, + ]; + } + function checkObjectSpread( node: TSESTree.JSXSpreadAttribute | TSESTree.SpreadElement, ): void { @@ -117,6 +185,7 @@ export default createRule({ context.report({ node, messageId: 'noPromiseSpreadInObject', + suggest: getPromiseSpreadSuggestions(node.argument), }); return; @@ -135,6 +204,7 @@ export default createRule({ context.report({ node, messageId: 'noMapSpreadInObject', + suggest: getMapSpreadSuggestions(node, type), }); return; diff --git a/packages/eslint-plugin/src/rules/return-await.ts b/packages/eslint-plugin/src/rules/return-await.ts index a040bbb9463e..0ee20eea92a8 100644 --- a/packages/eslint-plugin/src/rules/return-await.ts +++ b/packages/eslint-plugin/src/rules/return-await.ts @@ -12,8 +12,8 @@ import { isAwaitKeyword, needsToBeAwaited, nullThrows, + isHigherPrecedenceThanAwait, } from '../util'; -import { getOperatorPrecedence } from '../util/getOperatorPrecedence'; type FunctionNode = | TSESTree.ArrowFunctionExpression @@ -278,18 +278,6 @@ export default createRule({ ]; } - function isHigherPrecedenceThanAwait(node: ts.Node): boolean { - const operator = ts.isBinaryExpression(node) - ? node.operatorToken.kind - : ts.SyntaxKind.Unknown; - const nodePrecedence = getOperatorPrecedence(node.kind, operator); - const awaitPrecedence = getOperatorPrecedence( - ts.SyntaxKind.AwaitExpression, - ts.SyntaxKind.Unknown, - ); - return nodePrecedence > awaitPrecedence; - } - function test(node: TSESTree.Expression, expression: ts.Node): void { let child: ts.Node; diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index 8d074e03ba5e..6bd8189c5a51 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -26,8 +26,9 @@ export * from './scopeUtils'; export * from './types'; export * from './getConstraintInfo'; export * from './getValueOfLiteralType'; -export * from './truthinessAndNullishUtils'; +export * from './isHigherPrecedenceThanAwait'; export * from './skipChainExpression'; +export * from './truthinessAndNullishUtils'; // this is done for convenience - saves migrating all of the old rules export * from '@typescript-eslint/type-utils'; diff --git a/packages/eslint-plugin/src/util/isHigherPrecedenceThanAwait.ts b/packages/eslint-plugin/src/util/isHigherPrecedenceThanAwait.ts new file mode 100644 index 000000000000..20ee6885d269 --- /dev/null +++ b/packages/eslint-plugin/src/util/isHigherPrecedenceThanAwait.ts @@ -0,0 +1,15 @@ +import * as ts from 'typescript'; + +import { getOperatorPrecedence } from './getOperatorPrecedence'; + +export function isHigherPrecedenceThanAwait(tsNode: ts.Node): boolean { + const operator = ts.isBinaryExpression(tsNode) + ? tsNode.operatorToken.kind + : ts.SyntaxKind.Unknown; + const nodePrecedence = getOperatorPrecedence(tsNode.kind, operator); + const awaitPrecedence = getOperatorPrecedence( + ts.SyntaxKind.AwaitExpression, + ts.SyntaxKind.Unknown, + ); + return nodePrecedence > awaitPrecedence; +} diff --git a/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts b/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts index 8701ea580daa..e187a85a1975 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts @@ -741,6 +741,17 @@ ruleTester.run('no-misused-spread', rule, { endLine: 6, line: 3, messageId: 'noMapSpreadInObject', + suggestions: [ + { + messageId: 'replaceMapSpreadInObject', + output: ` + const o = Object.fromEntries(new Map([ + ['test-1', 1], + ['test-2', 2], + ])); + `, + }, + ], }, ], }, @@ -759,6 +770,19 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 27, line: 7, messageId: 'noMapSpreadInObject', + suggestions: [ + { + messageId: 'replaceMapSpreadInObject', + output: ` + const map = new Map([ + ['test-1', 1], + ['test-2', 2], + ]); + + const o = Object.fromEntries(map); + `, + }, + ], }, ], }, @@ -773,6 +797,109 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 27, line: 3, messageId: 'noMapSpreadInObject', + suggestions: [ + { + messageId: 'replaceMapSpreadInObject', + output: ` + declare const map: Map; + const o = Object.fromEntries(map); + `, + }, + ], + }, + ], + }, + { + code: noFormat` + declare const map: Map; + const o = { ...(map) }; + `, + errors: [ + { + column: 21, + endColumn: 29, + line: 3, + messageId: 'noMapSpreadInObject', + suggestions: [ + { + messageId: 'replaceMapSpreadInObject', + output: ` + declare const map: Map; + const o = Object.fromEntries(map); + `, + }, + ], + }, + ], + }, + { + code: ` + declare const map: Map; + const o = { ...(map, map) }; + `, + errors: [ + { + column: 21, + endColumn: 34, + line: 3, + messageId: 'noMapSpreadInObject', + suggestions: [ + { + messageId: 'replaceMapSpreadInObject', + output: ` + declare const map: Map; + const o = Object.fromEntries((map, map)); + `, + }, + ], + }, + ], + }, + { + code: ` + declare const map: Map; + const others = { a: 1 }; + const o = { ...map, ...others }; + `, + errors: [ + { + column: 21, + endColumn: 27, + line: 4, + messageId: 'noMapSpreadInObject', + suggestions: [ + { + messageId: 'replaceMapSpreadInObject', + output: ` + declare const map: Map; + const others = { a: 1 }; + const o = { ...Object.fromEntries(map), ...others }; + `, + }, + ], + }, + ], + }, + { + code: ` + declare const map: Map; + const o = { other: 1, ...map }; + `, + errors: [ + { + column: 31, + endColumn: 37, + line: 3, + messageId: 'noMapSpreadInObject', + suggestions: [ + { + messageId: 'replaceMapSpreadInObject', + output: ` + declare const map: Map; + const o = { other: 1, ...Object.fromEntries(map) }; + `, + }, + ], }, ], }, @@ -787,6 +914,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 27, line: 3, messageId: 'noMapSpreadInObject', + suggestions: [ + { + messageId: 'replaceMapSpreadInObject', + output: ` + declare const map: ReadonlyMap; + const o = Object.fromEntries(map); + `, + }, + ], }, ], }, @@ -801,6 +937,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 27, line: 3, messageId: 'noMapSpreadInObject', + suggestions: [ + { + messageId: 'replaceMapSpreadInObject', + output: ` + declare const map: WeakMap<{ a: number }, string>; + const o = Object.fromEntries(map); + `, + }, + ], }, ], }, @@ -829,6 +974,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 32, line: 3, messageId: 'noMapSpreadInObject', + suggestions: [ + { + messageId: 'replaceMapSpreadInObject', + output: ` + declare function getMap(): Map; + const o = Object.fromEntries(getMap()); + `, + }, + ], }, ], }, @@ -843,6 +997,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 25, line: 3, messageId: 'noMapSpreadInObject', + suggestions: [ + { + messageId: 'replaceMapSpreadInObject', + output: ` + declare const a: Map & Set; + const o = Object.fromEntries(a); + `, + }, + ], }, ], }, @@ -871,6 +1034,69 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 31, line: 3, messageId: 'noPromiseSpreadInObject', + suggestions: [ + { + messageId: 'addAwait', + output: ` + const promise = new Promise(() => {}); + const o = { ...await promise }; + `, + }, + ], + }, + ], + }, + { + code: ` + declare const promise: Promise<{ a: 1 }>; + async function foo() { + return { ...(promise || {}) }; + } + `, + errors: [ + { + column: 20, + endColumn: 38, + line: 4, + messageId: 'noPromiseSpreadInObject', + suggestions: [ + { + messageId: 'addAwait', + output: ` + declare const promise: Promise<{ a: 1 }>; + async function foo() { + return { ...(await (promise || {})) }; + } + `, + }, + ], + }, + ], + }, + { + code: ` + declare const promise: Promise; + async function foo() { + return { ...(Math.random() < 0.5 ? promise : {}) }; + } + `, + errors: [ + { + column: 20, + endColumn: 59, + line: 4, + messageId: 'noPromiseSpreadInObject', + suggestions: [ + { + messageId: 'addAwait', + output: ` + declare const promise: Promise; + async function foo() { + return { ...(await (Math.random() < 0.5 ? promise : {})) }; + } + `, + }, + ], }, ], }, @@ -886,6 +1112,16 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 30, line: 3, messageId: 'noPromiseSpreadInObject', + suggestions: [ + { + messageId: 'addAwait', + output: ` + function withPromise

>(promise: P) { + return { ...await promise }; + } + `, + }, + ], }, ], }, @@ -900,6 +1136,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 36, line: 3, messageId: 'noPromiseSpreadInObject', + suggestions: [ + { + messageId: 'addAwait', + output: ` + declare const maybePromise: Promise | { a: number }; + const o = { ...await maybePromise }; + `, + }, + ], }, ], }, @@ -914,6 +1159,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 31, line: 3, messageId: 'noPromiseSpreadInObject', + suggestions: [ + { + messageId: 'addAwait', + output: ` + declare const promise: Promise & { a: number }; + const o = { ...await promise }; + `, + }, + ], }, ], }, @@ -928,6 +1182,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 36, line: 3, messageId: 'noPromiseSpreadInObject', + suggestions: [ + { + messageId: 'addAwait', + output: ` + declare function getPromise(): Promise; + const o = { ...await getPromise() }; + `, + }, + ], }, ], }, @@ -942,6 +1205,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 36, line: 3, messageId: 'noPromiseSpreadInObject', + suggestions: [ + { + messageId: 'addAwait', + output: ` + declare function getPromise>(arg: T): T; + const o = { ...await getPromise() }; + `, + }, + ], }, ], }, @@ -1636,6 +1908,16 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 32, line: 4, messageId: 'noMapSpreadInObject', + suggestions: [ + { + messageId: 'replaceMapSpreadInObject', + output: ` + declare const map: Map; + + const o =

; + `, + }, + ], }, ], languageOptions: { @@ -1658,6 +1940,16 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 36, line: 4, messageId: 'noPromiseSpreadInObject', + suggestions: [ + { + messageId: 'addAwait', + output: ` + const promise = new Promise(() => {}); + + const o =
; + `, + }, + ], }, ], languageOptions: { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy