diff --git a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts index 544b78e177fd..58403fe8a141 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-assertions.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-assertions.ts @@ -8,7 +8,9 @@ type MessageIds = | 'as' | 'angle-bracket' | 'never' - | 'unexpectedObjectTypeAssertion'; + | 'unexpectedObjectTypeAssertion' + | 'replaceObjectTypeAssertionWithAnnotation' + | 'replaceObjectTypeAssertionWithSatisfies'; type OptUnion = | { assertionStyle: 'as' | 'angle-bracket'; @@ -24,6 +26,7 @@ export default util.createRule({ meta: { type: 'suggestion', fixable: 'code', + hasSuggestions: true, docs: { description: 'Enforce consistent usage of type assertions', recommended: 'strict', @@ -33,6 +36,10 @@ export default util.createRule({ 'angle-bracket': "Use '<{{cast}}>' instead of 'as {{cast}}'.", never: 'Do not use any type assertions.', unexpectedObjectTypeAssertion: 'Always prefer const x: T = { ... }.', + replaceObjectTypeAssertionWithAnnotation: + 'Use const x: {{cast}} = { ... } instead.', + replaceObjectTypeAssertionWithSatisfies: + 'Use const x = { ... } satisfies {{cast}} instead.', }, schema: [ { @@ -184,9 +191,42 @@ export default util.createRule({ checkType(node.typeAnnotation) && node.expression.type === AST_NODE_TYPES.ObjectExpression ) { + const suggest: TSESLint.ReportSuggestionArray = []; + if ( + node.parent?.type === AST_NODE_TYPES.VariableDeclarator && + !node.parent.id.typeAnnotation + ) { + const { parent } = node; + suggest.push({ + messageId: 'replaceObjectTypeAssertionWithAnnotation', + data: { cast: sourceCode.getText(node.typeAnnotation) }, + fix: fixer => [ + fixer.insertTextAfter( + parent.id, + `: ${sourceCode.getText(node.typeAnnotation)}`, + ), + fixer.replaceText(node, getTextWithParentheses(node.expression)), + ], + }); + } + suggest.push({ + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: sourceCode.getText(node.typeAnnotation) }, + fix: fixer => [ + fixer.replaceText(node, getTextWithParentheses(node.expression)), + fixer.insertTextAfter( + node, + ` satisfies ${context + .getSourceCode() + .getText(node.typeAnnotation)}`, + ), + ], + }); + context.report({ node, messageId: 'unexpectedObjectTypeAssertion', + suggest, }); } } 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 d8d61749d87a..45f672af9e4d 100644 --- a/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts @@ -37,9 +37,13 @@ const x = { key: 'value' } as const; const OBJECT_LITERAL_AS_CASTS = ` const x = {} as Foo; +const x = ({}) as a | b; +const x = {} as A + b; `; const OBJECT_LITERAL_ANGLE_BRACKET_CASTS = ` const x = >{}; +const x = ({}); +const x = {} + b; `; const OBJECT_LITERAL_ARGUMENT_AS_CASTS = ` print({ bar: 5 } as Foo) @@ -351,6 +355,45 @@ ruleTester.run('consistent-type-assertions', rule, { { messageId: 'unexpectedObjectTypeAssertion', line: 2, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithAnnotation', + data: { cast: 'Foo' }, + output: 'const x: Foo = {};', + }, + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'Foo' }, + output: 'const x = {} satisfies Foo;', + }, + ], + }, + { + messageId: 'unexpectedObjectTypeAssertion', + line: 3, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithAnnotation', + data: { cast: 'a | b' }, + output: 'const x: a | b = ({});', + }, + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'a | b' }, + output: 'const x = ({}) satisfies a | b;', + }, + ], + }, + { + messageId: 'unexpectedObjectTypeAssertion', + line: 4, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'A' }, + output: 'const x = {} satisfies A + b;', + }, + ], }, ], }), @@ -366,6 +409,45 @@ ruleTester.run('consistent-type-assertions', rule, { { messageId: 'unexpectedObjectTypeAssertion', line: 2, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithAnnotation', + data: { cast: 'Foo' }, + output: 'const x: Foo = {};', + }, + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'Foo' }, + output: 'const x = {} satisfies Foo;', + }, + ], + }, + { + messageId: 'unexpectedObjectTypeAssertion', + line: 3, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithAnnotation', + data: { cast: 'a | b' }, + output: 'const x: a | b = ({});', + }, + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'a | b' }, + output: 'const x = ({}) satisfies a | b;', + }, + ], + }, + { + messageId: 'unexpectedObjectTypeAssertion', + line: 4, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'A' }, + output: 'const x = {} satisfies A + b;', + }, + ], }, ], }), @@ -381,34 +463,122 @@ ruleTester.run('consistent-type-assertions', rule, { { messageId: 'unexpectedObjectTypeAssertion', line: 2, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithAnnotation', + data: { cast: 'Foo' }, + output: 'const x: Foo = {};', + }, + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'Foo' }, + output: 'const x = {} satisfies Foo;', + }, + ], }, { messageId: 'unexpectedObjectTypeAssertion', line: 3, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithAnnotation', + data: { cast: 'a | b' }, + output: 'const x: a | b = ({});', + }, + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'a | b' }, + output: 'const x = ({}) satisfies a | b;', + }, + ], }, { messageId: 'unexpectedObjectTypeAssertion', line: 4, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'A' }, + output: 'const x = {} satisfies A + b;', + }, + ], }, { messageId: 'unexpectedObjectTypeAssertion', line: 5, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'Foo' }, + output: 'print({ bar: 5 } satisfies Foo)', + }, + ], }, { messageId: 'unexpectedObjectTypeAssertion', line: 6, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'Foo' }, + output: 'new print({ bar: 5 } satisfies Foo)', + }, + ], }, { messageId: 'unexpectedObjectTypeAssertion', line: 7, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'Foo' }, + output: 'function foo() { throw { bar: 5 } satisfies Foo }', + }, + ], }, { messageId: 'unexpectedObjectTypeAssertion', line: 8, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'Foo.Bar' }, + output: 'function b(x = {} satisfies Foo.Bar) {}', + }, + ], }, { messageId: 'unexpectedObjectTypeAssertion', line: 9, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'Foo' }, + output: 'function c(x = {} satisfies Foo) {}', + }, + ], + }, + { + messageId: 'unexpectedObjectTypeAssertion', + line: 10, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'Foo' }, + output: 'print?.({ bar: 5 } satisfies Foo)', + }, + ], + }, + { + messageId: 'unexpectedObjectTypeAssertion', + line: 11, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'Foo' }, + output: 'print?.call({ bar: 5 } satisfies Foo)', + }, + ], }, ], }), @@ -424,26 +594,100 @@ ruleTester.run('consistent-type-assertions', rule, { { messageId: 'unexpectedObjectTypeAssertion', line: 2, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithAnnotation', + data: { cast: 'Foo' }, + output: 'const x: Foo = {};', + }, + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'Foo' }, + output: 'const x = {} satisfies Foo;', + }, + ], }, { messageId: 'unexpectedObjectTypeAssertion', line: 3, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithAnnotation', + data: { cast: 'a | b' }, + output: 'const x: a | b = ({});', + }, + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'a | b' }, + output: 'const x = ({}) satisfies a | b;', + }, + ], }, { messageId: 'unexpectedObjectTypeAssertion', line: 4, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'A' }, + output: 'const x = {} satisfies A + b;', + }, + ], }, { messageId: 'unexpectedObjectTypeAssertion', line: 5, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'Foo' }, + output: 'print({ bar: 5 } satisfies Foo)', + }, + ], }, { messageId: 'unexpectedObjectTypeAssertion', line: 6, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'Foo' }, + output: 'new print({ bar: 5 } satisfies Foo)', + }, + ], }, { messageId: 'unexpectedObjectTypeAssertion', line: 7, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'Foo' }, + output: 'function foo() { throw { bar: 5 } satisfies Foo }', + }, + ], + }, + { + messageId: 'unexpectedObjectTypeAssertion', + line: 8, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'Foo' }, + output: 'print?.({ bar: 5 } satisfies Foo)', + }, + ], + }, + { + messageId: 'unexpectedObjectTypeAssertion', + line: 9, + suggestions: [ + { + messageId: 'replaceObjectTypeAssertionWithSatisfies', + data: { cast: 'Foo' }, + output: 'print?.call({ bar: 5 } satisfies Foo)', + }, + ], }, ], }),