Skip to content

Commit

Permalink
feat(eslint-plugin): [no-use-before-define] add "allowNamedExports" o…
Browse files Browse the repository at this point in the history
…ption (#5397)

* feat(eslint-plugin): [no-use-before-define] add allowNamedExports option

* chore: tmp commit

* chore(eslint-plugin): tmp commit

* feat(eslint-plugin): add allowNamedExports option

* chore(eslint-plugin): lint

* chore(eslint-plugin): human readable refactor

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

Co-authored-by: tongz <iamtongton@gmail.com>
Co-authored-by: Josh Goldberg <me@joshuakgoldberg.com>
  • Loading branch information
3 people committed Jul 29, 2022
1 parent ad26b74 commit ad412cd
Show file tree
Hide file tree
Showing 2 changed files with 371 additions and 11 deletions.
65 changes: 54 additions & 11 deletions packages/eslint-plugin/src/rules/no-use-before-define.ts
Expand Up @@ -15,6 +15,7 @@ function parseOptions(options: string | Config | null): Required<Config> {
let variables = true;
let typedefs = true;
let ignoreTypeReferences = true;
let allowNamedExports = false;

if (typeof options === 'string') {
functions = options !== 'nofunc';
Expand All @@ -25,6 +26,7 @@ function parseOptions(options: string | Config | null): Required<Config> {
variables = options.variables !== false;
typedefs = options.typedefs !== false;
ignoreTypeReferences = options.ignoreTypeReferences !== false;
allowNamedExports = options.allowNamedExports !== false;
}

return {
Expand All @@ -34,6 +36,7 @@ function parseOptions(options: string | Config | null): Required<Config> {
variables,
typedefs,
ignoreTypeReferences,
allowNamedExports,
};
}

Expand Down Expand Up @@ -90,6 +93,17 @@ function isOuterVariable(
);
}

/**
* Checks whether or not a given reference is a export reference.
*/
function isNamedExports(reference: TSESLint.Scope.Reference): boolean {
const { identifier } = reference;
return (
identifier.parent?.type === AST_NODE_TYPES.ExportSpecifier &&
identifier.parent.local === identifier
);
}

/**
* Recursively checks whether or not a given reference has a type query declaration among it's parents
*/
Expand Down Expand Up @@ -218,6 +232,7 @@ interface Config {
variables?: boolean;
typedefs?: boolean;
ignoreTypeReferences?: boolean;
allowNamedExports?: boolean;
}
type Options = ['nofunc' | Config];
type MessageIds = 'noUseBeforeDefine';
Expand Down Expand Up @@ -249,6 +264,7 @@ export default util.createRule<Options, MessageIds>({
variables: { type: 'boolean' },
typedefs: { type: 'boolean' },
ignoreTypeReferences: { type: 'boolean' },
allowNamedExports: { type: 'boolean' },
},
additionalProperties: false,
},
Expand All @@ -264,6 +280,7 @@ export default util.createRule<Options, MessageIds>({
variables: true,
typedefs: true,
ignoreTypeReferences: true,
allowNamedExports: false,
},
],
create(context, optionsWithDefault) {
Expand Down Expand Up @@ -300,25 +317,57 @@ export default util.createRule<Options, MessageIds>({
return true;
}

function isDefinedBeforeUse(
variable: TSESLint.Scope.Variable,
reference: TSESLint.Scope.Reference,
): boolean {
return (
variable.identifiers[0].range[1] <= reference.identifier.range[1] &&
!isInInitializer(variable, reference)
);
}

/**
* Finds and validates all variables in a given scope.
*/
function findVariablesInScope(scope: TSESLint.Scope.Scope): void {
scope.references.forEach(reference => {
const variable = reference.resolved;

function report(): void {
context.report({
node: reference.identifier,
messageId: 'noUseBeforeDefine',
data: {
name: reference.identifier.name,
},
});
}

// Skips when the reference is:
// - initializations.
// - referring to an undefined variable.
// - referring to a global environment variable (there're no identifiers).
// - located preceded by the variable (except in initializers).
// - allowed by options.
if (reference.init) {
return;
}

if (!options.allowNamedExports && isNamedExports(reference)) {
if (!variable || !isDefinedBeforeUse(variable, reference)) {
report();
}
return;
}

if (!variable) {
return;
}

if (
reference.init ||
!variable ||
variable.identifiers.length === 0 ||
(variable.identifiers[0].range[1] <= reference.identifier.range[1] &&
!isInInitializer(variable, reference)) ||
isDefinedBeforeUse(variable, reference) ||
!isForbidden(variable, reference) ||
isClassRefInClassDecorator(variable, reference) ||
reference.from.type === TSESLint.Scope.ScopeType.functionType
Expand All @@ -327,13 +376,7 @@ export default util.createRule<Options, MessageIds>({
}

// Reports.
context.report({
node: reference.identifier,
messageId: 'noUseBeforeDefine',
data: {
name: reference.identifier.name,
},
});
report();
});

scope.childScopes.forEach(findVariablesInScope);
Expand Down

0 comments on commit ad412cd

Please sign in to comment.