Skip to content

only-export-components: SCREAMING_SNAKE_CASE constant exported via export { Name } is misidentified as a component, causing false positive when mixed with a class export #113

Description

@rakleed

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions