From f9f48f612b9afbcb2095014464d7b372d1ed9e40 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Tue, 24 Dec 2024 15:29:36 +0200 Subject: [PATCH 1/5] initial implementation --- packages/eslint-plugin/src/rules/no-shadow.ts | 45 ++++++++---- .../tests/rules/no-shadow/no-shadow.test.ts | 72 +++++++++++++++++++ 2 files changed, 105 insertions(+), 12 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-shadow.ts b/packages/eslint-plugin/src/rules/no-shadow.ts index 0e1f87149608..ae168c6480a9 100644 --- a/packages/eslint-plugin/src/rules/no-shadow.ts +++ b/packages/eslint-plugin/src/rules/no-shadow.ts @@ -11,7 +11,7 @@ type Options = [ { allow?: string[]; builtinGlobals?: boolean; - hoist?: 'all' | 'functions' | 'never'; + hoist?: 'all' | 'functions' | 'functions-and-types' | 'never' | 'types'; ignoreFunctionTypeParameterNameValueShadow?: boolean; ignoreOnInitialization?: boolean; ignoreTypeValueShadow?: boolean; @@ -28,6 +28,13 @@ const allowedFunctionVariableDefTypes = new Set([ AST_NODE_TYPES.TSConstructorType, ]); +const functionsHoistedNodes = new Set([AST_NODE_TYPES.FunctionDeclaration]); + +const typesHoistedNodes = new Set([ + AST_NODE_TYPES.TSInterfaceDeclaration, + AST_NODE_TYPES.TSTypeAliasDeclaration, +]); + export default createRule({ name: 'no-shadow', meta: { @@ -63,7 +70,7 @@ export default createRule({ type: 'string', description: 'Whether to report shadowing before outer functions or variables are defined.', - enum: ['all', 'functions', 'never'], + enum: ['all', 'functions', 'functions-and-types', 'never', 'types'], }, ignoreFunctionTypeParameterNameValueShadow: { type: 'boolean', @@ -88,7 +95,7 @@ export default createRule({ { allow: [], builtinGlobals: false, - hoist: 'functions', + hoist: 'functions-and-types', ignoreFunctionTypeParameterNameValueShadow: true, ignoreOnInitialization: false, ignoreTypeValueShadow: true, @@ -513,15 +520,29 @@ export default createRule({ const inner = getNameRange(variable); const outer = getNameRange(scopeVar); - return !!( - inner && - outer && - inner[1] < outer[0] && - // Excepts FunctionDeclaration if is {"hoist":"function"}. - (options.hoist !== 'functions' || - !outerDef || - outerDef.node.type !== AST_NODE_TYPES.FunctionDeclaration) - ); + if (!inner || !outer || inner[1] >= outer[0] || !outerDef) { + return false; + } + + // Excepts function declaration nodes if is {"hoist":"function"}. + if (options.hoist === 'functions') { + return !functionsHoistedNodes.has(outerDef.node.type); + } + + // Excepts type declaration nodes if is {"hoist":"types"}. + if (options.hoist === 'types') { + return !typesHoistedNodes.has(outerDef.node.type); + } + + // Except both if is {"hoist":"functions-and-types"} + if (options.hoist === 'functions-and-types') { + return ( + !functionsHoistedNodes.has(outerDef.node.type) && + !typesHoistedNodes.has(outerDef.node.type) + ); + } + + return true; } /** diff --git a/packages/eslint-plugin/tests/rules/no-shadow/no-shadow.test.ts b/packages/eslint-plugin/tests/rules/no-shadow/no-shadow.test.ts index 6dd82ef35747..5746214f7aa9 100644 --- a/packages/eslint-plugin/tests/rules/no-shadow/no-shadow.test.ts +++ b/packages/eslint-plugin/tests/rules/no-shadow/no-shadow.test.ts @@ -549,6 +549,78 @@ let y; }, { code: ` +type Foo = 1; +type A = 1; + `, + errors: [ + { + data: { + name: 'A', + shadowedColumn: 6, + shadowedLine: 3, + }, + messageId: 'noShadow', + type: AST_NODE_TYPES.Identifier, + }, + ], + options: [{ hoist: 'types' }], + }, + { + code: ` +interface Foo {} +type A = 1; + `, + errors: [ + { + data: { + name: 'A', + shadowedColumn: 6, + shadowedLine: 3, + }, + messageId: 'noShadow', + type: AST_NODE_TYPES.Identifier, + }, + ], + options: [{ hoist: 'types' }], + }, + { + code: ` +interface Foo {} +interface A {} + `, + errors: [ + { + data: { + name: 'A', + shadowedColumn: 11, + shadowedLine: 3, + }, + messageId: 'noShadow', + type: AST_NODE_TYPES.Identifier, + }, + ], + options: [{ hoist: 'types' }], + }, + { + code: ` +type Foo = 1; +interface A {} + `, + errors: [ + { + data: { + name: 'A', + shadowedColumn: 11, + shadowedLine: 3, + }, + messageId: 'noShadow', + type: AST_NODE_TYPES.Identifier, + }, + ], + options: [{ hoist: 'types' }], + }, + { + code: ` function foo any>(fn: T, args: any[]) {} `, errors: [ From 0eb600bb557369847f877677417c493bfd4bb4fe Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Thu, 26 Dec 2024 14:43:13 +0200 Subject: [PATCH 2/5] add relevant tests --- packages/eslint-plugin/src/rules/no-shadow.ts | 3 - .../tests/rules/no-shadow/no-shadow.test.ts | 380 ++++++++++++++++++ 2 files changed, 380 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-shadow.ts b/packages/eslint-plugin/src/rules/no-shadow.ts index ae168c6480a9..ebc3cc88f20c 100644 --- a/packages/eslint-plugin/src/rules/no-shadow.ts +++ b/packages/eslint-plugin/src/rules/no-shadow.ts @@ -524,17 +524,14 @@ export default createRule({ return false; } - // Excepts function declaration nodes if is {"hoist":"function"}. if (options.hoist === 'functions') { return !functionsHoistedNodes.has(outerDef.node.type); } - // Excepts type declaration nodes if is {"hoist":"types"}. if (options.hoist === 'types') { return !typesHoistedNodes.has(outerDef.node.type); } - // Except both if is {"hoist":"functions-and-types"} if (options.hoist === 'functions-and-types') { return ( !functionsHoistedNodes.has(outerDef.node.type) && diff --git a/packages/eslint-plugin/tests/rules/no-shadow/no-shadow.test.ts b/packages/eslint-plugin/tests/rules/no-shadow/no-shadow.test.ts index 5746214f7aa9..e4c420092c93 100644 --- a/packages/eslint-plugin/tests/rules/no-shadow/no-shadow.test.ts +++ b/packages/eslint-plugin/tests/rules/no-shadow/no-shadow.test.ts @@ -549,6 +549,25 @@ let y; }, { code: ` +let x = foo((x, y) => {}); +let y; + `, + errors: [ + { + data: { + name: 'x', + shadowedColumn: 5, + shadowedLine: 2, + }, + messageId: 'noShadow', + type: AST_NODE_TYPES.Identifier, + }, + ], + languageOptions: { parserOptions: { ecmaVersion: 6 } }, + options: [{ hoist: 'functions' }], + }, + { + code: ` type Foo = 1; type A = 1; `, @@ -619,6 +638,273 @@ interface A {} ], options: [{ hoist: 'types' }], }, + { + code: ` +{ + type A = 1; +} +type A = 1; + `, + errors: [ + { + data: { + name: 'A', + shadowedColumn: 6, + shadowedLine: 5, + }, + messageId: 'noShadow', + type: AST_NODE_TYPES.Identifier, + }, + ], + options: [{ hoist: 'types' }], + }, + { + code: ` +{ + interface A {} +} +type A = 1; + `, + errors: [ + { + data: { + name: 'A', + shadowedColumn: 6, + shadowedLine: 5, + }, + messageId: 'noShadow', + type: AST_NODE_TYPES.Identifier, + }, + ], + options: [{ hoist: 'types' }], + }, + + { + code: ` +type Foo = 1; +type A = 1; + `, + errors: [ + { + data: { + name: 'A', + shadowedColumn: 6, + shadowedLine: 3, + }, + messageId: 'noShadow', + type: AST_NODE_TYPES.Identifier, + }, + ], + options: [{ hoist: 'all' }], + }, + { + code: ` +interface Foo {} +type A = 1; + `, + errors: [ + { + data: { + name: 'A', + shadowedColumn: 6, + shadowedLine: 3, + }, + messageId: 'noShadow', + type: AST_NODE_TYPES.Identifier, + }, + ], + options: [{ hoist: 'all' }], + }, + { + code: ` +interface Foo {} +interface A {} + `, + errors: [ + { + data: { + name: 'A', + shadowedColumn: 11, + shadowedLine: 3, + }, + messageId: 'noShadow', + type: AST_NODE_TYPES.Identifier, + }, + ], + options: [{ hoist: 'all' }], + }, + { + code: ` +type Foo = 1; +interface A {} + `, + errors: [ + { + data: { + name: 'A', + shadowedColumn: 11, + shadowedLine: 3, + }, + messageId: 'noShadow', + type: AST_NODE_TYPES.Identifier, + }, + ], + options: [{ hoist: 'all' }], + }, + { + code: ` +{ + type A = 1; +} +type A = 1; + `, + errors: [ + { + data: { + name: 'A', + shadowedColumn: 6, + shadowedLine: 5, + }, + messageId: 'noShadow', + type: AST_NODE_TYPES.Identifier, + }, + ], + options: [{ hoist: 'all' }], + }, + { + code: ` +{ + interface A {} +} +type A = 1; + `, + errors: [ + { + data: { + name: 'A', + shadowedColumn: 6, + shadowedLine: 5, + }, + messageId: 'noShadow', + type: AST_NODE_TYPES.Identifier, + }, + ], + options: [{ hoist: 'all' }], + }, + + { + code: ` +type Foo = 1; +type A = 1; + `, + errors: [ + { + data: { + name: 'A', + shadowedColumn: 6, + shadowedLine: 3, + }, + messageId: 'noShadow', + type: AST_NODE_TYPES.Identifier, + }, + ], + options: [{ hoist: 'functions-and-types' }], + }, + { + code: ` +interface Foo {} +type A = 1; + `, + errors: [ + { + data: { + name: 'A', + shadowedColumn: 6, + shadowedLine: 3, + }, + messageId: 'noShadow', + type: AST_NODE_TYPES.Identifier, + }, + ], + options: [{ hoist: 'functions-and-types' }], + }, + { + code: ` +interface Foo {} +interface A {} + `, + errors: [ + { + data: { + name: 'A', + shadowedColumn: 11, + shadowedLine: 3, + }, + messageId: 'noShadow', + type: AST_NODE_TYPES.Identifier, + }, + ], + options: [{ hoist: 'functions-and-types' }], + }, + { + code: ` +type Foo = 1; +interface A {} + `, + errors: [ + { + data: { + name: 'A', + shadowedColumn: 11, + shadowedLine: 3, + }, + messageId: 'noShadow', + type: AST_NODE_TYPES.Identifier, + }, + ], + options: [{ hoist: 'functions-and-types' }], + }, + { + code: ` +{ + type A = 1; +} +type A = 1; + `, + errors: [ + { + data: { + name: 'A', + shadowedColumn: 6, + shadowedLine: 5, + }, + messageId: 'noShadow', + type: AST_NODE_TYPES.Identifier, + }, + ], + options: [{ hoist: 'functions-and-types' }], + }, + { + code: ` +{ + interface A {} +} +type A = 1; + `, + errors: [ + { + data: { + name: 'A', + shadowedColumn: 6, + shadowedLine: 5, + }, + messageId: 'noShadow', + type: AST_NODE_TYPES.Identifier, + }, + ], + options: [{ hoist: 'functions-and-types' }], + }, + { code: ` function foo any>(fn: T, args: any[]) {} @@ -1099,5 +1385,99 @@ const person = { options: [{ ignoreOnInitialization: true }], }, { code: 'const [x = y => y] = [].map(y => y);' }, + + { + code: ` +type Foo = 1; +type A = 1; + `, + options: [{ hoist: 'never' }], + }, + { + code: ` +interface Foo {} +type A = 1; + `, + options: [{ hoist: 'never' }], + }, + { + code: ` +interface Foo {} +interface A {} + `, + options: [{ hoist: 'never' }], + }, + { + code: ` +type Foo = 1; +interface A {} + `, + options: [{ hoist: 'never' }], + }, + { + code: ` +{ + type A = 1; +} +type A = 1; + `, + options: [{ hoist: 'never' }], + }, + { + code: ` +{ + interface Foo {} +} +type A = 1; + `, + options: [{ hoist: 'never' }], + }, + + { + code: ` +type Foo = 1; +type A = 1; + `, + options: [{ hoist: 'functions' }], + }, + { + code: ` +interface Foo {} +type A = 1; + `, + options: [{ hoist: 'functions' }], + }, + { + code: ` +interface Foo {} +interface A {} + `, + options: [{ hoist: 'functions' }], + }, + { + code: ` +type Foo = 1; +interface A {} + `, + options: [{ hoist: 'functions' }], + }, + { + code: ` +{ + type A = 1; +} +type A = 1; + `, + options: [{ hoist: 'functions' }], + }, + { + code: ` +{ + interface Foo {} +} +type A = 1; + `, + options: [{ hoist: 'functions' }], + }, ], }); From 0a63dc275b1daf07845485fdffeb0f91e4b92723 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Thu, 2 Jan 2025 19:22:27 +0200 Subject: [PATCH 3/5] add docs --- .../eslint-plugin/docs/rules/no-shadow.mdx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/eslint-plugin/docs/rules/no-shadow.mdx b/packages/eslint-plugin/docs/rules/no-shadow.mdx index ecd105f6e334..1e5255881d9a 100644 --- a/packages/eslint-plugin/docs/rules/no-shadow.mdx +++ b/packages/eslint-plugin/docs/rules/no-shadow.mdx @@ -17,18 +17,52 @@ It adds support for TypeScript's `this` parameters and global augmentation, and This rule adds the following options: ```ts +type AdditionalHoistOptionEntries = 'types' | 'functions-and-types'; + +type HoistOptionEntries = + | BaseNoShadowHoistOptionEntries + | AdditionalHoistOptionEntries; + interface Options extends BaseNoShadowOptions { + hoist?: HoistOptionEntries; ignoreTypeValueShadow?: boolean; ignoreFunctionTypeParameterNameValueShadow?: boolean; } const defaultOptions: Options = { ...baseNoShadowDefaultOptions, + hoist: 'functions-and-types', ignoreTypeValueShadow: true, ignoreFunctionTypeParameterNameValueShadow: true, }; ``` +### hoist: `types` + +Examples of incorrect code for the `{ "hoist": ["types"] }` option: + +```ts option='{ "hoist": ["types"] }' showPlaygroundButton +type Bar = 1; +type Foo = 1; +``` + +### hoist: `functions-and-types` + +Examples of incorrect code for the `{ "hoist": ["functions-and-types"] }` option: + +```ts option='{ "hoist": ["functions-and-types"] }' showPlaygroundButton +// types +type Bar = 1; +type Foo = 1; + +// functions +if (true) { + let b = 6; +} + +function b() {} +``` + ### `ignoreTypeValueShadow` {/* insert option description */} From b1649719a28890ca8833cd9312ba6b629b57121f Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Thu, 2 Jan 2025 22:29:08 +0200 Subject: [PATCH 4/5] snapshots --- .../eslint-plugin/docs/rules/no-shadow.mdx | 8 ++--- .../no-shadow.shot | 29 ++++++++++++++++++- .../tests/schema-snapshots/no-shadow.shot | 4 ++- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-shadow.mdx b/packages/eslint-plugin/docs/rules/no-shadow.mdx index 1e5255881d9a..41c274b4bf92 100644 --- a/packages/eslint-plugin/docs/rules/no-shadow.mdx +++ b/packages/eslint-plugin/docs/rules/no-shadow.mdx @@ -39,18 +39,18 @@ const defaultOptions: Options = { ### hoist: `types` -Examples of incorrect code for the `{ "hoist": ["types"] }` option: +Examples of incorrect code for the `{ "hoist": "types" }` option: -```ts option='{ "hoist": ["types"] }' showPlaygroundButton +```ts option='{ "hoist": "types" }' showPlaygroundButton type Bar = 1; type Foo = 1; ``` ### hoist: `functions-and-types` -Examples of incorrect code for the `{ "hoist": ["functions-and-types"] }` option: +Examples of incorrect code for the `{ "hoist": "functions-and-types" }` option: -```ts option='{ "hoist": ["functions-and-types"] }' showPlaygroundButton +```ts option='{ "hoist": "functions-and-types" }' showPlaygroundButton // types type Bar = 1; type Foo = 1; diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-shadow.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-shadow.shot index 46c8fe00fb7d..30d6e41c45b1 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-shadow.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-shadow.shot @@ -1,6 +1,33 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Validating rule docs no-shadow.mdx code examples ESLint output 1`] = ` +"Options: { "hoist": "types" } + +type Bar = 1; + ~~~ 'Foo' is already declared in the upper scope on line 2 column 6. +type Foo = 1; +" +`; + +exports[`Validating rule docs no-shadow.mdx code examples ESLint output 2`] = ` +"Options: { "hoist": "functions-and-types" } + +// types +type Bar = 1; + ~~~ 'Foo' is already declared in the upper scope on line 3 column 6. +type Foo = 1; + +// functions +if (true) { + let b = 6; + ~ 'b' is already declared in the upper scope on line 10 column 10. +} + +function b() {} +" +`; + +exports[`Validating rule docs no-shadow.mdx code examples ESLint output 3`] = ` "Options: { "ignoreTypeValueShadow": true } type Foo = number; @@ -15,7 +42,7 @@ function f() { " `; -exports[`Validating rule docs no-shadow.mdx code examples ESLint output 2`] = ` +exports[`Validating rule docs no-shadow.mdx code examples ESLint output 4`] = ` "Options: { "ignoreFunctionTypeParameterNameValueShadow": true } const test = 1; diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-shadow.shot b/packages/eslint-plugin/tests/schema-snapshots/no-shadow.shot index 22a2dba566de..1bf895c38491 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-shadow.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-shadow.shot @@ -21,7 +21,7 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos }, "hoist": { "description": "Whether to report shadowing before outer functions or variables are defined.", - "enum": ["all", "functions", "never"], + "enum": ["all", "functions", "functions-and-types", "never", "types"], "type": "string" }, "ignoreFunctionTypeParameterNameValueShadow": { @@ -53,7 +53,9 @@ type Options = [ /** Whether to report shadowing before outer functions or variables are defined. */ hoist?: | 'functions' + | 'functions-and-types' | 'never' + | 'types' /** Whether to report shadowing before outer functions or variables are defined. */ | 'all'; /** Whether to ignore function parameters named the same as a variable. */ From 7dd6b06b6669f714edce7e65204c2902c0a723f6 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sun, 12 Jan 2025 21:46:21 +0200 Subject: [PATCH 5/5] match original implementation --- packages/eslint-plugin/src/rules/no-shadow.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-shadow.ts b/packages/eslint-plugin/src/rules/no-shadow.ts index ebc3cc88f20c..ed3d01830368 100644 --- a/packages/eslint-plugin/src/rules/no-shadow.ts +++ b/packages/eslint-plugin/src/rules/no-shadow.ts @@ -520,10 +520,14 @@ export default createRule({ const inner = getNameRange(variable); const outer = getNameRange(scopeVar); - if (!inner || !outer || inner[1] >= outer[0] || !outerDef) { + if (!inner || !outer || inner[1] >= outer[0]) { return false; } + if (!outerDef) { + return true; + } + if (options.hoist === 'functions') { return !functionsHoistedNodes.has(outerDef.node.type); }