diff --git a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts index 73fbcc27af3..9a4949b734b 100644 --- a/packages/eslint-plugin/src/rules/prefer-optional-chain.ts +++ b/packages/eslint-plugin/src/rules/prefer-optional-chain.ts @@ -11,7 +11,8 @@ type ValidChainTarget = | TSESTree.ChainExpression | TSESTree.Identifier | TSESTree.MemberExpression - | TSESTree.ThisExpression; + | TSESTree.ThisExpression + | TSESTree.MetaProperty; /* The AST is always constructed such the first element is always the deepest element. @@ -115,10 +116,12 @@ export default util.createRule({ 'LogicalExpression[operator="||"] > UnaryExpression[operator="!"] > Identifier', 'LogicalExpression[operator="||"] > UnaryExpression[operator="!"] > MemberExpression', 'LogicalExpression[operator="||"] > UnaryExpression[operator="!"] > ChainExpression > MemberExpression', + 'LogicalExpression[operator="||"] > UnaryExpression[operator="!"] > MetaProperty', ].join(',')]( initialIdentifierOrNotEqualsExpr: | TSESTree.Identifier - | TSESTree.MemberExpression, + | TSESTree.MemberExpression + | TSESTree.MetaProperty, ): void { // selector guarantees this cast const initialExpression = ( @@ -190,13 +193,15 @@ export default util.createRule({ 'LogicalExpression[operator="&&"] > Identifier', 'LogicalExpression[operator="&&"] > MemberExpression', 'LogicalExpression[operator="&&"] > ChainExpression > MemberExpression', + 'LogicalExpression[operator="&&"] > MetaProperty', 'LogicalExpression[operator="&&"] > BinaryExpression[operator="!=="]', 'LogicalExpression[operator="&&"] > BinaryExpression[operator="!="]', ].join(',')]( initialIdentifierOrNotEqualsExpr: | TSESTree.BinaryExpression | TSESTree.Identifier - | TSESTree.MemberExpression, + | TSESTree.MemberExpression + | TSESTree.MetaProperty, ): void { // selector guarantees this cast const initialExpression = ( @@ -342,6 +347,10 @@ export default util.createRule({ return node.name; } + if (node.type === AST_NODE_TYPES.MetaProperty) { + return `${node.meta.name}.${node.property.name}`; + } + if (node.type === AST_NODE_TYPES.ThisExpression) { return 'this'; } @@ -381,6 +390,7 @@ export default util.createRule({ objectText = getMemberExpressionText(node.object); break; + case AST_NODE_TYPES.MetaProperty: case AST_NODE_TYPES.ThisExpression: objectText = getText(node.object); break; @@ -441,6 +451,7 @@ const ALLOWED_MEMBER_OBJECT_TYPES: ReadonlySet = new Set([ AST_NODE_TYPES.Identifier, AST_NODE_TYPES.MemberExpression, AST_NODE_TYPES.ThisExpression, + AST_NODE_TYPES.MetaProperty, ]); const ALLOWED_COMPUTED_PROP_TYPES: ReadonlySet = new Set([ AST_NODE_TYPES.Identifier, @@ -609,7 +620,8 @@ function isValidChainTarget( if ( allowIdentifier && (node.type === AST_NODE_TYPES.Identifier || - node.type === AST_NODE_TYPES.ThisExpression) + node.type === AST_NODE_TYPES.ThisExpression || + node.type === AST_NODE_TYPES.MetaProperty) ) { return true; } diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts index e1a1467d80f..120ad20aaea 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain.test.ts @@ -211,6 +211,12 @@ ruleTester.run('prefer-optional-chain', rule, { '!foo!.bar || !foo!.bar.baz;', '!foo!.bar!.baz || !foo!.bar!.baz!.paz;', '!foo.bar!.baz || !foo.bar!.baz!.paz;', + 'import.meta || true;', + 'import.meta || import.meta.foo;', + '!import.meta && false;', + '!import.meta && !import.meta.foo;', + 'new.target || new.target.length;', + '!new.target || true;', ], invalid: [ ...baseCases, @@ -1321,5 +1327,78 @@ foo?.bar(/* comment */a, }, ], }, + { + code: ` + class Foo { + constructor() { + new.target && new.target.length; + } + } + `, + output: null, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: ` + class Foo { + constructor() { + new.target?.length; + } + } + `, + }, + ], + }, + ], + }, + { + code: noFormat`import.meta && import.meta?.baz;`, + output: null, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: noFormat`import.meta?.baz;`, + }, + ], + }, + ], + }, + { + code: noFormat`!import.meta || !import.meta?.baz;`, + output: null, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: noFormat`!import.meta?.baz;`, + }, + ], + }, + ], + }, + + { + code: noFormat`import.meta && import.meta?.() && import.meta?.().baz;`, + output: null, + errors: [ + { + messageId: 'preferOptionalChain', + suggestions: [ + { + messageId: 'optionalChainSuggest', + output: noFormat`import.meta?.()?.baz;`, + }, + ], + }, + ], + }, ], });