Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 226 additions & 0 deletions packages/@stylexjs/eslint-plugin/__tests__/stylex-no-unused-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: `
Expand Down Expand Up @@ -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 <div {...stylex.props(styles.main)} />;
}
`,
output: `
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({
main: {
color: 'red',
},
});
export { styles } from './other-styles';
export default function Component() {
return <div {...stylex.props(styles.main)} />;
}
`,
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 <div {...stylex.props(styles.main)} />;
}
`,
output: `
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({
main: {
color: 'red',
},
});
export type { styles };
export default function Component() {
return <div {...stylex.props(styles.main)} />;
}
`,
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',
},
],
},
],
});
12 changes: 12 additions & 0 deletions packages/@stylexjs/eslint-plugin/src/stylex-no-unused.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,18 @@ const stylexNoUnused = {

// Exempt used styles: export const exportStyles = stylex.create({});
ExportNamedDeclaration(node: ExportNamedDeclaration) {
// Exempt used styles: export { exportStyles };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an edge case but should we make sure there are no false positives for
export type { styles }; or export { type styles }; by checking the exportKind on the declaration and specifiers?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added support for those cases. Can you please check again when you'll have some time?

// 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;
Expand Down