Skip to content

Commit 3d07a99

Browse files
authored
fix(eslint-plugin): [no-unused-vars] correct detection of unused vars in a declared module with export = (typescript-eslint#2505)
If a `declare module` has an `export =` in its body, then TS will only export that. If it doesn't have an `export =`, then all things are ambiently exported. This adds handling to correctly detect this case.
1 parent 2ada5af commit 3d07a99

File tree

7 files changed

+105
-16
lines changed

7 files changed

+105
-16
lines changed

packages/eslint-plugin/src/rules/adjacent-overload-signatures.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ type RuleNode =
1010
| TSESTree.TSModuleBlock
1111
| TSESTree.TSTypeLiteral
1212
| TSESTree.TSInterfaceBody;
13-
type Member = TSESTree.ClassElement | TSESTree.Statement | TSESTree.TypeElement;
13+
type Member =
14+
| TSESTree.ClassElement
15+
| TSESTree.ProgramStatement
16+
| TSESTree.TypeElement;
1417

1518
export default util.createRule({
1619
name: 'adjacent-overload-signatures',

packages/eslint-plugin/src/rules/indent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ export default util.createRule<Options, MessageIds>({
410410
// transform it to a BlockStatement
411411
return rules['BlockStatement, ClassBody']({
412412
type: AST_NODE_TYPES.BlockStatement,
413-
body: node.body,
413+
body: node.body as any,
414414

415415
// location data
416416
parent: node.parent,

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

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export default util.createRule<Options, MessageIds>({
2929
create(context) {
3030
const rules = baseRule.create(context);
3131
const filename = context.getFilename();
32+
const MODULE_DECL_CACHE = new Map<TSESTree.TSModuleDeclaration, boolean>();
3233

3334
/**
3435
* Gets a list of TS module definitions for a specified variable.
@@ -209,7 +210,7 @@ export default util.createRule<Options, MessageIds>({
209210
},
210211

211212
// declaration file handling
212-
[declarationSelector(AST_NODE_TYPES.Program, true)](
213+
[ambientDeclarationSelector(AST_NODE_TYPES.Program, true)](
213214
node: DeclarationSelectorNode,
214215
): void {
215216
if (!util.isDefinitionFile(filename)) {
@@ -219,14 +220,44 @@ export default util.createRule<Options, MessageIds>({
219220
},
220221

221222
// declared namespace handling
222-
[declarationSelector(
223+
[ambientDeclarationSelector(
223224
'TSModuleDeclaration[declare = true] > TSModuleBlock',
224225
false,
225226
)](node: DeclarationSelectorNode): void {
227+
const moduleDecl = util.nullThrows(
228+
node.parent?.parent,
229+
util.NullThrowsReasons.MissingParent,
230+
) as TSESTree.TSModuleDeclaration;
231+
232+
// declared modules with an `export =` statement will only export that one thing
233+
// all other statements are not automatically exported in this case
234+
if (checkModuleDeclForExportEquals(moduleDecl)) {
235+
return;
236+
}
237+
226238
markDeclarationChildAsUsed(node);
227239
},
228240
};
229241

242+
function checkModuleDeclForExportEquals(
243+
node: TSESTree.TSModuleDeclaration,
244+
): boolean {
245+
const cached = MODULE_DECL_CACHE.get(node);
246+
if (cached != null) {
247+
return cached;
248+
}
249+
250+
for (const statement of node.body?.body ?? []) {
251+
if (statement.type === AST_NODE_TYPES.TSExportAssignment) {
252+
MODULE_DECL_CACHE.set(node, true);
253+
return true;
254+
}
255+
}
256+
257+
MODULE_DECL_CACHE.set(node, false);
258+
return false;
259+
}
260+
230261
type DeclarationSelectorNode =
231262
| TSESTree.TSInterfaceDeclaration
232263
| TSESTree.TSTypeAliasDeclaration
@@ -236,7 +267,7 @@ export default util.createRule<Options, MessageIds>({
236267
| TSESTree.TSEnumDeclaration
237268
| TSESTree.TSModuleDeclaration
238269
| TSESTree.VariableDeclaration;
239-
function declarationSelector(
270+
function ambientDeclarationSelector(
240271
parent: string,
241272
childDeclare: boolean,
242273
): string {

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,18 @@ export type Test<U> = U extends (arg: {
848848
jsxFragmentName: 'Fragment',
849849
},
850850
},
851+
`
852+
declare module 'foo' {
853+
type Test = 1;
854+
}
855+
`,
856+
`
857+
declare module 'foo' {
858+
type Test = 1;
859+
const x: Test = 1;
860+
export = x;
861+
}
862+
`,
851863
],
852864

853865
invalid: [
@@ -1424,5 +1436,25 @@ export const ComponentFoo = () => {
14241436
},
14251437
],
14261438
},
1439+
{
1440+
code: `
1441+
declare module 'foo' {
1442+
type Test = any;
1443+
const x = 1;
1444+
export = x;
1445+
}
1446+
`,
1447+
errors: [
1448+
{
1449+
messageId: 'unusedVar',
1450+
line: 3,
1451+
data: {
1452+
varName: 'Test',
1453+
action: 'defined',
1454+
additional: '',
1455+
},
1456+
},
1457+
],
1458+
},
14271459
],
14281460
});

packages/eslint-plugin/typings/eslint-rules.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ declare module 'eslint/lib/rules/camelcase' {
4848
declare module 'eslint/lib/rules/indent' {
4949
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
5050

51-
type Listener = (node: TSESTree.Node) => void;
5251
type ElementList = number | 'first' | 'off';
5352
const rule: TSESLint.RuleModule<
5453
'wrongIndentation',

packages/scope-manager/src/scope/ScopeBase.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
ReferenceTypeFlag,
1616
} from '../referencer/Reference';
1717
import { Variable } from '../variable';
18+
import { TSModuleScope } from './TSModuleScope';
1819

1920
/**
2021
* Test if scope is strict
@@ -124,6 +125,14 @@ function registerScope(scopeManager: ScopeManager, scope: Scope): void {
124125

125126
const generator = createIdGenerator();
126127

128+
type VariableScope = GlobalScope | FunctionScope | ModuleScope | TSModuleScope;
129+
const VARIABLE_SCOPE_TYPES = new Set([
130+
ScopeType.global,
131+
ScopeType.function,
132+
ScopeType.module,
133+
ScopeType.tsModule,
134+
]);
135+
127136
type AnyScope = ScopeBase<ScopeType, TSESTree.Node, Scope | null>;
128137
abstract class ScopeBase<
129138
TType extends ScopeType,
@@ -209,11 +218,11 @@ abstract class ScopeBase<
209218
*/
210219
public readonly variables: Variable[] = [];
211220
/**
212-
* For 'global', 'function', and 'module' scopes, this is a self-reference.
221+
* For scopes that can contain variable declarations, this is a self-reference.
213222
* For other scope types this is the *variableScope* value of the parent scope.
214223
* @public
215224
*/
216-
public readonly variableScope: GlobalScope | FunctionScope | ModuleScope;
225+
public readonly variableScope: VariableScope;
217226

218227
constructor(
219228
scopeManager: ScopeManager,
@@ -228,12 +237,9 @@ abstract class ScopeBase<
228237
this.#dynamic =
229238
this.type === ScopeType.global || this.type === ScopeType.with;
230239
this.block = block;
231-
this.variableScope =
232-
this.type === 'global' ||
233-
this.type === 'function' ||
234-
this.type === 'module'
235-
? (this as AnyScope['variableScope'])
236-
: upperScopeAsScopeBase.variableScope;
240+
this.variableScope = this.isVariableScope()
241+
? this
242+
: upperScopeAsScopeBase.variableScope;
237243
this.upper = upperScope;
238244

239245
/**
@@ -252,6 +258,10 @@ abstract class ScopeBase<
252258
registerScope(scopeManager, this as Scope);
253259
}
254260

261+
private isVariableScope(): this is VariableScope {
262+
return VARIABLE_SCOPE_TYPES.has(this.type);
263+
}
264+
255265
public shouldStaticallyClose(): boolean {
256266
return !this.#dynamic;
257267
}

packages/types/src/ts-estree.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,20 @@ export type PrimaryExpression =
457457
| TemplateLiteral
458458
| ThisExpression
459459
| TSNullKeyword;
460+
export type ProgramStatement =
461+
| ClassDeclaration
462+
| ExportAllDeclaration
463+
| ExportDefaultDeclaration
464+
| ExportNamedDeclaration
465+
| ImportDeclaration
466+
| Statement
467+
| TSDeclareFunction
468+
| TSEnumDeclaration
469+
| TSExportAssignment
470+
| TSImportEqualsDeclaration
471+
| TSInterfaceDeclaration
472+
| TSNamespaceExportDeclaration
473+
| TSTypeAliasDeclaration;
460474
export type Property = PropertyComputedName | PropertyNonComputedName;
461475
export type PropertyName = PropertyNameComputed | PropertyNameNonComputed;
462476
export type PropertyNameComputed = Expression;
@@ -1146,7 +1160,7 @@ export interface ObjectPattern extends BaseNode {
11461160

11471161
export interface Program extends BaseNode {
11481162
type: AST_NODE_TYPES.Program;
1149-
body: Statement[];
1163+
body: ProgramStatement[];
11501164
sourceType: 'module' | 'script';
11511165
comments?: Comment[];
11521166
tokens?: Token[];
@@ -1473,7 +1487,7 @@ export interface TSMethodSignatureNonComputedName
14731487

14741488
export interface TSModuleBlock extends BaseNode {
14751489
type: AST_NODE_TYPES.TSModuleBlock;
1476-
body: Statement[];
1490+
body: ProgramStatement[];
14771491
}
14781492

14791493
export interface TSModuleDeclaration extends BaseNode {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy