From a93cebfc0f2026c50972bcb110bcd3295ba9a44d Mon Sep 17 00:00:00 2001 From: Idan Attias Date: Mon, 1 Nov 2021 05:29:14 +0200 Subject: [PATCH] feat(eslint-plugin): [no-shadow] exclude external type declaration merging (#3959) * fix(eslint-plugin): [no-shadow] exclude external type declaration merging * fix(eslint-plugin): [no-shadow] typo * Apply suggestions from code review Co-authored-by: Josh Goldberg * fix(eslint-plugin): lint - remove isStringLiteral (no longer used) Co-authored-by: Josh Goldberg --- packages/eslint-plugin/src/rules/no-shadow.ts | 49 ++++++++++- .../tests/rules/no-shadow.test.ts | 87 +++++++++++++++++++ 2 files changed, 134 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-shadow.ts b/packages/eslint-plugin/src/rules/no-shadow.ts index 27c68930931..dfba42cf301 100644 --- a/packages/eslint-plugin/src/rules/no-shadow.ts +++ b/packages/eslint-plugin/src/rules/no-shadow.ts @@ -94,10 +94,10 @@ export default util.createRule({ } function isTypeImport( - definition: Definition, + definition?: Definition, ): definition is ImportBindingDefinition { return ( - definition.type === DefinitionType.ImportBinding && + definition?.type === DefinitionType.ImportBinding && definition.parent.importKind === 'type' ); } @@ -224,6 +224,47 @@ export default util.createRule({ ); } + function isImportDeclaration( + definition: + | TSESTree.ImportDeclaration + | TSESTree.TSImportEqualsDeclaration, + ): definition is TSESTree.ImportDeclaration { + return definition.type === AST_NODE_TYPES.ImportDeclaration; + } + + function isExternalModuleDeclarationWithName( + scope: TSESLint.Scope.Scope, + name: string, + ): boolean { + return ( + scope.type === ScopeType.tsModule && + scope.block.type === AST_NODE_TYPES.TSModuleDeclaration && + scope.block.id.type === AST_NODE_TYPES.Literal && + scope.block.id.value === name + ); + } + + function isExternalDeclarationMerging( + scope: TSESLint.Scope.Scope, + variable: TSESLint.Scope.Variable, + shadowed: TSESLint.Scope.Variable, + ): boolean { + const [firstDefinition] = shadowed.defs; + const [secondDefinition] = variable.defs; + + return ( + isTypeImport(firstDefinition) && + isImportDeclaration(firstDefinition.parent) && + isExternalModuleDeclarationWithName( + scope, + firstDefinition.parent.source.value, + ) && + secondDefinition.node.type === AST_NODE_TYPES.TSInterfaceDeclaration && + secondDefinition.node.parent?.type === + AST_NODE_TYPES.ExportNamedDeclaration + ); + } + /** * Check if variable name is allowed. * @param variable The variable to check. @@ -403,6 +444,10 @@ export default util.createRule({ continue; } + if (isExternalDeclarationMerging(scope, variable, shadowed)) { + continue; + } + const isESLintGlobal = 'writeable' in shadowed; if ( (shadowed.identifiers.length > 0 || diff --git a/packages/eslint-plugin/tests/rules/no-shadow.test.ts b/packages/eslint-plugin/tests/rules/no-shadow.test.ts index c3e5a09f344..bedb825df70 100644 --- a/packages/eslint-plugin/tests/rules/no-shadow.test.ts +++ b/packages/eslint-plugin/tests/rules/no-shadow.test.ts @@ -51,6 +51,15 @@ class Foo { } interface Foo { prop2: string; +} + `, + ` +import type { Foo } from 'bar'; + +declare module 'bar' { + export interface Foo { + x: string; + } } `, // type value shadowing @@ -1442,5 +1451,83 @@ function doThing(foo: number, bar: number) {} }, ], }, + { + code: ` +interface Foo {} + +declare module 'bar' { + export interface Foo { + x: string; + } +} + `, + errors: [ + { + messageId: 'noShadow', + data: { name: 'Foo' }, + type: AST_NODE_TYPES.Identifier, + line: 5, + column: 20, + }, + ], + }, + { + code: ` +import type { Foo } from 'bar'; + +declare module 'baz' { + export interface Foo { + x: string; + } +} + `, + errors: [ + { + messageId: 'noShadow', + data: { name: 'Foo' }, + type: AST_NODE_TYPES.Identifier, + line: 5, + column: 20, + }, + ], + }, + { + code: ` +import type { Foo } from 'bar'; + +declare module 'bar' { + export type Foo = string; +} + `, + errors: [ + { + messageId: 'noShadow', + data: { name: 'Foo' }, + type: AST_NODE_TYPES.Identifier, + line: 5, + column: 15, + }, + ], + }, + { + code: ` +import type { Foo } from 'bar'; + +declare module 'bar' { + interface Foo { + x: string; + } +} + `, + errors: [ + { + messageId: 'noShadow', + data: { name: 'Foo' }, + type: AST_NODE_TYPES.Identifier, + line: 5, + column: 13, + }, + ], + }, ], });