From ad412cdd74dc5619fbe4bf27c0a5eb5c5a4b65ca Mon Sep 17 00:00:00 2001 From: TongZ <471205975@qq.com> Date: Fri, 29 Jul 2022 13:04:10 +0800 Subject: [PATCH] feat(eslint-plugin): [no-use-before-define] add "allowNamedExports" option (#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 Co-authored-by: Josh Goldberg --- .../src/rules/no-use-before-define.ts | 65 +++- .../tests/rules/no-use-before-define.test.ts | 317 ++++++++++++++++++ 2 files changed, 371 insertions(+), 11 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-use-before-define.ts b/packages/eslint-plugin/src/rules/no-use-before-define.ts index 3a5b51938b7..f9e397283ce 100644 --- a/packages/eslint-plugin/src/rules/no-use-before-define.ts +++ b/packages/eslint-plugin/src/rules/no-use-before-define.ts @@ -15,6 +15,7 @@ function parseOptions(options: string | Config | null): Required { let variables = true; let typedefs = true; let ignoreTypeReferences = true; + let allowNamedExports = false; if (typeof options === 'string') { functions = options !== 'nofunc'; @@ -25,6 +26,7 @@ function parseOptions(options: string | Config | null): Required { variables = options.variables !== false; typedefs = options.typedefs !== false; ignoreTypeReferences = options.ignoreTypeReferences !== false; + allowNamedExports = options.allowNamedExports !== false; } return { @@ -34,6 +36,7 @@ function parseOptions(options: string | Config | null): Required { variables, typedefs, ignoreTypeReferences, + allowNamedExports, }; } @@ -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 */ @@ -218,6 +232,7 @@ interface Config { variables?: boolean; typedefs?: boolean; ignoreTypeReferences?: boolean; + allowNamedExports?: boolean; } type Options = ['nofunc' | Config]; type MessageIds = 'noUseBeforeDefine'; @@ -249,6 +264,7 @@ export default util.createRule({ variables: { type: 'boolean' }, typedefs: { type: 'boolean' }, ignoreTypeReferences: { type: 'boolean' }, + allowNamedExports: { type: 'boolean' }, }, additionalProperties: false, }, @@ -264,6 +280,7 @@ export default util.createRule({ variables: true, typedefs: true, ignoreTypeReferences: true, + allowNamedExports: false, }, ], create(context, optionsWithDefault) { @@ -300,6 +317,16 @@ export default util.createRule({ 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. */ @@ -307,18 +334,40 @@ export default util.createRule({ 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 @@ -327,13 +376,7 @@ export default util.createRule({ } // Reports. - context.report({ - node: reference.identifier, - messageId: 'noUseBeforeDefine', - data: { - name: reference.identifier.name, - }, - }); + report(); }); scope.childScopes.forEach(findVariablesInScope); diff --git a/packages/eslint-plugin/tests/rules/no-use-before-define.test.ts b/packages/eslint-plugin/tests/rules/no-use-before-define.test.ts index 32924973a5c..676704fb279 100644 --- a/packages/eslint-plugin/tests/rules/no-use-before-define.test.ts +++ b/packages/eslint-plugin/tests/rules/no-use-before-define.test.ts @@ -326,6 +326,92 @@ enum Foo { `, options: [{ enums: false }], }, + + // "allowNamedExports" option + { + code: ` +export { a }; +const a = 1; + `, + options: [{ allowNamedExports: true }], + parserOptions, + }, + { + code: ` +export { a as b }; +const a = 1; + `, + options: [{ allowNamedExports: true }], + parserOptions, + }, + { + code: ` +export { a, b }; +let a, b; + `, + options: [{ allowNamedExports: true }], + parserOptions, + }, + { + code: ` +export { a }; +var a; + `, + options: [{ allowNamedExports: true }], + parserOptions, + }, + { + code: ` +export { f }; +function f() {} + `, + options: [{ allowNamedExports: true }], + parserOptions, + }, + { + code: ` +export { C }; +class C {} + `, + options: [{ allowNamedExports: true }], + parserOptions, + }, + { + code: ` +export { Foo }; + +enum Foo { + BAR, +} + `, + options: [{ allowNamedExports: true }], + parserOptions, + }, + { + code: ` +export { Foo }; + +namespace Foo { + export let bar = () => console.log('bar'); +} + `, + options: [{ allowNamedExports: true }], + parserOptions, + }, + { + code: ` +export { Foo, baz }; + +enum Foo { + BAR, +} + +let baz: Enum; +enum Enum {} + `, + options: [{ allowNamedExports: true }], + parserOptions, + }, // https://github.com/typescript-eslint/typescript-eslint/issues/2502 { code: ` @@ -1094,6 +1180,237 @@ enum Foo { }, ], }, + // "allowNamedExports" option + { + code: ` +export { a }; +const a = 1; + `, + parserOptions, + errors: [ + { + messageId: 'noUseBeforeDefine', + data: { name: 'a' }, + }, + ], + }, + { + code: ` +export { a }; +const a = 1; + `, + options: [{}], + parserOptions, + errors: [ + { + messageId: 'noUseBeforeDefine', + data: { name: 'a' }, + }, + ], + }, + { + code: ` +export { a }; +const a = 1; + `, + options: [{ allowNamedExports: false }], + parserOptions, + errors: [ + { + messageId: 'noUseBeforeDefine', + data: { name: 'a' }, + }, + ], + }, + { + code: ` +export { a }; +const a = 1; + `, + options: ['nofunc'], + parserOptions, + errors: [ + { + messageId: 'noUseBeforeDefine', + data: { name: 'a' }, + }, + ], + }, + { + code: ` +export { a as b }; +const a = 1; + `, + parserOptions, + errors: [ + { + messageId: 'noUseBeforeDefine', + data: { name: 'a' }, + }, + ], + }, + { + code: ` +export { a, b }; +let a, b; + `, + parserOptions, + errors: [ + { + messageId: 'noUseBeforeDefine', + data: { name: 'a' }, + }, + { + messageId: 'noUseBeforeDefine', + data: { name: 'b' }, + }, + ], + }, + { + code: ` +export { a }; +var a; + `, + parserOptions, + errors: [ + { + messageId: 'noUseBeforeDefine', + data: { name: 'a' }, + }, + ], + }, + { + code: ` +export { f }; +function f() {} + `, + parserOptions, + errors: [ + { + messageId: 'noUseBeforeDefine', + data: { name: 'f' }, + }, + ], + }, + { + code: ` +export { C }; +class C {} + `, + parserOptions, + errors: [ + { + messageId: 'noUseBeforeDefine', + data: { name: 'C' }, + }, + ], + }, + { + code: ` +export const foo = a; +const a = 1; + `, + options: [{ allowNamedExports: true }], + parserOptions, + errors: [ + { + messageId: 'noUseBeforeDefine', + data: { name: 'a' }, + }, + ], + }, + { + code: ` +export function foo() { + return a; +} +const a = 1; + `, + options: [{ allowNamedExports: true }], + parserOptions, + + errors: [ + { + messageId: 'noUseBeforeDefine', + data: { name: 'a' }, + }, + ], + }, + { + code: ` +export class C { + foo() { + return a; + } +} +const a = 1; + `, + options: [{ allowNamedExports: true }], + parserOptions, + + errors: [ + { + messageId: 'noUseBeforeDefine', + data: { name: 'a' }, + }, + ], + }, + { + code: ` +export { Foo }; + +enum Foo { + BAR, +} + `, + parserOptions, + errors: [ + { + messageId: 'noUseBeforeDefine', + data: { name: 'Foo' }, + }, + ], + }, + { + code: ` +export { Foo }; + +namespace Foo { + export let bar = () => console.log('bar'); +} + `, + parserOptions, + errors: [ + { + messageId: 'noUseBeforeDefine', + data: { name: 'Foo' }, + }, + ], + }, + { + code: ` +export { Foo, baz }; + +enum Foo { + BAR, +} + +let baz: Enum; +enum Enum {} + `, + options: [{ ignoreTypeReferences: true, allowNamedExports: false }], + parserOptions, + errors: [ + { + messageId: 'noUseBeforeDefine', + data: { name: 'Foo' }, + }, + { + messageId: 'noUseBeforeDefine', + data: { name: 'baz' }, + }, + ], + }, { code: ` f();