From 4d4ff19fd970ab053262946703cc115a71682755 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Mon, 27 Jan 2025 02:19:25 +0900 Subject: [PATCH 1/9] temp --- .../src/rules/no-misused-spread.ts | 68 ++++++- .../tests/rules/no-misused-spread.test.ts | 169 ++++++++++++++++++ 2 files changed, 235 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-spread.ts b/packages/eslint-plugin/src/rules/no-misused-spread.ts index 17991adc6a49..85f34878e9ad 100644 --- a/packages/eslint-plugin/src/rules/no-misused-spread.ts +++ b/packages/eslint-plugin/src/rules/no-misused-spread.ts @@ -1,4 +1,4 @@ -import type { TSESTree } from '@typescript-eslint/utils'; +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -8,7 +8,9 @@ import type { TypeOrValueSpecifier } from '../util'; import { createRule, getConstrainedTypeAtLocation, + getOperatorPrecedence, getParserServices, + getWrappingFixer, isBuiltinSymbolLike, isPromiseLike, isTypeFlagSet, @@ -30,7 +32,10 @@ type MessageIds = | 'noIterableSpreadInObject' | 'noMapSpreadInObject' | 'noPromiseSpreadInObject' - | 'noStringSpread'; + | 'noStringSpread' + | 'replaceMapSpreadInObject' + | 'replacePromiseSpreadInObject' + | 'replaceStringSpread'; export default createRule({ name: 'no-misused-spread', @@ -42,6 +47,7 @@ export default createRule({ recommended: 'strict', requiresTypeChecking: true, }, + hasSuggestions: true, messages: { noArraySpreadInObject: 'Using the spread operator on an array in an object will result in a list of indices.', @@ -59,6 +65,9 @@ export default createRule({ 'Using the spread operator on Promise in an object can cause unexpected behavior. Did you forget to await the promise?', noStringSpread: "Using the spread operator on a string can cause unexpected behavior. Prefer `.split('')` instead.", + replaceMapSpreadInObject: 'replace map spread in object', + replacePromiseSpreadInObject: 'replace promise in spread', + replaceStringSpread: 'replace string spread', }, schema: [ { @@ -99,6 +108,59 @@ export default createRule({ } } + function getMapSpreadSuggestions( + node: TSESTree.Expression, + type: ts.Type, + ): TSESLint.ReportSuggestionArray | null { + const types = tsutils.unionTypeParts(type); + if (types.some(t => !isMap(services.program, t))) { + return null; + } + return [ + { + messageId: 'replaceMapSpreadInObject', + fix: getWrappingFixer({ + node, + sourceCode: context.sourceCode, + wrap: code => `Object.entries(${code})`, + }), + }, + ]; + } + + 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; + } + + function getPromiseSpreadSuggestions( + node: TSESTree.Expression, + ): TSESLint.ReportSuggestionArray { + const isHighPrecendence = isHigherPrecedenceThanAwait( + services.esTreeNodeToTSNodeMap.get(node), + ); + + return [ + { + messageId: 'replacePromiseSpreadInObject', + fix: fixer => + isHighPrecendence + ? fixer.insertTextBefore(node, 'await ') + : [ + fixer.insertTextBefore(node, 'await ('), + fixer.insertTextAfter(node, ')'), + ], + }, + ]; + } + function checkObjectSpread( node: TSESTree.JSXSpreadAttribute | TSESTree.SpreadElement, ): void { @@ -112,6 +174,7 @@ export default createRule({ context.report({ node, messageId: 'noPromiseSpreadInObject', + suggest: getPromiseSpreadSuggestions(node.argument), }); return; @@ -130,6 +193,7 @@ export default createRule({ context.report({ node, messageId: 'noMapSpreadInObject', + suggest: getMapSpreadSuggestions(node.argument, type), }); return; 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..2323eedf6732 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,19 @@ ruleTester.run('no-misused-spread', rule, { endLine: 6, line: 3, messageId: 'noMapSpreadInObject', + suggestions: [ + { + messageId: 'replaceMapSpreadInObject', + output: ` + const o = { + ...Object.entries(new Map([ + ['test-1', 1], + ['test-2', 2], + ])), + }; + `, + }, + ], }, ], }, @@ -759,6 +772,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.entries(map) }; + `, + }, + ], }, ], }, @@ -773,6 +799,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 27, line: 3, messageId: 'noMapSpreadInObject', + suggestions: [ + { + messageId: 'replaceMapSpreadInObject', + output: ` + declare const map: Map; + const o = { ...Object.entries(map) }; + `, + }, + ], }, ], }, @@ -787,6 +822,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 27, line: 3, messageId: 'noMapSpreadInObject', + suggestions: [ + { + messageId: 'replaceMapSpreadInObject', + output: ` + declare const map: ReadonlyMap; + const o = { ...Object.entries(map) }; + `, + }, + ], }, ], }, @@ -801,6 +845,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.entries(map) }; + `, + }, + ], }, ], }, @@ -829,6 +882,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 32, line: 3, messageId: 'noMapSpreadInObject', + suggestions: [ + { + messageId: 'replaceMapSpreadInObject', + output: ` + declare function getMap(): Map; + const o = { ...Object.entries(getMap()) }; + `, + }, + ], }, ], }, @@ -843,6 +905,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.entries(a) }; + `, + }, + ], }, ], }, @@ -871,6 +942,38 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 31, line: 3, messageId: 'noPromiseSpreadInObject', + suggestions: [ + { + messageId: 'replacePromiseSpreadInObject', + output: ` + const promise = new Promise(() => {}); + const o = { ...await promise }; + `, + }, + ], + }, + ], + }, + { + code: ` + const promise = new Promise(() => {}); + const o = { ...(promise || {}) }; + `, + errors: [ + { + column: 21, + endColumn: 39, + line: 3, + messageId: 'noPromiseSpreadInObject', + suggestions: [ + { + messageId: 'replacePromiseSpreadInObject', + output: ` + const promise = new Promise(() => {}); + const o = { ...await (promise || {}) }; + `, + }, + ], }, ], }, @@ -886,6 +989,16 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 30, line: 3, messageId: 'noPromiseSpreadInObject', + suggestions: [ + { + messageId: 'replacePromiseSpreadInObject', + output: ` + function withPromise

>(promise: P) { + return { ...await promise }; + } + `, + }, + ], }, ], }, @@ -900,6 +1013,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 36, line: 3, messageId: 'noPromiseSpreadInObject', + suggestions: [ + { + messageId: 'replacePromiseSpreadInObject', + output: ` + declare const maybePromise: Promise | { a: number }; + const o = { ...await maybePromise }; + `, + }, + ], }, ], }, @@ -914,6 +1036,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 31, line: 3, messageId: 'noPromiseSpreadInObject', + suggestions: [ + { + messageId: 'replacePromiseSpreadInObject', + output: ` + declare const promise: Promise & { a: number }; + const o = { ...await promise }; + `, + }, + ], }, ], }, @@ -928,6 +1059,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 36, line: 3, messageId: 'noPromiseSpreadInObject', + suggestions: [ + { + messageId: 'replacePromiseSpreadInObject', + output: ` + declare function getPromise(): Promise; + const o = { ...await getPromise() }; + `, + }, + ], }, ], }, @@ -942,6 +1082,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 36, line: 3, messageId: 'noPromiseSpreadInObject', + suggestions: [ + { + messageId: 'replacePromiseSpreadInObject', + output: ` + declare function getPromise>(arg: T): T; + const o = { ...await getPromise() }; + `, + }, + ], }, ], }, @@ -1636,6 +1785,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 +1817,16 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 36, line: 4, messageId: 'noPromiseSpreadInObject', + suggestions: [ + { + messageId: 'replacePromiseSpreadInObject', + output: ` + const promise = new Promise(() => {}); + + const o =
; + `, + }, + ], }, ], languageOptions: { From c17644e4e36166764b7eeca99b2f716a8499f574 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Tue, 28 Jan 2025 00:43:36 +0900 Subject: [PATCH 2/9] implement --- .../src/rules/no-misused-spread.ts | 33 ++++- .../tests/rules/no-misused-spread.test.ts | 113 +++++++++++++++--- 2 files changed, 126 insertions(+), 20 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-spread.ts b/packages/eslint-plugin/src/rules/no-misused-spread.ts index 85f34878e9ad..8f7a806ea07b 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 { 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'; @@ -25,6 +26,7 @@ type Options = [ ]; type MessageIds = + | 'addAwait' | 'noArraySpreadInObject' | 'noClassDeclarationSpreadInObject' | 'noClassInstanceSpreadInObject' @@ -34,7 +36,6 @@ type MessageIds = | 'noPromiseSpreadInObject' | 'noStringSpread' | 'replaceMapSpreadInObject' - | 'replacePromiseSpreadInObject' | 'replaceStringSpread'; export default createRule({ @@ -49,6 +50,7 @@ export default createRule({ }, 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: @@ -65,9 +67,9 @@ export default createRule({ 'Using the spread operator on Promise in an object can cause unexpected behavior. Did you forget to await the promise?', noStringSpread: "Using the spread operator on a string can cause unexpected behavior. Prefer `.split('')` instead.", - replaceMapSpreadInObject: 'replace map spread in object', - replacePromiseSpreadInObject: 'replace promise in spread', - replaceStringSpread: 'replace string spread', + replaceMapSpreadInObject: + 'Replace map spread in object with `Object.fromEntries(map)`', + replaceStringSpread: "Replace string spread with `.split('')`", }, schema: [ { @@ -104,6 +106,7 @@ export default createRule({ context.report({ node, messageId: 'noStringSpread', + suggest: getStringSpreadSuggestions(node, type), }); } } @@ -149,7 +152,7 @@ export default createRule({ return [ { - messageId: 'replacePromiseSpreadInObject', + messageId: 'addAwait', fix: fixer => isHighPrecendence ? fixer.insertTextBefore(node, 'await ') @@ -161,6 +164,26 @@ export default createRule({ ]; } + function getStringSpreadSuggestions( + node: TSESTree.SpreadElement, + type: ts.Type, + ): TSESLint.ReportSuggestionArray | null { + if ( + node.parent.type !== AST_NODE_TYPES.ArrayExpression || + node.parent.elements.length > 1 || + tsutils.unionTypeParts(type).some(type => !isString(type)) + ) { + return null; + } + const code = context.sourceCode.getText(node.argument); + return [ + { + messageId: 'replaceStringSpread', + fix: fixer => fixer.replaceText(node.parent, `${code}.split('')`), + }, + ]; + } + function checkObjectSpread( node: TSESTree.JSXSpreadAttribute | TSESTree.SpreadElement, ): void { 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 2323eedf6732..9e18c2c0c441 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts @@ -306,6 +306,12 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 21, line: 1, messageId: 'noStringSpread', + suggestions: [ + { + messageId: 'replaceStringSpread', + output: "const a = 'test'.split('');", + }, + ], }, ], }, @@ -321,6 +327,16 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 26, line: 3, messageId: 'noStringSpread', + suggestions: [ + { + messageId: 'replaceStringSpread', + output: ` + function withText(text: Text) { + return text.split(''); + } + `, + }, + ], }, ], }, @@ -335,6 +351,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 27, line: 3, messageId: 'noStringSpread', + suggestions: [ + { + messageId: 'replaceStringSpread', + output: ` + const test = 'hello'; + const a = test.split(''); + `, + }, + ], }, ], }, @@ -349,6 +374,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 27, line: 3, messageId: 'noStringSpread', + suggestions: [ + { + messageId: 'replaceStringSpread', + output: ` + const test = \`he\${'ll'}o\`; + const a = test.split(''); + `, + }, + ], }, ], }, @@ -363,6 +397,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 27, line: 3, messageId: 'noStringSpread', + suggestions: [ + { + messageId: 'replaceStringSpread', + output: ` + declare const test: string; + const a = test.split(''); + `, + }, + ], }, ], }, @@ -391,6 +434,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 27, line: 3, messageId: 'noStringSpread', + suggestions: [ + { + messageId: 'replaceStringSpread', + output: ` + declare const test: string & { __brand: 'test' }; + const a = test.split(''); + `, + }, + ], }, ], }, @@ -419,6 +471,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 34, line: 3, messageId: 'noStringSpread', + suggestions: [ + { + messageId: 'replaceStringSpread', + output: ` + declare function getString(): string; + const a = getString().split(''); + `, + }, + ], }, ], }, @@ -490,6 +551,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 34, line: 3, messageId: 'noStringSpread', + suggestions: [ + { + messageId: 'replaceStringSpread', + output: ` + declare function getString(): T; + const a = getString().split(''); + `, + }, + ], }, ], }, @@ -504,6 +574,15 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 34, line: 3, messageId: 'noStringSpread', + suggestions: [ + { + messageId: 'replaceStringSpread', + output: ` + declare function getString(): string & { __brand: 'test' }; + const a = getString().split(''); + `, + }, + ], }, ], }, @@ -944,7 +1023,7 @@ ruleTester.run('no-misused-spread', rule, { messageId: 'noPromiseSpreadInObject', suggestions: [ { - messageId: 'replacePromiseSpreadInObject', + messageId: 'addAwait', output: ` const promise = new Promise(() => {}); const o = { ...await promise }; @@ -956,21 +1035,25 @@ ruleTester.run('no-misused-spread', rule, { }, { code: ` - const promise = new Promise(() => {}); - const o = { ...(promise || {}) }; + declare const promise: Promise<{ a: 1 }>; + async function foo() { + return { ...(promise || {}) }; + } `, errors: [ { - column: 21, - endColumn: 39, - line: 3, + column: 20, + endColumn: 38, + line: 4, messageId: 'noPromiseSpreadInObject', suggestions: [ { - messageId: 'replacePromiseSpreadInObject', + messageId: 'addAwait', output: ` - const promise = new Promise(() => {}); - const o = { ...await (promise || {}) }; + declare const promise: Promise<{ a: 1 }>; + async function foo() { + return { ...(await (promise || {})) }; + } `, }, ], @@ -991,7 +1074,7 @@ ruleTester.run('no-misused-spread', rule, { messageId: 'noPromiseSpreadInObject', suggestions: [ { - messageId: 'replacePromiseSpreadInObject', + messageId: 'addAwait', output: ` function withPromise

>(promise: P) { return { ...await promise }; @@ -1015,7 +1098,7 @@ ruleTester.run('no-misused-spread', rule, { messageId: 'noPromiseSpreadInObject', suggestions: [ { - messageId: 'replacePromiseSpreadInObject', + messageId: 'addAwait', output: ` declare const maybePromise: Promise | { a: number }; const o = { ...await maybePromise }; @@ -1038,7 +1121,7 @@ ruleTester.run('no-misused-spread', rule, { messageId: 'noPromiseSpreadInObject', suggestions: [ { - messageId: 'replacePromiseSpreadInObject', + messageId: 'addAwait', output: ` declare const promise: Promise & { a: number }; const o = { ...await promise }; @@ -1061,7 +1144,7 @@ ruleTester.run('no-misused-spread', rule, { messageId: 'noPromiseSpreadInObject', suggestions: [ { - messageId: 'replacePromiseSpreadInObject', + messageId: 'addAwait', output: ` declare function getPromise(): Promise; const o = { ...await getPromise() }; @@ -1084,7 +1167,7 @@ ruleTester.run('no-misused-spread', rule, { messageId: 'noPromiseSpreadInObject', suggestions: [ { - messageId: 'replacePromiseSpreadInObject', + messageId: 'addAwait', output: ` declare function getPromise>(arg: T): T; const o = { ...await getPromise() }; @@ -1819,7 +1902,7 @@ ruleTester.run('no-misused-spread', rule, { messageId: 'noPromiseSpreadInObject', suggestions: [ { - messageId: 'replacePromiseSpreadInObject', + messageId: 'addAwait', output: ` const promise = new Promise(() => {}); From e7098ddf33c73201a3e2695ee62ab4233662be6c Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Tue, 28 Jan 2025 18:27:10 +0900 Subject: [PATCH 3/9] implement --- .../src/rules/no-misused-spread.ts | 15 +++--- .../tests/rules/no-misused-spread.test.ts | 48 +++++++++++++++++++ 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-spread.ts b/packages/eslint-plugin/src/rules/no-misused-spread.ts index 8f7a806ea07b..059f5e26a840 100644 --- a/packages/eslint-plugin/src/rules/no-misused-spread.ts +++ b/packages/eslint-plugin/src/rules/no-misused-spread.ts @@ -168,18 +168,21 @@ export default createRule({ node: TSESTree.SpreadElement, type: ts.Type, ): TSESLint.ReportSuggestionArray | null { - if ( - node.parent.type !== AST_NODE_TYPES.ArrayExpression || - node.parent.elements.length > 1 || - tsutils.unionTypeParts(type).some(type => !isString(type)) - ) { + if (tsutils.unionTypeParts(type).some(type => !isString(type))) { return null; } + + const targetNode: TSESTree.Node = + node.parent.type !== AST_NODE_TYPES.ArrayExpression || + node.parent.elements.length > 1 + ? node.argument + : node.parent; + const code = context.sourceCode.getText(node.argument); return [ { messageId: 'replaceStringSpread', - fix: fixer => fixer.replaceText(node.parent, `${code}.split('')`), + fix: fixer => fixer.replaceText(targetNode, `${code}.split('')`), }, ]; } 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 9e18c2c0c441..c695b446ce0a 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts @@ -497,6 +497,18 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 29, line: 6, messageId: 'noStringSpread', + suggestions: [ + { + messageId: 'replaceStringSpread', + output: ` + declare function textIdentity(...args: string[]); + + declare const text: string; + + textIdentity(...text.split('')); + `, + }, + ], }, ], }, @@ -514,12 +526,36 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 29, line: 6, messageId: 'noStringSpread', + suggestions: [ + { + messageId: 'replaceStringSpread', + output: ` + declare function textIdentity(...args: string[]); + + declare const text: string; + + textIdentity(...text.split(''), 'and', ...text); + `, + }, + ], }, { column: 38, endColumn: 45, line: 6, messageId: 'noStringSpread', + suggestions: [ + { + messageId: 'replaceStringSpread', + output: ` + declare function textIdentity(...args: string[]); + + declare const text: string; + + textIdentity(...text, 'and', ...text.split('')); + `, + }, + ], }, ], }, @@ -537,6 +573,18 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 31, line: 5, messageId: 'noStringSpread', + suggestions: [ + { + messageId: 'replaceStringSpread', + output: ` + declare function textIdentity(...args: string[]); + + function withText(text: Text) { + textIdentity(...text.split('')); + } + `, + }, + ], }, ], }, From 128a7c2f143130ad99412f43c632ac5e8ef4f0ee Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Tue, 28 Jan 2025 22:29:37 +0900 Subject: [PATCH 4/9] fix --- .../src/rules/no-misused-spread.ts | 2 +- .../tests/rules/no-misused-spread.test.ts | 43 +++++++++++++++---- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-spread.ts b/packages/eslint-plugin/src/rules/no-misused-spread.ts index 059f5e26a840..6457a1d4d007 100644 --- a/packages/eslint-plugin/src/rules/no-misused-spread.ts +++ b/packages/eslint-plugin/src/rules/no-misused-spread.ts @@ -125,7 +125,7 @@ export default createRule({ fix: getWrappingFixer({ node, sourceCode: context.sourceCode, - wrap: code => `Object.entries(${code})`, + wrap: code => `Object.fromEntries(${code})`, }), }, ]; 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 c695b446ce0a..7b3ef2b21895 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts @@ -873,7 +873,7 @@ ruleTester.run('no-misused-spread', rule, { messageId: 'replaceMapSpreadInObject', output: ` const o = { - ...Object.entries(new Map([ + ...Object.fromEntries(new Map([ ['test-1', 1], ['test-2', 2], ])), @@ -908,7 +908,7 @@ ruleTester.run('no-misused-spread', rule, { ['test-2', 2], ]); - const o = { ...Object.entries(map) }; + const o = { ...Object.fromEntries(map) }; `, }, ], @@ -931,7 +931,7 @@ ruleTester.run('no-misused-spread', rule, { messageId: 'replaceMapSpreadInObject', output: ` declare const map: Map; - const o = { ...Object.entries(map) }; + const o = { ...Object.fromEntries(map) }; `, }, ], @@ -954,7 +954,7 @@ ruleTester.run('no-misused-spread', rule, { messageId: 'replaceMapSpreadInObject', output: ` declare const map: ReadonlyMap; - const o = { ...Object.entries(map) }; + const o = { ...Object.fromEntries(map) }; `, }, ], @@ -977,7 +977,7 @@ ruleTester.run('no-misused-spread', rule, { messageId: 'replaceMapSpreadInObject', output: ` declare const map: WeakMap<{ a: number }, string>; - const o = { ...Object.entries(map) }; + const o = { ...Object.fromEntries(map) }; `, }, ], @@ -1014,7 +1014,7 @@ ruleTester.run('no-misused-spread', rule, { messageId: 'replaceMapSpreadInObject', output: ` declare function getMap(): Map; - const o = { ...Object.entries(getMap()) }; + const o = { ...Object.fromEntries(getMap()) }; `, }, ], @@ -1037,7 +1037,7 @@ ruleTester.run('no-misused-spread', rule, { messageId: 'replaceMapSpreadInObject', output: ` declare const a: Map & Set; - const o = { ...Object.entries(a) }; + const o = { ...Object.fromEntries(a) }; `, }, ], @@ -1108,6 +1108,33 @@ ruleTester.run('no-misused-spread', rule, { }, ], }, + { + 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 : {})) }; + } + `, + }, + ], + }, + ], + }, { code: ` function withPromise

>(promise: P) { @@ -1922,7 +1949,7 @@ ruleTester.run('no-misused-spread', rule, { output: ` declare const map: Map; - const o =

; + const o =
; `, }, ], From 111e56897dde3190f5f6da46df572e25b6476a30 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Fri, 14 Feb 2025 23:35:00 +0900 Subject: [PATCH 5/9] merge --- .../src/rules/no-misused-spread.ts | 29 +--- .../tests/rules/no-misused-spread.test.ts | 127 ------------------ 2 files changed, 1 insertion(+), 155 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-spread.ts b/packages/eslint-plugin/src/rules/no-misused-spread.ts index 378efaae7d86..ed75c0261115 100644 --- a/packages/eslint-plugin/src/rules/no-misused-spread.ts +++ b/packages/eslint-plugin/src/rules/no-misused-spread.ts @@ -1,6 +1,5 @@ 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'; @@ -35,8 +34,7 @@ type MessageIds = | 'noMapSpreadInObject' | 'noPromiseSpreadInObject' | 'noStringSpread' - | 'replaceMapSpreadInObject' - | 'replaceStringSpread'; + | 'replaceMapSpreadInObject'; export default createRule({ name: 'no-misused-spread', @@ -74,7 +72,6 @@ export default createRule({ ].join('\n'), replaceMapSpreadInObject: 'Replace map spread in object with `Object.fromEntries(map)`', - replaceStringSpread: "Replace string spread with `.split('')`", }, schema: [ { @@ -111,7 +108,6 @@ export default createRule({ context.report({ node, messageId: 'noStringSpread', - suggest: getStringSpreadSuggestions(node, type), }); } } @@ -169,29 +165,6 @@ export default createRule({ ]; } - function getStringSpreadSuggestions( - node: TSESTree.SpreadElement, - type: ts.Type, - ): TSESLint.ReportSuggestionArray | null { - if (tsutils.unionTypeParts(type).some(type => !isString(type))) { - return null; - } - - const targetNode: TSESTree.Node = - node.parent.type !== AST_NODE_TYPES.ArrayExpression || - node.parent.elements.length > 1 - ? node.argument - : node.parent; - - const code = context.sourceCode.getText(node.argument); - return [ - { - messageId: 'replaceStringSpread', - fix: fixer => fixer.replaceText(targetNode, `${code}.split('')`), - }, - ]; - } - function checkObjectSpread( node: TSESTree.JSXSpreadAttribute | TSESTree.SpreadElement, ): void { 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 7b3ef2b21895..57385b82f34b 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts @@ -306,12 +306,6 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 21, line: 1, messageId: 'noStringSpread', - suggestions: [ - { - messageId: 'replaceStringSpread', - output: "const a = 'test'.split('');", - }, - ], }, ], }, @@ -327,16 +321,6 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 26, line: 3, messageId: 'noStringSpread', - suggestions: [ - { - messageId: 'replaceStringSpread', - output: ` - function withText(text: Text) { - return text.split(''); - } - `, - }, - ], }, ], }, @@ -351,15 +335,6 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 27, line: 3, messageId: 'noStringSpread', - suggestions: [ - { - messageId: 'replaceStringSpread', - output: ` - const test = 'hello'; - const a = test.split(''); - `, - }, - ], }, ], }, @@ -374,15 +349,6 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 27, line: 3, messageId: 'noStringSpread', - suggestions: [ - { - messageId: 'replaceStringSpread', - output: ` - const test = \`he\${'ll'}o\`; - const a = test.split(''); - `, - }, - ], }, ], }, @@ -397,15 +363,6 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 27, line: 3, messageId: 'noStringSpread', - suggestions: [ - { - messageId: 'replaceStringSpread', - output: ` - declare const test: string; - const a = test.split(''); - `, - }, - ], }, ], }, @@ -434,15 +391,6 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 27, line: 3, messageId: 'noStringSpread', - suggestions: [ - { - messageId: 'replaceStringSpread', - output: ` - declare const test: string & { __brand: 'test' }; - const a = test.split(''); - `, - }, - ], }, ], }, @@ -471,15 +419,6 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 34, line: 3, messageId: 'noStringSpread', - suggestions: [ - { - messageId: 'replaceStringSpread', - output: ` - declare function getString(): string; - const a = getString().split(''); - `, - }, - ], }, ], }, @@ -497,18 +436,6 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 29, line: 6, messageId: 'noStringSpread', - suggestions: [ - { - messageId: 'replaceStringSpread', - output: ` - declare function textIdentity(...args: string[]); - - declare const text: string; - - textIdentity(...text.split('')); - `, - }, - ], }, ], }, @@ -526,36 +453,12 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 29, line: 6, messageId: 'noStringSpread', - suggestions: [ - { - messageId: 'replaceStringSpread', - output: ` - declare function textIdentity(...args: string[]); - - declare const text: string; - - textIdentity(...text.split(''), 'and', ...text); - `, - }, - ], }, { column: 38, endColumn: 45, line: 6, messageId: 'noStringSpread', - suggestions: [ - { - messageId: 'replaceStringSpread', - output: ` - declare function textIdentity(...args: string[]); - - declare const text: string; - - textIdentity(...text, 'and', ...text.split('')); - `, - }, - ], }, ], }, @@ -573,18 +476,6 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 31, line: 5, messageId: 'noStringSpread', - suggestions: [ - { - messageId: 'replaceStringSpread', - output: ` - declare function textIdentity(...args: string[]); - - function withText(text: Text) { - textIdentity(...text.split('')); - } - `, - }, - ], }, ], }, @@ -599,15 +490,6 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 34, line: 3, messageId: 'noStringSpread', - suggestions: [ - { - messageId: 'replaceStringSpread', - output: ` - declare function getString(): T; - const a = getString().split(''); - `, - }, - ], }, ], }, @@ -622,15 +504,6 @@ ruleTester.run('no-misused-spread', rule, { endColumn: 34, line: 3, messageId: 'noStringSpread', - suggestions: [ - { - messageId: 'replaceStringSpread', - output: ` - declare function getString(): string & { __brand: 'test' }; - const a = getString().split(''); - `, - }, - ], }, ], }, From affa14b011ca72b90daacb1f26724a13630a14ef Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Sun, 16 Feb 2025 22:05:53 +0900 Subject: [PATCH 6/9] fix --- .../src/rules/no-misused-spread.ts | 30 ++++++++++++++++--- .../tests/rules/no-misused-spread.test.ts | 18 +++++------ 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-spread.ts b/packages/eslint-plugin/src/rules/no-misused-spread.ts index ed75c0261115..56bb3328532f 100644 --- a/packages/eslint-plugin/src/rules/no-misused-spread.ts +++ b/packages/eslint-plugin/src/rules/no-misused-spread.ts @@ -1,4 +1,8 @@ -import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import { + AST_NODE_TYPES, + type TSESLint, + type TSESTree, +} from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -113,18 +117,36 @@ export default createRule({ } function getMapSpreadSuggestions( - node: TSESTree.Expression, + 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: node.argument, sourceCode: context.sourceCode, wrap: code => `Object.fromEntries(${code})`, }), @@ -197,7 +219,7 @@ export default createRule({ context.report({ node, messageId: 'noMapSpreadInObject', - suggest: getMapSpreadSuggestions(node.argument, type), + suggest: getMapSpreadSuggestions(node, type), }); return; 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 57385b82f34b..68e2d7732261 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts @@ -745,12 +745,10 @@ ruleTester.run('no-misused-spread', rule, { { messageId: 'replaceMapSpreadInObject', output: ` - const o = { - ...Object.fromEntries(new Map([ + const o = Object.fromEntries(new Map([ ['test-1', 1], ['test-2', 2], - ])), - }; + ])); `, }, ], @@ -781,7 +779,7 @@ ruleTester.run('no-misused-spread', rule, { ['test-2', 2], ]); - const o = { ...Object.fromEntries(map) }; + const o = Object.fromEntries(map); `, }, ], @@ -804,7 +802,7 @@ ruleTester.run('no-misused-spread', rule, { messageId: 'replaceMapSpreadInObject', output: ` declare const map: Map; - const o = { ...Object.fromEntries(map) }; + const o = Object.fromEntries(map); `, }, ], @@ -827,7 +825,7 @@ ruleTester.run('no-misused-spread', rule, { messageId: 'replaceMapSpreadInObject', output: ` declare const map: ReadonlyMap; - const o = { ...Object.fromEntries(map) }; + const o = Object.fromEntries(map); `, }, ], @@ -850,7 +848,7 @@ ruleTester.run('no-misused-spread', rule, { messageId: 'replaceMapSpreadInObject', output: ` declare const map: WeakMap<{ a: number }, string>; - const o = { ...Object.fromEntries(map) }; + const o = Object.fromEntries(map); `, }, ], @@ -887,7 +885,7 @@ ruleTester.run('no-misused-spread', rule, { messageId: 'replaceMapSpreadInObject', output: ` declare function getMap(): Map; - const o = { ...Object.fromEntries(getMap()) }; + const o = Object.fromEntries(getMap()); `, }, ], @@ -910,7 +908,7 @@ ruleTester.run('no-misused-spread', rule, { messageId: 'replaceMapSpreadInObject', output: ` declare const a: Map & Set; - const o = { ...Object.fromEntries(a) }; + const o = Object.fromEntries(a); `, }, ], From 7977d08f8dae7337783ba132ce405775101b0e37 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Sun, 16 Feb 2025 22:14:46 +0900 Subject: [PATCH 7/9] add tests --- .../src/rules/no-misused-spread.ts | 7 +-- .../tests/rules/no-misused-spread.test.ts | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-misused-spread.ts b/packages/eslint-plugin/src/rules/no-misused-spread.ts index 56bb3328532f..cc28f91a10e7 100644 --- a/packages/eslint-plugin/src/rules/no-misused-spread.ts +++ b/packages/eslint-plugin/src/rules/no-misused-spread.ts @@ -1,9 +1,6 @@ -import { - AST_NODE_TYPES, - type TSESLint, - 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'; 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 68e2d7732261..3e64c2c58ba2 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts @@ -809,6 +809,54 @@ ruleTester.run('no-misused-spread', rule, { }, ], }, + { + code: ` + declare const map: Map; + const others = { a: 1 }; + const o = { ...map, ...a }; + `, + 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), ...a }; + `, + }, + ], + }, + ], + }, + { + code: ` + declare const map: Map; + const o = { a: 1, ...map }; + `, + errors: [ + { + column: 27, + endColumn: 33, + line: 3, + messageId: 'noMapSpreadInObject', + suggestions: [ + { + messageId: 'replaceMapSpreadInObject', + output: ` + declare const map: Map; + const o = { a: 1, ...Object.fromEntries(map) }; + `, + }, + ], + }, + ], + }, { code: ` declare const map: ReadonlyMap; From 75ce541358bebcbbcc528a5ed0c9b487f7e6f48f Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Sun, 16 Feb 2025 22:21:30 +0900 Subject: [PATCH 8/9] Update no-misused-spread.test.ts --- .../tests/rules/no-misused-spread.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 3e64c2c58ba2..da18207824be 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts @@ -813,7 +813,7 @@ ruleTester.run('no-misused-spread', rule, { code: ` declare const map: Map; const others = { a: 1 }; - const o = { ...map, ...a }; + const o = { ...map, ...others }; `, errors: [ { @@ -827,7 +827,7 @@ ruleTester.run('no-misused-spread', rule, { output: ` declare const map: Map; const others = { a: 1 }; - const o = { ...Object.fromEntries(map), ...a }; + const o = { ...Object.fromEntries(map), ...others }; `, }, ], @@ -837,12 +837,12 @@ ruleTester.run('no-misused-spread', rule, { { code: ` declare const map: Map; - const o = { a: 1, ...map }; + const o = { other: 1, ...map }; `, errors: [ { - column: 27, - endColumn: 33, + column: 31, + endColumn: 37, line: 3, messageId: 'noMapSpreadInObject', suggestions: [ @@ -850,7 +850,7 @@ ruleTester.run('no-misused-spread', rule, { messageId: 'replaceMapSpreadInObject', output: ` declare const map: Map; - const o = { a: 1, ...Object.fromEntries(map) }; + const o = { other: 1, ...Object.fromEntries(map) }; `, }, ], From 3832ddaf0d693399807a16ba5c696921b4d307ad Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Wed, 19 Feb 2025 21:15:53 +0900 Subject: [PATCH 9/9] appy reviews --- .../src/rules/no-misused-spread.ts | 16 +------ .../eslint-plugin/src/rules/return-await.ts | 14 +----- packages/eslint-plugin/src/util/index.ts | 1 + .../src/util/isHigherPrecedenceThanAwait.ts | 15 ++++++ .../tests/rules/no-misused-spread.test.ts | 46 +++++++++++++++++++ 5 files changed, 65 insertions(+), 27 deletions(-) create mode 100644 packages/eslint-plugin/src/util/isHigherPrecedenceThanAwait.ts diff --git a/packages/eslint-plugin/src/rules/no-misused-spread.ts b/packages/eslint-plugin/src/rules/no-misused-spread.ts index cc28f91a10e7..74b2ddb6abf5 100644 --- a/packages/eslint-plugin/src/rules/no-misused-spread.ts +++ b/packages/eslint-plugin/src/rules/no-misused-spread.ts @@ -9,7 +9,6 @@ import type { TypeOrValueSpecifier } from '../util'; import { createRule, getConstrainedTypeAtLocation, - getOperatorPrecedence, getParserServices, getWrappingFixer, isBuiltinSymbolLike, @@ -17,6 +16,7 @@ import { isTypeFlagSet, readonlynessOptionsSchema, typeMatchesSomeSpecifier, + isHigherPrecedenceThanAwait, } from '../util'; type Options = [ @@ -72,7 +72,7 @@ export default createRule({ "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(map)`', + 'Replace map spread in object with `Object.fromEntries()`', }, schema: [ { @@ -151,18 +151,6 @@ export default createRule({ ]; } - 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; - } - function getPromiseSpreadSuggestions( node: TSESTree.Expression, ): TSESLint.ReportSuggestionArray { 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 c8a0927b162b..ae9e99e4b2bb 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -26,6 +26,7 @@ export * from './types'; export * from './getConstraintInfo'; export * from './getValueOfLiteralType'; export * from './truthinessAndNullishUtils'; +export * from './isHigherPrecedenceThanAwait'; // 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 da18207824be..e187a85a1975 100644 --- a/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts +++ b/packages/eslint-plugin/tests/rules/no-misused-spread.test.ts @@ -809,6 +809,52 @@ ruleTester.run('no-misused-spread', rule, { }, ], }, + { + 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;