Follow-up to #109 / #110 — a related false positive in the same code path.
reactComponentNameRE (/^[A-Z][a-zA-Z0-9_]*$/u) matches any identifier starting with an uppercase letter, including underscores — so it also matches SCREAMING_SNAKE_CASE constants like ENTITY_TYPE.
For inline exports (export const ENTITY_TYPE = 'Foo';) this is harmless, because handleExportIdentifier also inspects the initializer via isExpressionReactComponent and correctly classifies a string/number/object literal as non-component.
But for exports via a specifier list (const ENTITY_TYPE = 'Foo'; export { ENTITY_TYPE };), handleExportIdentifier is called without an initializer, so it falls back to the name-only check and treats ENTITY_TYPE as a component (hasReactExport = true).
That's invisible on its own (no error, since there's nothing to contrast it with), but it produces a false positive as soon as the same file also exports something that's correctly classified as non-component via the same specifier list — e.g. a plain class (already handled specially since #110):
const ENTITY_TYPE = 'Foo';
class SelectOption {
constructor(label, value) {
this.label = label;
this.value = value;
}
}
export { ENTITY_TYPE, SelectOption };
This reports SelectOption with namedExport, even though the file exports zero React components.
Fix
Mirror the ClassName scope-resolution added in #110: for a specifier referring to a local const/let variable, resolve its initializer from scope and run it through the normal handleExportIdentifier(identifier, init) path instead of the name-only fallback.
if (def?.type === "ClassName") {
handleExportDeclaration(def.node);
continue;
}
+if (def?.type === "Variable" && def.node.init !== null) {
+ handleExportIdentifier(
+ specifier.exported.type === "Identifier"
+ && specifier.exported.name === "default"
+ ? specifier.local
+ : specifier.exported,
+ def.node.init,
+ );
+ continue;
+}
(def.node is already typed as VariableDeclarator when def.type === "Variable", so no extra narrowing is needed there.)
PR: #114
Follow-up to #109 / #110 — a related false positive in the same code path.
reactComponentNameRE(/^[A-Z][a-zA-Z0-9_]*$/u) matches any identifier starting with an uppercase letter, including underscores — so it also matches SCREAMING_SNAKE_CASE constants likeENTITY_TYPE.For inline exports (
export const ENTITY_TYPE = 'Foo';) this is harmless, becausehandleExportIdentifieralso inspects the initializer viaisExpressionReactComponentand correctly classifies a string/number/object literal as non-component.But for exports via a specifier list (
const ENTITY_TYPE = 'Foo'; export { ENTITY_TYPE };),handleExportIdentifieris called without an initializer, so it falls back to the name-only check and treatsENTITY_TYPEas a component (hasReactExport = true).That's invisible on its own (no error, since there's nothing to contrast it with), but it produces a false positive as soon as the same file also exports something that's correctly classified as non-component via the same specifier list — e.g. a plain class (already handled specially since #110):
This reports
SelectOptionwithnamedExport, even though the file exports zero React components.Fix
Mirror the
ClassNamescope-resolution added in #110: for a specifier referring to a localconst/letvariable, resolve its initializer from scope and run it through the normalhandleExportIdentifier(identifier, init)path instead of the name-only fallback.if (def?.type === "ClassName") { handleExportDeclaration(def.node); continue; } +if (def?.type === "Variable" && def.node.init !== null) { + handleExportIdentifier( + specifier.exported.type === "Identifier" + && specifier.exported.name === "default" + ? specifier.local + : specifier.exported, + def.node.init, + ); + continue; +}(
def.nodeis already typed asVariableDeclaratorwhendef.type === "Variable", so no extra narrowing is needed there.)PR: #114