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-use-before-define] add "allowNamedExports" option #5397

Merged
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