Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): [no-shadow] add option ignoreFunctionTypeParameterNameValueShadow #2470

Merged
merged 1 commit into from Sep 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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