Skip to content

Commit

Permalink
feat(eslint-plugin): [no-shadow] add option `ignoreFunctionTypeParame…
Browse files Browse the repository at this point in the history
…terNameValueShadow` (#2470)
  • Loading branch information
bradzacher committed Sep 2, 2020
1 parent ffdfade commit bfe255f
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 17 deletions.
3 changes: 3 additions & 0 deletions package.json
Expand Up @@ -57,6 +57,9 @@
"footer-max-length": [
0
],
"footer-max-line-length": [
0
],
"header-max-length": [
0
]
Expand Down
39 changes: 38 additions & 1 deletion packages/eslint-plugin/docs/rules/no-shadow.md
Expand Up @@ -23,17 +23,21 @@ This rule adds the following options:
```ts
interface Options extends BaseNoShadowOptions {
ignoreTypeValueShadow?: boolean;
ignoreFunctionTypeParameterNameValueShadow?: boolean;
}

const defaultOptions: Options = {
...baseNoShadowDefaultOptions,
ignoreTypeValueShadow: true,
ignoreFunctionTypeParameterNameValueShadow: true,
};
```

### `ignoreTypeValueShadow`

When set to `true`, the rule will ignore when you name a type and a variable with the same name.
When set to `true`, the rule will ignore the case when you name a type the same as a variable.

TypeScript allows types and variables to shadow one-another. This is generally safe because you cannot use variables in type locations without a `typeof` operator, so there's little risk of confusion.

Examples of **correct** code with `{ ignoreTypeValueShadow: true }`:

Expand All @@ -47,4 +51,37 @@ interface Bar {
const Bar = 'test';
```

### `ignoreFunctionTypeParameterNameValueShadow`

When set to `true`, the rule will ignore the case when you name a function type argument the same as a variable.

Each of a function type's arguments creates a value variable within the scope of the function type. This is done so that you can reference the type later using the `typeof` operator:

```ts
type Func = (test: string) => typeof test;

declare const fn: Func;
const result = fn('str'); // typeof result === string
```

This means that function type arguments shadow value variable names in parent scopes:

```ts
let test = 1;
type TestType = typeof test; // === number
type Func = (test: string) => typeof test; // this "test" references the argument, not the variable

declare const fn: Func;
const result = fn('str'); // typeof result === string
```

If you do not use the `typeof` operator in a function type return type position, you can safely turn this option on.

Examples of **correct** code with `{ ignoreFunctionTypeParameterNameValueShadow: true }`:

```ts
const test = 1;
type Func = (test: string) => typeof test;
```

<sup>Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/no-shadow.md)</sup>
50 changes: 41 additions & 9 deletions packages/eslint-plugin/src/rules/no-shadow.ts
Expand Up @@ -12,6 +12,7 @@ type Options = [
builtinGlobals?: boolean;
hoist?: 'all' | 'functions' | 'never';
ignoreTypeValueShadow?: boolean;
ignoreFunctionTypeParameterNameValueShadow?: boolean;
},
];

Expand Down Expand Up @@ -45,6 +46,9 @@ export default util.createRule<Options, MessageIds>({
ignoreTypeValueShadow: {
type: 'boolean',
},
ignoreFunctionTypeParameterNameValueShadow: {
type: 'boolean',
},
},
additionalProperties: false,
},
Expand All @@ -59,6 +63,7 @@ export default util.createRule<Options, MessageIds>({
builtinGlobals: false,
hoist: 'functions',
ignoreTypeValueShadow: true,
ignoreFunctionTypeParameterNameValueShadow: true,
},
],
create(context, [options]) {
Expand All @@ -77,15 +82,37 @@ export default util.createRule<Options, MessageIds>({
return false;
}

if (
!('isValueVariable' in shadowed) ||
!('isValueVariable' in variable)
) {
// one of them is an eslint global variable
if (!('isValueVariable' in variable)) {
// this shouldn't happen...
return false;
}

const isShadowedValue =
'isValueVariable' in shadowed ? shadowed.isValueVariable : true;
return variable.isValueVariable !== isShadowedValue;
}

function isFunctionTypeParameterNameValueShadow(
variable: TSESLint.Scope.Variable,
shadowed: TSESLint.Scope.Variable,
): boolean {
if (options.ignoreFunctionTypeParameterNameValueShadow !== true) {
return false;
}

if (!('isValueVariable' in variable)) {
// this shouldn't happen...
return false;
}

const isShadowedValue =
'isValueVariable' in shadowed ? shadowed.isValueVariable : true;
if (!isShadowedValue) {
return false;
}

return variable.isValueVariable !== shadowed.isValueVariable;
const id = variable.identifiers[0];
return util.isFunctionType(id.parent);
}

/**
Expand Down Expand Up @@ -117,12 +144,12 @@ export default util.createRule<Options, MessageIds>({
}

/**
* Checks if a variable of the class name in the class scope of ClassDeclaration.
* Checks if a variable of the class name in the class scope of TSEnumDeclaration.
*
* ClassDeclaration creates two variables of its name into its outer scope and its class scope.
* TSEnumDeclaration creates two variables of its name into its outer scope and its class scope.
* So we should ignore the variable in the class scope.
* @param variable The variable to check.
* @returns Whether or not the variable of the class name in the class scope of ClassDeclaration.
* @returns Whether or not the variable of the class name in the class scope of TSEnumDeclaration.
*/
function isDuplicatedEnumNameVariable(
variable: TSESLint.Scope.Variable,
Expand Down Expand Up @@ -273,6 +300,11 @@ export default util.createRule<Options, MessageIds>({
continue;
}

// ignore function type parameter name shadowing if configured
if (isFunctionTypeParameterNameValueShadow(variable, shadowed)) {
continue;
}

const isESLintGlobal = 'writeable' in shadowed;
if (
(shadowed.identifiers.length > 0 ||
Expand Down
122 changes: 115 additions & 7 deletions packages/eslint-plugin/tests/rules/no-shadow.test.ts
Expand Up @@ -56,6 +56,58 @@ const x = 1;
type x = string;
}
`,
{
code: `
type Foo = 1;
`,
options: [{ ignoreTypeValueShadow: true }],
globals: {
Foo: 'writable',
},
},
{
code: `
type Foo = 1;
`,
options: [
{
ignoreTypeValueShadow: false,
builtinGlobals: false,
},
],
globals: {
Foo: 'writable',
},
},
// https://github.com/typescript-eslint/typescript-eslint/issues/2360
`
enum Direction {
left = 'left',
right = 'right',
}
`,
// https://github.com/typescript-eslint/typescript-eslint/issues/2447
{
code: `
const test = 1;
type Fn = (test: string) => typeof test;
`,
options: [{ ignoreFunctionTypeParameterNameValueShadow: true }],
},
{
code: `
type Fn = (Foo: string) => typeof Foo;
`,
options: [
{
ignoreFunctionTypeParameterNameValueShadow: true,
builtinGlobals: false,
},
],
globals: {
Foo: 'writable',
},
},
],
invalid: [
{
Expand Down Expand Up @@ -109,6 +161,69 @@ const x = 1;
},
],
},
{
code: `
type Foo = 1;
`,
options: [
{
ignoreTypeValueShadow: false,
builtinGlobals: true,
},
],
globals: {
Foo: 'writable',
},
errors: [
{
messageId: 'noShadow',
data: {
name: 'Foo',
},
line: 2,
},
],
},
// https://github.com/typescript-eslint/typescript-eslint/issues/2447
{
code: `
const test = 1;
type Fn = (test: string) => typeof test;
`,
options: [{ ignoreFunctionTypeParameterNameValueShadow: false }],
errors: [
{
messageId: 'noShadow',
data: {
name: 'test',
},
line: 3,
},
],
},
{
code: `
type Fn = (Foo: string) => typeof Foo;
`,
options: [
{
ignoreFunctionTypeParameterNameValueShadow: false,
builtinGlobals: true,
},
],
globals: {
Foo: 'writable',
},
errors: [
{
messageId: 'noShadow',
data: {
name: 'Foo',
},
line: 2,
},
],
},
],
});

Expand Down Expand Up @@ -431,13 +546,6 @@ function foo(cb) {
`,
options: [{ allow: ['cb'] }],
},
// https://github.com/typescript-eslint/typescript-eslint/issues/2360
`
enum Direction {
left = 'left',
right = 'right',
}
`,
],
invalid: [
{
Expand Down

0 comments on commit bfe255f

Please sign in to comment.