diff --git a/packages/eslint-plugin/docs/rules/no-shadow.mdx b/packages/eslint-plugin/docs/rules/no-shadow.mdx index ecd105f6e334..41c274b4bf92 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 */} diff --git a/packages/eslint-plugin/src/rules/no-shadow.ts b/packages/eslint-plugin/src/rules/no-shadow.ts index 0e1f87149608..ed3d01830368 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,30 @@ 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]) { + return false; + } + + if (!outerDef) { + return true; + } + + if (options.hoist === 'functions') { + return !functionsHoistedNodes.has(outerDef.node.type); + } + + if (options.hoist === 'types') { + return !typesHoistedNodes.has(outerDef.node.type); + } + + 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/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/rules/no-shadow/no-shadow.test.ts b/packages/eslint-plugin/tests/rules/no-shadow/no-shadow.test.ts index 6dd82ef35747..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 @@ -547,6 +547,364 @@ let y; languageOptions: { parserOptions: { ecmaVersion: 6 } }, options: [{ hoist: 'all' }], }, + { + 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; + `, + 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: ` +{ + 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[]) {} @@ -1027,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' }], + }, ], }); 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. */