From ce12c1f99dc63b8b3b87702b03e09934f3d788bb Mon Sep 17 00:00:00 2001 From: Trofim Samusev Date: Wed, 29 Jul 2026 16:46:08 +0400 Subject: [PATCH] [eslint-plugin] Fix no-unused false positive for styles exported via export specifier `no-unused` only inspected `node.declaration` in its `ExportNamedDeclaration` visitor, so `export { styles };` (where the exported bindings live in `node.specifiers`) never exempted the styles object and every key was reported as unused. Because the rule is fixable, `--fix` then deleted the style keys from the source. Exempt specifier exports too, skipping re-exports (`export { styles } from './x'`), which refer to another module's binding rather than a local variable. --- .../__tests__/stylex-no-unused-test.js | 226 ++++++++++++++++++ .../eslint-plugin/src/stylex-no-unused.js | 12 + 2 files changed, 238 insertions(+) diff --git a/packages/@stylexjs/eslint-plugin/__tests__/stylex-no-unused-test.js b/packages/@stylexjs/eslint-plugin/__tests__/stylex-no-unused-test.js index 482798600..64dc3a7d5 100644 --- a/packages/@stylexjs/eslint-plugin/__tests__/stylex-no-unused-test.js +++ b/packages/@stylexjs/eslint-plugin/__tests__/stylex-no-unused-test.js @@ -172,6 +172,58 @@ eslintTester.run('stylex-no-unused', rule.default, { export default styles; `, }, + { + // styles named export specifier + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + display: 'flex', + }, + dynamic: (color) => ({ + backgroundColor: color, + }) + }); + export { styles }; + `, + }, + { + // styles renamed export specifier + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + display: 'flex', + }, + }); + export { styles as sharedStyles }; + `, + }, + { + // styles export specifier alongside other exports + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + display: 'flex', + }, + }); + const theme = 'dark'; + export { theme, styles }; + `, + }, + { + // styles named default export specifier + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + display: 'flex', + }, + }); + export { styles as default }; + `, + }, { // styles named default inline export code: ` @@ -429,5 +481,179 @@ eslintTester.run('stylex-no-unused', rule.default, { }, ], }, + { + // re-export of another module's binding does not exempt local styles + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + color: 'red', + }, + unused: { + fontSize: '16px', + }, + }); + export { styles } from './other-styles'; + export default function Component() { + return
; + } + `, + output: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + color: 'red', + }, + }); + export { styles } from './other-styles'; + export default function Component() { + return
; + } + `, + errors: [ + { + message: 'Unused style detected: styles.unused', + }, + ], + }, + { + // type-only export does not export the styles object itself + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + color: 'red', + }, + unused: { + fontSize: '16px', + }, + }); + export type { styles }; + export default function Component() { + return
; + } + `, + output: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + color: 'red', + }, + }); + export type { styles }; + export default function Component() { + return
; + } + `, + errors: [ + { + message: 'Unused style detected: styles.unused', + }, + ], + }, + ], +}); + +// `export { type styles }` is TypeScript-only syntax, so the inline type +// specifier cases need the TypeScript parser. +const tsEslintTester = new ESLintTester({ + parser: require.resolve('@typescript-eslint/parser'), + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + }, +}); + +tsEslintTester.run('stylex-no-unused (typescript)', rule.default, { + valid: [ + { + // styles named export specifier + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + display: 'flex', + }, + }); + export { styles }; + `, + }, + { + // value export specifier alongside an inline type export specifier + code: ` + import * as stylex from '@stylexjs/stylex'; + type Theme = 'dark'; + const styles = stylex.create({ + main: { + display: 'flex', + }, + }); + export { type Theme, styles }; + `, + }, + ], + invalid: [ + { + // inline type export specifier does not exempt local styles + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + color: 'red', + }, + unused: { + fontSize: '16px', + }, + }); + export { type styles }; + stylex.props(styles.main); + `, + output: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + color: 'red', + }, + }); + export { type styles }; + stylex.props(styles.main); + `, + errors: [ + { + message: 'Unused style detected: styles.unused', + }, + ], + }, + { + // type-only export does not exempt local styles + code: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + color: 'red', + }, + unused: { + fontSize: '16px', + }, + }); + export type { styles }; + stylex.props(styles.main); + `, + output: ` + import * as stylex from '@stylexjs/stylex'; + const styles = stylex.create({ + main: { + color: 'red', + }, + }); + export type { styles }; + stylex.props(styles.main); + `, + errors: [ + { + message: 'Unused style detected: styles.unused', + }, + ], + }, ], }); diff --git a/packages/@stylexjs/eslint-plugin/src/stylex-no-unused.js b/packages/@stylexjs/eslint-plugin/src/stylex-no-unused.js index 9b62603c9..f9b4f6dae 100644 --- a/packages/@stylexjs/eslint-plugin/src/stylex-no-unused.js +++ b/packages/@stylexjs/eslint-plugin/src/stylex-no-unused.js @@ -209,6 +209,18 @@ const stylexNoUnused = { // Exempt used styles: export const exportStyles = stylex.create({}); ExportNamedDeclaration(node: ExportNamedDeclaration) { + // Exempt used styles: export { exportStyles }; + // Skip re-exports and type-only exports: no local value is exported + if (node.source == null && (node as $FlowFixMe).exportKind !== 'type') { + for (const specifier of node.specifiers) { + // Skip type-only specifier: export { type exportStyles }; + if ((specifier as $FlowFixMe).exportKind === 'type') { + continue; + } + stylexProperties.delete(specifier.local.name); + } + } + const declarations = node.declaration?.declarations; if (declarations?.length !== 1) { return;