From 8b43da5a169ca93dceef533fa93793b8de61651f Mon Sep 17 00:00:00 2001 From: dora <31735614+dora1998@users.noreply.github.com> Date: Tue, 11 Apr 2023 18:05:40 +0900 Subject: [PATCH 01/12] fix(eslint-plugin): [consistent-type-assertions] wrap object return value with parentheses --- .../src/rules/consistent-type-assertions.ts | 23 ++++++-- .../rules/consistent-type-assertions.test.ts | 58 ++++++++++++++++++- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts index 58403fe8a141..3c0938c5f93d 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts @@ -132,16 +132,27 @@ export default util.createRule({ : {}, fix: messageId === 'as' - ? (fixer): TSESLint.RuleFix[] => [ - fixer.replaceText( + ? function* (fixer): Generator { + yield fixer.replaceText( node, getTextWithParentheses(node.expression), - ), - fixer.insertTextAfter( + ); + + if ( + node.expression.type === AST_NODE_TYPES.ObjectExpression && + node.parent?.type === + AST_NODE_TYPES.ArrowFunctionExpression && + !util.isParenthesized(node.expression, sourceCode) + ) { + yield fixer.insertTextBefore(node, '('); + yield fixer.insertTextAfter(node, ')'); + } + + yield fixer.insertTextAfter( node, ` as ${getTextWithParentheses(node.typeAnnotation)}`, - ), - ] + ); + } : undefined, }); } diff --git a/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts b/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts index 45f672af9e4d..e3f8ba5e2189 100644 --- a/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts @@ -14,7 +14,10 @@ const x = !'string'; const x = a + b; const x = <(A)>a + (b); const x = (new Generic()); -const x = (new (Generic)());`; +const x = (new (Generic)()); +const x = () => { bar: 5 }; +const x = () => ({ bar: 5 }); +const x = () => bar;`; const ANGLE_BRACKET_TESTS = `${ANGLE_BRACKET_TESTS_EXCEPT_CONST_CASE} const x = { key: 'value' }; @@ -29,7 +32,10 @@ const x = !'string' as A; const x = a as A + b; const x = a as (A) + (b); const x = (new Generic()) as Foo; -const x = (new (Generic as Foo)());`; +const x = (new (Generic as Foo)()); +const x = () => ({ bar: 5 }) as Foo; +const x = () => ({ bar: 5 }) as Foo; +const x = () => bar as Foo;`; const AS_TESTS = `${AS_TESTS_EXCEPT_CONST_CASE} const x = { key: 'value' } as const; @@ -198,6 +204,18 @@ ruleTester.run('consistent-type-assertions', rule, { messageId: 'angle-bracket', line: 11, }, + { + messageId: 'angle-bracket', + line: 12, + }, + { + messageId: 'angle-bracket', + line: 13, + }, + { + messageId: 'angle-bracket', + line: 14, + }, ], }), ...batchedSingleLineTests({ @@ -248,6 +266,18 @@ ruleTester.run('consistent-type-assertions', rule, { messageId: 'as', line: 11, }, + { + messageId: 'as', + line: 12, + }, + { + messageId: 'as', + line: 13, + }, + { + messageId: 'as', + line: 14, + }, ], output: AS_TESTS, }), @@ -295,6 +325,18 @@ ruleTester.run('consistent-type-assertions', rule, { messageId: 'never', line: 10, }, + { + messageId: 'never', + line: 11, + }, + { + messageId: 'never', + line: 12, + }, + { + messageId: 'never', + line: 13, + }, ], }), ...batchedSingleLineTests({ @@ -341,6 +383,18 @@ ruleTester.run('consistent-type-assertions', rule, { messageId: 'never', line: 10, }, + { + messageId: 'never', + line: 11, + }, + { + messageId: 'never', + line: 12, + }, + { + messageId: 'never', + line: 13, + }, ], }), ...batchedSingleLineTests({ From c72d3c2c50176687ed44142251cbd7a51c82fb8f Mon Sep 17 00:00:00 2001 From: dora <31735614+dora1998@users.noreply.github.com> Date: Fri, 14 Apr 2023 02:44:23 +0900 Subject: [PATCH 02/12] use getWrappingFixer --- .../src/rules/consistent-type-assertions.ts | 28 +++++-------------- .../src/util/getWrappingFixer.ts | 12 +++++++- .../rules/consistent-type-assertions.test.ts | 16 +++++------ .../rules/strict-boolean-expressions.test.ts | 18 ++++++------ .../tests/util/getWrappingFixer.test.ts | 10 +++---- 5 files changed, 40 insertions(+), 44 deletions(-) diff --git a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts index 3c0938c5f93d..4fada0d310ec 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts @@ -132,27 +132,13 @@ export default util.createRule({ : {}, fix: messageId === 'as' - ? function* (fixer): Generator { - yield fixer.replaceText( - node, - getTextWithParentheses(node.expression), - ); - - if ( - node.expression.type === AST_NODE_TYPES.ObjectExpression && - node.parent?.type === - AST_NODE_TYPES.ArrowFunctionExpression && - !util.isParenthesized(node.expression, sourceCode) - ) { - yield fixer.insertTextBefore(node, '('); - yield fixer.insertTextAfter(node, ')'); - } - - yield fixer.insertTextAfter( - node, - ` as ${getTextWithParentheses(node.typeAnnotation)}`, - ); - } + ? util.getWrappingFixer({ + sourceCode, + node, + innerNode: [node.expression, node.typeAnnotation], + wrap: (expressionCode, typeAnnotationCode) => + `${expressionCode} as ${typeAnnotationCode}`, + }) : undefined, }); } diff --git a/packages/eslint-plugin/src/util/getWrappingFixer.ts b/packages/eslint-plugin/src/util/getWrappingFixer.ts index 73203cf8cdfc..8a325894a56b 100644 --- a/packages/eslint-plugin/src/util/getWrappingFixer.ts +++ b/packages/eslint-plugin/src/util/getWrappingFixer.ts @@ -73,12 +73,15 @@ export function isStrongPrecedenceNode(innerNode: TSESTree.Node): boolean { return ( innerNode.type === AST_NODE_TYPES.Literal || innerNode.type === AST_NODE_TYPES.Identifier || + innerNode.type === AST_NODE_TYPES.TSTypeReference || + innerNode.type === AST_NODE_TYPES.TSTypeOperator || innerNode.type === AST_NODE_TYPES.ArrayExpression || innerNode.type === AST_NODE_TYPES.ObjectExpression || innerNode.type === AST_NODE_TYPES.MemberExpression || innerNode.type === AST_NODE_TYPES.CallExpression || innerNode.type === AST_NODE_TYPES.NewExpression || - innerNode.type === AST_NODE_TYPES.TaggedTemplateExpression + innerNode.type === AST_NODE_TYPES.TaggedTemplateExpression || + innerNode.type === AST_NODE_TYPES.TSInstantiationExpression ); } @@ -121,6 +124,13 @@ function isWeakPrecedenceParent(node: TSESTree.Node): boolean { return true; } + if ( + parent.type === AST_NODE_TYPES.ArrowFunctionExpression && + parent.body === node + ) { + return true; + } + return false; } diff --git a/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts b/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts index e3f8ba5e2189..5db9beb9065b 100644 --- a/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts @@ -27,15 +27,15 @@ const AS_TESTS_EXCEPT_CONST_CASE = ` const x = new Generic() as Foo; const x = b as A; const x = [1] as readonly number[]; -const x = ('string') as a | b; -const x = !'string' as A; -const x = a as A + b; -const x = a as (A) + (b); -const x = (new Generic()) as Foo; +const x = 'string' as (a | b); +const x = (!'string') as A; +const x = (a as A) + b; +const x = (a as A) + (b); +const x = new Generic() as Foo; const x = (new (Generic as Foo)()); -const x = () => ({ bar: 5 }) as Foo; -const x = () => ({ bar: 5 }) as Foo; -const x = () => bar as Foo;`; +const x = () => ({ bar: 5 } as Foo); +const x = () => ({ bar: 5 } as Foo); +const x = () => (bar as Foo);`; const AS_TESTS = `${AS_TESTS_EXCEPT_CONST_CASE} const x = { key: 'value' } as const; diff --git a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts index 3994fc77313a..2d8073000633 100644 --- a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts +++ b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts @@ -750,16 +750,16 @@ if (y) { { messageId: 'conditionFixCompareZero', // TODO: fix compare zero suggestion for bigint - output: ` (x: bigint) => x === 0;`, + output: ` (x: bigint) => (x === 0);`, }, { // TODO: remove check NaN suggestion for bigint messageId: 'conditionFixCompareNaN', - output: ` (x: bigint) => Number.isNaN(x);`, + output: ` (x: bigint) => (Number.isNaN(x));`, }, { messageId: 'conditionFixCastBoolean', - output: ` (x: bigint) => !Boolean(x);`, + output: ` (x: bigint) => (!Boolean(x));`, }, ], }, @@ -893,7 +893,7 @@ if (y) { }, { messageId: 'conditionFixCompareFalse', - output: ` (x?: boolean) => x === false;`, + output: ` (x?: boolean) => (x === false);`, }, ], }, @@ -930,7 +930,7 @@ if (y) { ], output: ` declare const x: object | null; if (x != null) {} - (x?: { a: number }) => x == null; + (x?: { a: number }) => (x == null); (x: T) => (x != null) ? 1 : 0; `, }), @@ -970,7 +970,7 @@ if (y) { suggestions: [ { messageId: 'conditionFixCompareNullish', - output: ' (x?: string) => x == null;', + output: ' (x?: string) => (x == null);', }, { messageId: 'conditionFixDefaultEmptyString', @@ -978,7 +978,7 @@ if (y) { }, { messageId: 'conditionFixCastBoolean', - output: ' (x?: string) => !Boolean(x);', + output: ' (x?: string) => (!Boolean(x));', }, ], }, @@ -1064,7 +1064,7 @@ if (y) { suggestions: [ { messageId: 'conditionFixCompareNullish', - output: ' (x?: number) => x == null;', + output: ' (x?: number) => (x == null);', }, { messageId: 'conditionFixDefaultZero', @@ -1072,7 +1072,7 @@ if (y) { }, { messageId: 'conditionFixCastBoolean', - output: ' (x?: number) => !Boolean(x);', + output: ' (x?: number) => (!Boolean(x));', }, ], }, diff --git a/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts b/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts index 2c7b7977ffbf..591476f75ea9 100644 --- a/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts +++ b/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts @@ -132,6 +132,11 @@ ruleTester.run('getWrappingFixer', rule, { errors: [{ messageId: 'addVoid' }], output: '(void wrapMe).prop', }, + { + code: '() => wrapMe', + errors: [{ messageId: 'addVoid' }], + output: '() => (void wrapMe)', + }, // shouldn't add outer parens when not necessary { @@ -183,11 +188,6 @@ ruleTester.run('getWrappingFixer', rule, { errors: [{ messageId: 'addVoid' }], output: 'function* fn() { yield void wrapMe }', }, - { - code: '() => wrapMe', - errors: [{ messageId: 'addVoid' }], - output: '() => void wrapMe', - }, { code: 'if (wrapMe) {}', errors: [{ messageId: 'addVoid' }], From 2662e6e11b575a027c9d8aef952fd3a7dfad7d78 Mon Sep 17 00:00:00 2001 From: dora <31735614+dora1998@users.noreply.github.com> Date: Fri, 14 Apr 2023 03:32:20 +0900 Subject: [PATCH 03/12] fix --- .../src/util/getWrappingFixer.ts | 32 ++++++-- .../rules/consistent-type-assertions.test.ts | 6 +- .../rules/strict-boolean-expressions.test.ts | 18 ++--- .../tests/util/getWrappingFixer.test.ts | 78 +++++++++++++++---- 4 files changed, 99 insertions(+), 35 deletions(-) diff --git a/packages/eslint-plugin/src/util/getWrappingFixer.ts b/packages/eslint-plugin/src/util/getWrappingFixer.ts index 8a325894a56b..f58c7c55926d 100644 --- a/packages/eslint-plugin/src/util/getWrappingFixer.ts +++ b/packages/eslint-plugin/src/util/getWrappingFixer.ts @@ -42,6 +42,13 @@ export function getWrappingFixer( code = `(${code})`; } + // check the inner node is used as return body of arrow function + if (isObjectExpressionInOneLineReturn(node, innerNode)) { + // the code we are editing might break arrow function expressions + // let's wrap our node in parens in case it's gotten mistaken as block statement + code = `(${code})`; + } + return code; }); @@ -124,13 +131,6 @@ function isWeakPrecedenceParent(node: TSESTree.Node): boolean { return true; } - if ( - parent.type === AST_NODE_TYPES.ArrowFunctionExpression && - parent.body === node - ) { - return true; - } - return false; } @@ -215,3 +215,21 @@ function isLeftHandSide(node: TSESTree.Node): boolean { return false; } + +/** + * Checks if a node's parent is arrow function expression and a inner node is object expression + */ +function isObjectExpressionInOneLineReturn( + node: TSESTree.Node, + innerNode: TSESTree.Node, +): boolean { + if ( + node.parent?.type === AST_NODE_TYPES.ArrowFunctionExpression && + node.parent?.body === node && + innerNode.type === AST_NODE_TYPES.ObjectExpression + ) { + return true; + } + + return false; +} diff --git a/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts b/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts index 5db9beb9065b..021d37856890 100644 --- a/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts @@ -33,9 +33,9 @@ const x = (a as A) + b; const x = (a as A) + (b); const x = new Generic() as Foo; const x = (new (Generic as Foo)()); -const x = () => ({ bar: 5 } as Foo); -const x = () => ({ bar: 5 } as Foo); -const x = () => (bar as Foo);`; +const x = () => ({ bar: 5 }) as Foo; +const x = () => ({ bar: 5 }) as Foo; +const x = () => bar as Foo;`; const AS_TESTS = `${AS_TESTS_EXCEPT_CONST_CASE} const x = { key: 'value' } as const; diff --git a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts index 2d8073000633..3994fc77313a 100644 --- a/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts +++ b/packages/eslint-plugin/tests/rules/strict-boolean-expressions.test.ts @@ -750,16 +750,16 @@ if (y) { { messageId: 'conditionFixCompareZero', // TODO: fix compare zero suggestion for bigint - output: ` (x: bigint) => (x === 0);`, + output: ` (x: bigint) => x === 0;`, }, { // TODO: remove check NaN suggestion for bigint messageId: 'conditionFixCompareNaN', - output: ` (x: bigint) => (Number.isNaN(x));`, + output: ` (x: bigint) => Number.isNaN(x);`, }, { messageId: 'conditionFixCastBoolean', - output: ` (x: bigint) => (!Boolean(x));`, + output: ` (x: bigint) => !Boolean(x);`, }, ], }, @@ -893,7 +893,7 @@ if (y) { }, { messageId: 'conditionFixCompareFalse', - output: ` (x?: boolean) => (x === false);`, + output: ` (x?: boolean) => x === false;`, }, ], }, @@ -930,7 +930,7 @@ if (y) { ], output: ` declare const x: object | null; if (x != null) {} - (x?: { a: number }) => (x == null); + (x?: { a: number }) => x == null; (x: T) => (x != null) ? 1 : 0; `, }), @@ -970,7 +970,7 @@ if (y) { suggestions: [ { messageId: 'conditionFixCompareNullish', - output: ' (x?: string) => (x == null);', + output: ' (x?: string) => x == null;', }, { messageId: 'conditionFixDefaultEmptyString', @@ -978,7 +978,7 @@ if (y) { }, { messageId: 'conditionFixCastBoolean', - output: ' (x?: string) => (!Boolean(x));', + output: ' (x?: string) => !Boolean(x);', }, ], }, @@ -1064,7 +1064,7 @@ if (y) { suggestions: [ { messageId: 'conditionFixCompareNullish', - output: ' (x?: number) => (x == null);', + output: ' (x?: number) => x == null;', }, { messageId: 'conditionFixDefaultZero', @@ -1072,7 +1072,7 @@ if (y) { }, { messageId: 'conditionFixCastBoolean', - output: ' (x?: number) => (!Boolean(x));', + output: ' (x?: number) => !Boolean(x);', }, ], }, diff --git a/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts b/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts index 591476f75ea9..7717dd821b89 100644 --- a/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts +++ b/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts @@ -3,7 +3,16 @@ import type { TSESTree } from '@typescript-eslint/utils'; import { createRule, getWrappingFixer } from '../../src/util'; import { getFixturesRootDir, RuleTester } from '../RuleTester'; -const rule = createRule({ +const rootPath = getFixturesRootDir(); +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: rootPath, + project: './tsconfig.json', + }, +}); + +const voidEverythingRule = createRule({ name: 'void-everything', defaultOptions: [], meta: { @@ -45,16 +54,7 @@ const rule = createRule({ }, }); -const rootPath = getFixturesRootDir(); -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', - parserOptions: { - tsconfigRootDir: rootPath, - project: './tsconfig.json', - }, -}); - -ruleTester.run('getWrappingFixer', rule, { +ruleTester.run('getWrappingFixer - voidEverythingRule', voidEverythingRule, { valid: [], invalid: [ // should add parens when inner expression might need them @@ -132,11 +132,6 @@ ruleTester.run('getWrappingFixer', rule, { errors: [{ messageId: 'addVoid' }], output: '(void wrapMe).prop', }, - { - code: '() => wrapMe', - errors: [{ messageId: 'addVoid' }], - output: '() => (void wrapMe)', - }, // shouldn't add outer parens when not necessary { @@ -309,3 +304,54 @@ ruleTester.run('getWrappingFixer', rule, { }, ], }); + +const removeFunctionRule = createRule({ + name: 'remove-function', + defaultOptions: [], + meta: { + type: 'suggestion', + fixable: 'code', + docs: { + description: + 'Remove function with first arg remaining in random places for test purposes.', + recommended: false, + }, + messages: { + removeFunction: 'Please remove this function', + }, + schema: [], + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + const report = (node: TSESTree.CallExpression): void => { + context.report({ + node, + messageId: 'removeFunction', + fix: getWrappingFixer({ + sourceCode, + node, + innerNode: [node.arguments[0]], + wrap: code => code, + }), + }); + }; + + return { + 'CallExpression[callee.name="fn"]': report, + }; + }, +}); + +ruleTester.run('getWrappingFixer - removeFunctionRule', removeFunctionRule, { + valid: [], + invalid: [ + // should add parens when a inner node is a part of return body of node's parent + { + code: '() => fn({ x: "wrapObject" })', + errors: [{ messageId: 'removeFunction' }], + output: '() => ({ x: "wrapObject" })', + }, + ], +}); From 620311f05e11dfd50ee3826f19e5b69179465706 Mon Sep 17 00:00:00 2001 From: dora <31735614+dora1998@users.noreply.github.com> Date: Fri, 14 Apr 2023 09:19:01 +0900 Subject: [PATCH 04/12] fix --- .../eslint-plugin/src/util/getWrappingFixer.ts | 10 +++------- .../tests/util/getWrappingFixer.test.ts | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/eslint-plugin/src/util/getWrappingFixer.ts b/packages/eslint-plugin/src/util/getWrappingFixer.ts index f58c7c55926d..8f09301d8d23 100644 --- a/packages/eslint-plugin/src/util/getWrappingFixer.ts +++ b/packages/eslint-plugin/src/util/getWrappingFixer.ts @@ -223,13 +223,9 @@ function isObjectExpressionInOneLineReturn( node: TSESTree.Node, innerNode: TSESTree.Node, ): boolean { - if ( + return ( node.parent?.type === AST_NODE_TYPES.ArrowFunctionExpression && - node.parent?.body === node && + node.parent.body === node && innerNode.type === AST_NODE_TYPES.ObjectExpression - ) { - return true; - } - - return false; + ); } diff --git a/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts b/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts index 7717dd821b89..00fa7b8dccad 100644 --- a/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts +++ b/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts @@ -353,5 +353,22 @@ ruleTester.run('getWrappingFixer - removeFunctionRule', removeFunctionRule, { errors: [{ messageId: 'removeFunction' }], output: '() => ({ x: "wrapObject" })', }, + + // shouldn't add parens when not necessary + { + code: 'const a = fn({ x: "wrapObject" })', + errors: [{ messageId: 'removeFunction' }], + output: 'const a = { x: "wrapObject" }', + }, + { + code: '() => fn("string")', + errors: [{ messageId: 'removeFunction' }], + output: '() => "string"', + }, + { + code: '(params = fn({ x: "wrapObject" })) => "string"', + errors: [{ messageId: 'removeFunction' }], + output: '(params = { x: "wrapObject" }) => "string"', + }, ], }); From 5862985406b152c66f199d5bb26687a31173463e Mon Sep 17 00:00:00 2001 From: dora <31735614+dora1998@users.noreply.github.com> Date: Fri, 14 Apr 2023 09:20:20 +0900 Subject: [PATCH 05/12] fix --- packages/eslint-plugin/tests/util/getWrappingFixer.test.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts b/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts index 00fa7b8dccad..3084d430e402 100644 --- a/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts +++ b/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts @@ -365,10 +365,5 @@ ruleTester.run('getWrappingFixer - removeFunctionRule', removeFunctionRule, { errors: [{ messageId: 'removeFunction' }], output: '() => "string"', }, - { - code: '(params = fn({ x: "wrapObject" })) => "string"', - errors: [{ messageId: 'removeFunction' }], - output: '(params = { x: "wrapObject" }) => "string"', - }, ], }); From 02ada976f0e2ca2fef4baa32406d7436dd61ae98 Mon Sep 17 00:00:00 2001 From: dora <31735614+dora1998@users.noreply.github.com> Date: Sat, 15 Apr 2023 16:41:13 +0900 Subject: [PATCH 06/12] fix --- .../src/util/getWrappingFixer.ts | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/eslint-plugin/src/util/getWrappingFixer.ts b/packages/eslint-plugin/src/util/getWrappingFixer.ts index 8f09301d8d23..f4d3af9861e4 100644 --- a/packages/eslint-plugin/src/util/getWrappingFixer.ts +++ b/packages/eslint-plugin/src/util/getWrappingFixer.ts @@ -35,17 +35,15 @@ export function getWrappingFixer( const innerCodes = innerNodes.map(innerNode => { let code = sourceCode.getText(innerNode); - // check the inner expression's precedence - if (!isStrongPrecedenceNode(innerNode)) { - // the code we are adding might have stronger precedence than our wrapped node - // let's wrap our node in parens in case it has a weaker precedence than the code we are wrapping it in - code = `(${code})`; - } - - // check the inner node is used as return body of arrow function - if (isObjectExpressionInOneLineReturn(node, innerNode)) { - // the code we are editing might break arrow function expressions - // let's wrap our node in parens in case it's gotten mistaken as block statement + /** + * Wrap our node in parens to prevent the following cases: + * - It has a weaker precedence than the code we are wrapping it in + * - It's gotten mistaken as block statement instead of object expression + */ + if ( + !isStrongPrecedenceNode(innerNode) || + isObjectExpressionInOneLineReturn(node, innerNode) + ) { code = `(${code})`; } From 00771e7c0cb137f46e62e1606fe5ef4df0101f6c Mon Sep 17 00:00:00 2001 From: dora <31735614+dora1998@users.noreply.github.com> Date: Mon, 24 Jul 2023 01:26:56 +0900 Subject: [PATCH 07/12] update getOperatorPrecedence --- packages/eslint-plugin/src/util/getOperatorPrecedence.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint-plugin/src/util/getOperatorPrecedence.ts b/packages/eslint-plugin/src/util/getOperatorPrecedence.ts index b7a9d75fb155..5ed87d5b3ff0 100644 --- a/packages/eslint-plugin/src/util/getOperatorPrecedence.ts +++ b/packages/eslint-plugin/src/util/getOperatorPrecedence.ts @@ -265,6 +265,7 @@ export function getOperatorPrecedence( return OperatorPrecedence.Member; case SyntaxKind.AsExpression: + case SyntaxKind.SatisfiesExpression: return OperatorPrecedence.Relational; case SyntaxKind.ThisKeyword: From 2d10a7fb43c9071962ed10e56bed364fc0cd46f0 Mon Sep 17 00:00:00 2001 From: dora <31735614+dora1998@users.noreply.github.com> Date: Mon, 24 Jul 2023 01:28:04 +0900 Subject: [PATCH 08/12] use precedence to wrap node in parens --- .../src/rules/consistent-type-assertions.ts | 49 ++++++++-- .../eslint-plugin/src/util/getWrappedCode.ts | 9 ++ .../rules/consistent-type-assertions.test.ts | 32 +++++-- .../tests/util/getWrappedCode.test.ts | 92 +++++++++++++++++++ 4 files changed, 167 insertions(+), 15 deletions(-) create mode 100644 packages/eslint-plugin/src/util/getWrappedCode.ts create mode 100644 packages/eslint-plugin/tests/util/getWrappedCode.test.ts diff --git a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts index 4fada0d310ec..2d6aa40ec651 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts @@ -1,7 +1,10 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import { isBinaryExpression, isNewExpression } from 'tsutils'; +import * as ts from 'typescript'; import * as util from '../util'; +import { getWrappedCode } from '../util/getWrappedCode'; // intentionally mirroring the options type MessageIds = @@ -79,6 +82,7 @@ export default util.createRule({ ], create(context, [options]) { const sourceCode = context.getSourceCode(); + const parserServices = util.getParserServices(context, true); function isConst(node: TSESTree.TypeNode): boolean { if (node.type !== AST_NODE_TYPES.TSTypeReference) { @@ -122,7 +126,6 @@ export default util.createRule({ if (isConst(node.typeAnnotation) && messageId === 'never') { return; } - context.report({ node, messageId, @@ -132,13 +135,43 @@ export default util.createRule({ : {}, fix: messageId === 'as' - ? util.getWrappingFixer({ - sourceCode, - node, - innerNode: [node.expression, node.typeAnnotation], - wrap: (expressionCode, typeAnnotationCode) => - `${expressionCode} as ${typeAnnotationCode}`, - }) + ? (fixer): TSESLint.RuleFix => { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get( + node as TSESTree.TSTypeAssertion, + ); + + /** + * AsExpression has lower precedence than TypeAssertionExpression, + * so we don't need to wrap expression and typeAnnotation in parens. + */ + const expressionCode = sourceCode.getText(node.expression); + const typeAnnotationCode = sourceCode.getText( + node.typeAnnotation, + ); + + const asPrecedence = util.getOperatorPrecedence( + ts.SyntaxKind.AsExpression, + ts.SyntaxKind.Unknown, + ); + const parentPrecedence = util.getOperatorPrecedence( + tsNode.parent.kind, + isBinaryExpression(tsNode.parent) + ? tsNode.parent.operatorToken.kind + : ts.SyntaxKind.Unknown, + isNewExpression(tsNode.parent) + ? tsNode.parent.arguments != null && + tsNode.parent.arguments.length > 0 + : undefined, + ); + + const text = `${expressionCode} as ${typeAnnotationCode}`; + return fixer.replaceText( + node, + util.isParenthesized(node, sourceCode) + ? text + : getWrappedCode(text, asPrecedence, parentPrecedence), + ); + } : undefined, }); } diff --git a/packages/eslint-plugin/src/util/getWrappedCode.ts b/packages/eslint-plugin/src/util/getWrappedCode.ts new file mode 100644 index 000000000000..db2999ab9922 --- /dev/null +++ b/packages/eslint-plugin/src/util/getWrappedCode.ts @@ -0,0 +1,9 @@ +import type { OperatorPrecedence } from './getOperatorPrecedence'; + +export function getWrappedCode( + text: string, + nodePrecedence: OperatorPrecedence, + parentPrecedence: OperatorPrecedence, +): string { + return nodePrecedence > parentPrecedence ? text : `(${text})`; +} diff --git a/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts b/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts index 021d37856890..f27ddd879a4c 100644 --- a/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts @@ -14,7 +14,8 @@ const x = !'string'; const x = a + b; const x = <(A)>a + (b); const x = (new Generic()); -const x = (new (Generic)()); +const x = new (Generic)(); +const x = new (Generic)('string'); const x = () => { bar: 5 }; const x = () => ({ bar: 5 }); const x = () => bar;`; @@ -27,15 +28,16 @@ const AS_TESTS_EXCEPT_CONST_CASE = ` const x = new Generic() as Foo; const x = b as A; const x = [1] as readonly number[]; -const x = 'string' as (a | b); -const x = (!'string') as A; +const x = 'string' as a | b; +const x = !'string' as A; const x = (a as A) + b; const x = (a as A) + (b); const x = new Generic() as Foo; -const x = (new (Generic as Foo)()); -const x = () => ({ bar: 5 }) as Foo; -const x = () => ({ bar: 5 }) as Foo; -const x = () => bar as Foo;`; +const x = new (Generic as Foo)(); +const x = new (Generic as Foo)('string'); +const x = () => ({ bar: 5 } as Foo); +const x = () => ({ bar: 5 } as Foo); +const x = () => (bar as Foo);`; const AS_TESTS = `${AS_TESTS_EXCEPT_CONST_CASE} const x = { key: 'value' } as const; @@ -216,6 +218,10 @@ ruleTester.run('consistent-type-assertions', rule, { messageId: 'angle-bracket', line: 14, }, + { + messageId: 'angle-bracket', + line: 15, + }, ], }), ...batchedSingleLineTests({ @@ -278,6 +284,10 @@ ruleTester.run('consistent-type-assertions', rule, { messageId: 'as', line: 14, }, + { + messageId: 'as', + line: 15, + }, ], output: AS_TESTS, }), @@ -337,6 +347,10 @@ ruleTester.run('consistent-type-assertions', rule, { messageId: 'never', line: 13, }, + { + messageId: 'never', + line: 14, + }, ], }), ...batchedSingleLineTests({ @@ -395,6 +409,10 @@ ruleTester.run('consistent-type-assertions', rule, { messageId: 'never', line: 13, }, + { + messageId: 'never', + line: 14, + }, ], }), ...batchedSingleLineTests({ diff --git a/packages/eslint-plugin/tests/util/getWrappedCode.test.ts b/packages/eslint-plugin/tests/util/getWrappedCode.test.ts new file mode 100644 index 000000000000..716992c88e66 --- /dev/null +++ b/packages/eslint-plugin/tests/util/getWrappedCode.test.ts @@ -0,0 +1,92 @@ +import type { TSESTree } from '@typescript-eslint/utils'; +import { isBinaryExpression } from 'tsutils'; +import * as ts from 'typescript'; + +import * as util from '../../src/util'; +import { getWrappedCode } from '../../src/util/getWrappedCode'; +import { getFixturesRootDir, RuleTester } from '../RuleTester'; + +const rootPath = getFixturesRootDir(); +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: rootPath, + project: './tsconfig.json', + }, +}); + +const removeFunctionRule = util.createRule({ + name: 'remove-function', + defaultOptions: [], + meta: { + type: 'suggestion', + fixable: 'code', + docs: { + description: + 'Remove function with first arg remaining in random places for test purposes.', + recommended: false, + }, + messages: { + removeFunction: 'Please remove this function', + }, + schema: [], + }, + + create(context) { + const sourceCode = context.getSourceCode(); + const parserServices = util.getParserServices(context, true); + + const report = (node: TSESTree.CallExpression): void => { + context.report({ + node, + messageId: 'removeFunction', + fix: fixer => { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); + const tsArgumentNode = tsNode.arguments[0]; + + const nodePrecedence = util.getOperatorPrecedence( + tsArgumentNode.kind, + isBinaryExpression(tsArgumentNode) + ? tsArgumentNode.operatorToken.kind + : ts.SyntaxKind.Unknown, + ); + const parentPrecedence = util.getOperatorPrecedence( + tsNode.parent.kind, + isBinaryExpression(tsNode.parent) + ? tsNode.parent.operatorToken.kind + : ts.SyntaxKind.Unknown, + ); + + const text = sourceCode.getText(node.arguments[0]); + return fixer.replaceText( + node, + getWrappedCode(text, nodePrecedence, parentPrecedence), + ); + }, + }); + }; + + return { + 'CallExpression[callee.name="fn"]': report, + }; + }, +}); + +ruleTester.run('getWrappedCode - removeFunctionRule', removeFunctionRule, { + valid: [], + invalid: [ + // should add parens when the first argument node has lower precedence than the parent node of the CallExpression + { + code: '() => fn({ x: "wrapObject" })', + errors: [{ messageId: 'removeFunction' }], + output: '() => ({ x: "wrapObject" })', + }, + + // shouldn't add parens when not necessary + { + code: 'const a = fn({ x: "wrapObject" })', + errors: [{ messageId: 'removeFunction' }], + output: 'const a = { x: "wrapObject" }', + }, + ], +}); From 0552295fe590a8bae1de8fc00fa067ee9180c1ef Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 15 Aug 2023 14:45:50 -0400 Subject: [PATCH 09/12] Fix up post-merge --- .../src/rules/consistent-type-assertions.ts | 6 +++--- packages/eslint-plugin/tests/util/getWrappedCode.test.ts | 9 ++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts index 092cba507ec4..a5de2723f5d9 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts @@ -1,6 +1,6 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -import { isBinaryExpression, isNewExpression } from 'tsutils'; +// import { isBinaryExpression, isNewExpression } from 'ts-api-utils'; import * as ts from 'typescript'; import * as util from '../util'; @@ -158,10 +158,10 @@ export default util.createRule({ ); const parentPrecedence = util.getOperatorPrecedence( tsNode.parent.kind, - isBinaryExpression(tsNode.parent) + ts.isBinaryExpression(tsNode.parent) ? tsNode.parent.operatorToken.kind : ts.SyntaxKind.Unknown, - isNewExpression(tsNode.parent) + ts.isNewExpression(tsNode.parent) ? tsNode.parent.arguments != null && tsNode.parent.arguments.length > 0 : undefined, diff --git a/packages/eslint-plugin/tests/util/getWrappedCode.test.ts b/packages/eslint-plugin/tests/util/getWrappedCode.test.ts index 716992c88e66..b99139a36ae1 100644 --- a/packages/eslint-plugin/tests/util/getWrappedCode.test.ts +++ b/packages/eslint-plugin/tests/util/getWrappedCode.test.ts @@ -1,10 +1,10 @@ import type { TSESTree } from '@typescript-eslint/utils'; -import { isBinaryExpression } from 'tsutils'; +import { RuleTester } from '@typescript-eslint/rule-tester'; import * as ts from 'typescript'; import * as util from '../../src/util'; import { getWrappedCode } from '../../src/util/getWrappedCode'; -import { getFixturesRootDir, RuleTester } from '../RuleTester'; +import { getFixturesRootDir } from '../RuleTester'; const rootPath = getFixturesRootDir(); const ruleTester = new RuleTester({ @@ -24,7 +24,6 @@ const removeFunctionRule = util.createRule({ docs: { description: 'Remove function with first arg remaining in random places for test purposes.', - recommended: false, }, messages: { removeFunction: 'Please remove this function', @@ -46,13 +45,13 @@ const removeFunctionRule = util.createRule({ const nodePrecedence = util.getOperatorPrecedence( tsArgumentNode.kind, - isBinaryExpression(tsArgumentNode) + ts.isBinaryExpression(tsArgumentNode) ? tsArgumentNode.operatorToken.kind : ts.SyntaxKind.Unknown, ); const parentPrecedence = util.getOperatorPrecedence( tsNode.parent.kind, - isBinaryExpression(tsNode.parent) + ts.isBinaryExpression(tsNode.parent) ? tsNode.parent.operatorToken.kind : ts.SyntaxKind.Unknown, ); From 05999352ef11bf814fce1e95c5e016450b5b1f3a Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 15 Aug 2023 14:46:39 -0400 Subject: [PATCH 10/12] lint --fix --- packages/eslint-plugin/tests/util/getWrappedCode.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/tests/util/getWrappedCode.test.ts b/packages/eslint-plugin/tests/util/getWrappedCode.test.ts index b99139a36ae1..516642c16b57 100644 --- a/packages/eslint-plugin/tests/util/getWrappedCode.test.ts +++ b/packages/eslint-plugin/tests/util/getWrappedCode.test.ts @@ -1,5 +1,5 @@ -import type { TSESTree } from '@typescript-eslint/utils'; import { RuleTester } from '@typescript-eslint/rule-tester'; +import type { TSESTree } from '@typescript-eslint/utils'; import * as ts from 'typescript'; import * as util from '../../src/util'; From 0e2277d9de4bb83b8307fb7c66c496357482ca1f Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Tue, 15 Aug 2023 14:48:39 -0400 Subject: [PATCH 11/12] remove comment --- packages/eslint-plugin/src/rules/consistent-type-assertions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts index a5de2723f5d9..d2e610255f17 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts @@ -1,6 +1,5 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -// import { isBinaryExpression, isNewExpression } from 'ts-api-utils'; import * as ts from 'typescript'; import * as util from '../util'; From 6948bfd04f76e7572345631ae4cdea90d97b3ea8 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sat, 26 Aug 2023 15:04:36 -0400 Subject: [PATCH 12/12] Remove post-merge artifact --- packages/eslint-plugin/tests/util/getWrappingFixer.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts b/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts index 186e32b52d6b..ac7c8740b0d3 100644 --- a/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts +++ b/packages/eslint-plugin/tests/util/getWrappingFixer.test.ts @@ -314,7 +314,6 @@ const removeFunctionRule = createRule({ docs: { description: 'Remove function with first arg remaining in random places for test purposes.', - recommended: false, }, messages: { removeFunction: 'Please remove this function',