Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 924913d

Browse files
authored
feat(eslint-plugin): [no-unused-vars] report if var used only in typeof (typescript-eslint#9330)
1 parent afcfbfc commit 924913d

File tree

12 files changed

+239
-36
lines changed

12 files changed

+239
-36
lines changed

packages/eslint-plugin/src/rules/no-shadow.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
import type {
2-
Definition,
3-
ImportBindingDefinition,
4-
} from '@typescript-eslint/scope-manager';
51
import { DefinitionType, ScopeType } from '@typescript-eslint/scope-manager';
62
import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
73
import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils';
84

95
import { createRule } from '../util';
6+
import { isTypeImport } from '../util/isTypeImport';
107

118
type MessageIds = 'noShadow' | 'noShadowGlobal';
129
type Options = [
@@ -102,17 +99,6 @@ export default createRule<Options, MessageIds>({
10299
);
103100
}
104101

105-
function isTypeImport(
106-
definition?: Definition,
107-
): definition is ImportBindingDefinition {
108-
return (
109-
definition?.type === DefinitionType.ImportBinding &&
110-
(definition.parent.importKind === 'type' ||
111-
(definition.node.type === AST_NODE_TYPES.ImportSpecifier &&
112-
definition.node.importKind === 'type'))
113-
);
114-
}
115-
116102
function isTypeValueShadow(
117103
variable: TSESLint.Scope.Variable,
118104
shadowed: TSESLint.Scope.Variable,

packages/eslint-plugin/src/rules/no-unused-vars.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import {
1515
nullThrows,
1616
NullThrowsReasons,
1717
} from '../util';
18+
import { referenceContainsTypeQuery } from '../util/referenceContainsTypeQuery';
1819

19-
export type MessageIds = 'unusedVar' | 'usedIgnoredVar';
20+
export type MessageIds = 'unusedVar' | 'usedIgnoredVar' | 'usedOnlyAsType';
2021
export type Options = [
2122
| 'all'
2223
| 'local'
@@ -115,6 +116,8 @@ export default createRule<Options, MessageIds>({
115116
unusedVar: "'{{varName}}' is {{action}} but never used{{additional}}.",
116117
usedIgnoredVar:
117118
"'{{varName}}' is marked as ignored but is used{{additional}}.",
119+
usedOnlyAsType:
120+
"'{{varName}}' is {{action}} but only used as a type{{additional}}.",
118121
},
119122
},
120123
defaultOptions: [{}],
@@ -581,6 +584,12 @@ export default createRule<Options, MessageIds>({
581584
? writeReferences[writeReferences.length - 1].identifier
582585
: unusedVar.identifiers[0];
583586

587+
const usedOnlyAsType = unusedVar.references.some(ref =>
588+
referenceContainsTypeQuery(ref.identifier),
589+
);
590+
591+
const messageId = usedOnlyAsType ? 'usedOnlyAsType' : 'unusedVar';
592+
584593
const { start } = id.loc;
585594
const idLength = id.name.length;
586595

@@ -594,7 +603,7 @@ export default createRule<Options, MessageIds>({
594603

595604
context.report({
596605
loc,
597-
messageId: 'unusedVar',
606+
messageId,
598607
data: unusedVar.references.some(ref => ref.isWrite())
599608
? getAssignedMessageData(unusedVar)
600609
: getDefinedMessageData(unusedVar),

packages/eslint-plugin/src/rules/no-use-before-define.ts

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { TSESTree } from '@typescript-eslint/utils';
33
import { AST_NODE_TYPES, TSESLint } from '@typescript-eslint/utils';
44

55
import { createRule } from '../util';
6+
import { referenceContainsTypeQuery } from '../util/referenceContainsTypeQuery';
67

78
const SENTINEL_TYPE =
89
/^(?:(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression|CatchClause|ImportDeclaration|ExportNamedDeclaration)$/;
@@ -106,24 +107,6 @@ function isNamedExports(reference: TSESLint.Scope.Reference): boolean {
106107
);
107108
}
108109

109-
/**
110-
* Recursively checks whether or not a given reference has a type query declaration among it's parents
111-
*/
112-
function referenceContainsTypeQuery(node: TSESTree.Node): boolean {
113-
switch (node.type) {
114-
case AST_NODE_TYPES.TSTypeQuery:
115-
return true;
116-
117-
case AST_NODE_TYPES.TSQualifiedName:
118-
case AST_NODE_TYPES.Identifier:
119-
return referenceContainsTypeQuery(node.parent);
120-
121-
default:
122-
// if we find a different node, there's no chance that we're in a TSTypeQuery
123-
return false;
124-
}
125-
}
126-
127110
/**
128111
* Checks whether or not a given reference is a type reference.
129112
*/

packages/eslint-plugin/src/util/collectUnusedVariables.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import {
1515
TSESLint,
1616
} from '@typescript-eslint/utils';
1717

18+
import { isTypeImport } from './isTypeImport';
19+
import { referenceContainsTypeQuery } from './referenceContainsTypeQuery';
20+
1821
interface VariableAnalysis {
1922
readonly unusedVariables: ReadonlySet<ScopeVariable>;
2023
readonly usedVariables: ReadonlySet<ScopeVariable>;
@@ -784,6 +787,8 @@ function isUsedVariable(variable: ScopeVariable): boolean {
784787
const enumDeclNodes = getEnumDeclarations(variable);
785788
const isEnumDecl = enumDeclNodes.size > 0;
786789

790+
const isImportedAsType = variable.defs.every(isTypeImport);
791+
787792
let rhsNode: TSESTree.Node | null = null;
788793

789794
return variable.references.some(ref => {
@@ -794,6 +799,7 @@ function isUsedVariable(variable: ScopeVariable): boolean {
794799
return (
795800
ref.isRead() &&
796801
!forItself &&
802+
!(!isImportedAsType && referenceContainsTypeQuery(ref.identifier)) &&
797803
!(isFunctionDefinition && isSelfReference(ref, functionNodes)) &&
798804
!(isTypeDecl && isInsideOneOf(ref, typeDeclNodes)) &&
799805
!(isModuleDecl && isSelfReference(ref, moduleDeclNodes)) &&
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type {
2+
Definition,
3+
ImportBindingDefinition,
4+
} from '@typescript-eslint/scope-manager';
5+
import { DefinitionType } from '@typescript-eslint/scope-manager';
6+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
7+
8+
/**
9+
* Determine whether a variable definition is a type import. e.g.:
10+
*
11+
* ```ts
12+
* import type { Foo } from 'foo';
13+
* import { type Bar } from 'bar';
14+
* ```
15+
*
16+
* @param definition - The variable definition to check.
17+
*/
18+
export function isTypeImport(
19+
definition?: Definition,
20+
): definition is ImportBindingDefinition {
21+
return (
22+
definition?.type === DefinitionType.ImportBinding &&
23+
(definition.parent.importKind === 'type' ||
24+
(definition.node.type === AST_NODE_TYPES.ImportSpecifier &&
25+
definition.node.importKind === 'type'))
26+
);
27+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { TSESTree } from '@typescript-eslint/utils';
2+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
3+
4+
/**
5+
* Recursively checks whether a given reference has a type query declaration among its parents
6+
*/
7+
export function referenceContainsTypeQuery(node: TSESTree.Node): boolean {
8+
switch (node.type) {
9+
case AST_NODE_TYPES.TSTypeQuery:
10+
return true;
11+
12+
case AST_NODE_TYPES.TSQualifiedName:
13+
case AST_NODE_TYPES.Identifier:
14+
return referenceContainsTypeQuery(node.parent);
15+
16+
default:
17+
// if we find a different node, there's no chance that we're in a TSTypeQuery
18+
return false;
19+
}
20+
}

packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts

Lines changed: 168 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -774,7 +774,7 @@ export function foo() {
774774
`,
775775
// https://github.com/typescript-eslint/typescript-eslint/issues/5152
776776
`
777-
function foo<T>(value: T): T {
777+
export function foo<T>(value: T): T {
778778
return { value };
779779
}
780780
export type Foo<T> = typeof foo<T>;
@@ -1150,6 +1150,18 @@ export const x: _Foo = 1;
11501150
`,
11511151
options: [{ varsIgnorePattern: '^_', reportUsedIgnorePattern: false }],
11521152
},
1153+
`
1154+
export const foo: number = 1;
1155+
1156+
export type Foo = typeof foo;
1157+
`,
1158+
`
1159+
import { foo } from 'foo';
1160+
1161+
export type Foo = typeof foo;
1162+
1163+
export const bar = (): Foo => foo;
1164+
`,
11531165
],
11541166

11551167
invalid: [
@@ -2093,5 +2105,160 @@ export const x = _Foo;
20932105
},
20942106
],
20952107
},
2108+
{
2109+
code: `
2110+
const foo: number = 1;
2111+
2112+
export type Foo = typeof foo;
2113+
`,
2114+
errors: [
2115+
{
2116+
messageId: 'usedOnlyAsType',
2117+
data: {
2118+
varName: 'foo',
2119+
action: 'assigned a value',
2120+
additional: '',
2121+
},
2122+
line: 2,
2123+
column: 15,
2124+
endLine: 2,
2125+
endColumn: 18,
2126+
},
2127+
],
2128+
},
2129+
{
2130+
code: `
2131+
declare const foo: number;
2132+
2133+
export type Foo = typeof foo;
2134+
`,
2135+
errors: [
2136+
{
2137+
messageId: 'usedOnlyAsType',
2138+
data: {
2139+
varName: 'foo',
2140+
action: 'defined',
2141+
additional: '',
2142+
},
2143+
line: 2,
2144+
column: 23,
2145+
endLine: 2,
2146+
endColumn: 26,
2147+
},
2148+
],
2149+
},
2150+
{
2151+
code: `
2152+
const foo: number = 1;
2153+
2154+
export type Foo = typeof foo | string;
2155+
`,
2156+
errors: [
2157+
{
2158+
messageId: 'usedOnlyAsType',
2159+
data: {
2160+
varName: 'foo',
2161+
action: 'assigned a value',
2162+
additional: '',
2163+
},
2164+
line: 2,
2165+
column: 15,
2166+
endLine: 2,
2167+
endColumn: 18,
2168+
},
2169+
],
2170+
},
2171+
{
2172+
code: `
2173+
const foo: number = 1;
2174+
2175+
export type Foo = (typeof foo | string) & { __brand: 'foo' };
2176+
`,
2177+
errors: [
2178+
{
2179+
messageId: 'usedOnlyAsType',
2180+
data: {
2181+
varName: 'foo',
2182+
action: 'assigned a value',
2183+
additional: '',
2184+
},
2185+
line: 2,
2186+
column: 15,
2187+
endLine: 2,
2188+
endColumn: 18,
2189+
},
2190+
],
2191+
},
2192+
{
2193+
code: `
2194+
const foo = {
2195+
bar: {
2196+
baz: 123,
2197+
},
2198+
};
2199+
2200+
export type Bar = typeof foo.bar;
2201+
`,
2202+
errors: [
2203+
{
2204+
messageId: 'usedOnlyAsType',
2205+
data: {
2206+
varName: 'foo',
2207+
action: 'assigned a value',
2208+
additional: '',
2209+
},
2210+
line: 2,
2211+
column: 15,
2212+
endLine: 2,
2213+
endColumn: 18,
2214+
},
2215+
],
2216+
},
2217+
{
2218+
code: `
2219+
const foo = {
2220+
bar: {
2221+
baz: 123,
2222+
},
2223+
};
2224+
2225+
export type Bar = (typeof foo)['bar'];
2226+
`,
2227+
errors: [
2228+
{
2229+
messageId: 'usedOnlyAsType',
2230+
data: {
2231+
varName: 'foo',
2232+
action: 'assigned a value',
2233+
additional: '',
2234+
},
2235+
line: 2,
2236+
column: 15,
2237+
endLine: 2,
2238+
endColumn: 18,
2239+
},
2240+
],
2241+
},
2242+
{
2243+
code: `
2244+
import { foo } from 'foo';
2245+
2246+
export type Bar = typeof foo;
2247+
`,
2248+
errors: [
2249+
{
2250+
messageId: 'usedOnlyAsType',
2251+
data: {
2252+
varName: 'foo',
2253+
action: 'defined',
2254+
additional: '',
2255+
},
2256+
line: 2,
2257+
column: 18,
2258+
endLine: 2,
2259+
endColumn: 21,
2260+
},
2261+
],
2262+
},
20962263
],
20972264
});

packages/utils/src/ts-eslint/Linter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export type MinimalRuleModule<
2020
> = Partial<Omit<RuleModule<MessageIds, Options>, 'create'>> &
2121
Pick<RuleModule<MessageIds, Options>, 'create'>;
2222

23+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2324
declare class LinterBase {
2425
/**
2526
* Initialize the Linter.

packages/utils/src/ts-eslint/RuleTester.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ interface RuleTesterConfig extends ClassicConfig.Config {
147147
readonly parserOptions?: Readonly<ParserOptions>;
148148
}
149149

150+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
150151
declare class RuleTesterBase {
151152
/**
152153
* Creates a new instance of RuleTester.

packages/utils/src/ts-eslint/SourceCode.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ declare class TokenStore {
209209
): SourceCode.ReturnTypeFromOptions<T>[];
210210
}
211211

212+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
212213
declare class SourceCodeBase extends TokenStore {
213214
/**
214215
* Represents parsed source code.

0 commit comments

Comments
 (0)