Skip to content

Commit bfd0e7a

Browse files
feat: support TypeScript syntax in no-use-before-define (#19566)
1 parent 68c61c0 commit bfd0e7a

File tree

4 files changed

+1839
-1
lines changed

4 files changed

+1839
-1
lines changed

docs/src/rules/no-use-before-define.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,18 @@ export { foo };
157157
These references are safe even if the variables are declared later in the code.
158158
Default is `false`.
159159

160+
This rule additionally supports TypeScript type syntax. The following options enable checking for the references to `type`, `interface` and `enum` declarations:
161+
162+
* `enums` (`boolean`) -
163+
If it is `true`, the rule warns every reference to an `enum` before it is defined.
164+
Defult is `true`.
165+
* `typedefs` (`boolean`) -
166+
If it is `true`, this rule warns every reference to a type `alias` or `interface` before its declaration. If `false`, the rule allows using type `alias`es and `interface`s before they are defined.
167+
Default is `true`.
168+
* `ignoreTypeReferences` (`boolean`) -
169+
If it is `true`, rule will ignore all type references, such as in type annotations and assertions.
170+
Default is `true`.
171+
160172
This rule accepts `"nofunc"` string as an option.
161173
`"nofunc"` is the same as `{ "functions": false, "classes": true, "variables": true, "allowNamedExports": false }`.
162174

@@ -350,3 +362,129 @@ const d = 1;
350362
```
351363

352364
:::
365+
366+
### enums (TypeScript only)
367+
368+
Examples of **incorrect** code for the `{ "enums": true }` option:
369+
370+
::: incorrect
371+
372+
```ts
373+
/*eslint no-use-before-define: ["error", { "enums": true }]*/
374+
375+
const x = Foo.FOO;
376+
377+
enum Foo {
378+
FOO,
379+
}
380+
```
381+
382+
:::
383+
384+
Examples of **correct** code for the `{ "enums": true }` option:
385+
386+
::: correct
387+
388+
```ts
389+
/*eslint no-use-before-define: ["error", { "enums": true }]*/
390+
391+
enum Foo {
392+
FOO,
393+
}
394+
395+
const x = Foo.FOO;
396+
```
397+
398+
:::
399+
400+
### typedefs (TypeScript only)
401+
402+
Examples of **incorrect** code for the `{ "enums": true }` with `{ "ignoreTypeReferences": false }` option:
403+
404+
::: incorrect
405+
406+
```ts
407+
/*eslint no-use-before-define: ["error", { "typedefs": true, "ignoreTypeReferences": false }]*/
408+
409+
let myVar: StringOrNumber;
410+
411+
type StringOrNumber = string | number;
412+
413+
const x: Foo = {};
414+
415+
interface Foo {}
416+
```
417+
418+
:::
419+
420+
Examples of **correct** code for the `{ "typedefs": true }` with `{ "ignoreTypeReferences": false }` option:
421+
422+
::: correct
423+
424+
```ts
425+
/*eslint no-use-before-define: ["error", { "typedefs": true, "ignoreTypeReferences": false }]*/
426+
427+
type StringOrNumber = string | number;
428+
429+
let myVar: StringOrNumber;
430+
431+
interface Foo {}
432+
433+
const x: Foo = {};
434+
```
435+
436+
:::
437+
438+
### ignoreTypeReferences (TypeScript only)
439+
440+
Examples of **incorrect** code for the `{ "ignoreTypeReferences": false }` option:
441+
442+
::: incorrect
443+
444+
```ts
445+
/*eslint no-use-before-define: ["error", { "ignoreTypeReferences": false }]*/
446+
447+
let var1: StringOrNumber;
448+
449+
type StringOrNumber = string | number;
450+
451+
let var2: Enum;
452+
453+
enum Enum {}
454+
```
455+
456+
:::
457+
458+
Examples of **correct** code for the `{ "ignoreTypeReferences": false }` option:
459+
460+
::: correct
461+
462+
```ts
463+
/*eslint no-use-before-define: ["error", { "ignoreTypeReferences": false }]*/
464+
465+
type StringOrNumber = string | number;
466+
467+
let myVar: StringOrNumber;
468+
469+
enum Enum {}
470+
471+
let var2: Enum;
472+
```
473+
474+
Examples of **correct** code for the `{ "ignoreTypeReferences": false }` with `{ "typedefs": false }` option:
475+
476+
::: correct
477+
478+
```ts
479+
/*eslint no-use-before-define: ["error", { "ignoreTypeReferences": false, "typedefs": false, }]*/
480+
481+
let myVar: StringOrNumber;
482+
483+
type StringOrNumber = string | number;
484+
485+
const x: Foo = {};
486+
487+
interface Foo {}
488+
```
489+
490+
:::

lib/rules/no-use-before-define.js

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ function parseOptions(options) {
3030
classes: true,
3131
variables: true,
3232
allowNamedExports: false,
33+
enums: true,
34+
typedefs: true,
35+
ignoreTypeReferences: true,
3336
};
3437
}
3538

@@ -208,6 +211,57 @@ function isEvaluatedDuringInitialization(reference) {
208211
return false;
209212
}
210213

214+
/**
215+
* check whether the reference contains a type query.
216+
* @param {ASTNode} node Identifier node to check.
217+
* @returns {boolean} true if reference contains type query.
218+
*/
219+
function referenceContainsTypeQuery(node) {
220+
switch (node.type) {
221+
case "TSTypeQuery":
222+
return true;
223+
224+
case "TSQualifiedName":
225+
case "Identifier":
226+
return referenceContainsTypeQuery(node.parent);
227+
228+
default:
229+
// if we find a different node, there's no chance that we're in a TSTypeQuery
230+
return false;
231+
}
232+
}
233+
234+
/**
235+
* Decorators are transpiled such that the decorator is placed after the class declaration
236+
* So it is considered safe
237+
* @param {Variable} variable The variable to check.
238+
* @param {Reference} reference The reference to check.
239+
* @returns {boolean} `true` if the reference is in a class decorator.
240+
*/
241+
function isClassRefInClassDecorator(variable, reference) {
242+
if (variable.defs[0].type !== "ClassName") {
243+
return false;
244+
}
245+
246+
if (
247+
!variable.defs[0].node.decorators ||
248+
variable.defs[0].node.decorators.length === 0
249+
) {
250+
return false;
251+
}
252+
253+
for (const deco of variable.defs[0].node.decorators) {
254+
if (
255+
reference.identifier.range[0] >= deco.range[0] &&
256+
reference.identifier.range[1] <= deco.range[1]
257+
) {
258+
return true;
259+
}
260+
}
261+
262+
return false;
263+
}
264+
211265
//------------------------------------------------------------------------------
212266
// Rule Definition
213267
//------------------------------------------------------------------------------
@@ -237,6 +291,9 @@ module.exports = {
237291
classes: { type: "boolean" },
238292
variables: { type: "boolean" },
239293
allowNamedExports: { type: "boolean" },
294+
enums: { type: "boolean" },
295+
typedefs: { type: "boolean" },
296+
ignoreTypeReferences: { type: "boolean" },
240297
},
241298
additionalProperties: false,
242299
},
@@ -250,6 +307,9 @@ module.exports = {
250307
functions: true,
251308
variables: true,
252309
allowNamedExports: false,
310+
enums: true,
311+
typedefs: true,
312+
ignoreTypeReferences: true,
253313
},
254314
],
255315

@@ -310,6 +370,41 @@ module.exports = {
310370
return false;
311371
}
312372

373+
if (!options.enums && definitionType === "TSEnumName") {
374+
return false;
375+
}
376+
377+
if (!options.typedefs && definitionType === "Type") {
378+
return false;
379+
}
380+
381+
if (
382+
options.ignoreTypeReferences &&
383+
(referenceContainsTypeQuery(identifier) ||
384+
identifier.parent.type === "TSTypeReference")
385+
) {
386+
return false;
387+
}
388+
389+
// skip nested namespace aliases as variable references
390+
if (identifier.parent.type === "TSQualifiedName") {
391+
let currentNode = identifier.parent;
392+
393+
while (currentNode.type === "TSQualifiedName") {
394+
currentNode = currentNode.left;
395+
}
396+
397+
if (currentNode === identifier) {
398+
return true;
399+
}
400+
401+
return false;
402+
}
403+
404+
if (isClassRefInClassDecorator(variable, reference)) {
405+
return false;
406+
}
407+
313408
return true;
314409
}
315410

@@ -326,7 +421,8 @@ module.exports = {
326421
if (
327422
reference.identifier.range[1] <
328423
definitionIdentifier.range[1] ||
329-
isEvaluatedDuringInitialization(reference)
424+
(isEvaluatedDuringInitialization(reference) &&
425+
reference.identifier.parent.type !== "TSTypeReference")
330426
) {
331427
context.report({
332428
node: reference.identifier,

lib/types/rules.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4090,6 +4090,18 @@ export interface ESLintRules extends Linter.RulesRecord {
40904090
* @default false
40914091
*/
40924092
allowNamedExports: boolean;
4093+
/**
4094+
* @default true
4095+
*/
4096+
enums: boolean;
4097+
/**
4098+
* @default true
4099+
*/
4100+
typedefs: boolean;
4101+
/**
4102+
* @default true
4103+
*/
4104+
ignoreTypeReferences: boolean;
40934105
}>
40944106
| "nofunc",
40954107
]

0 commit comments

Comments
 (0)