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
13 changes: 11 additions & 2 deletions packages/@stylexjs/eslint-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,20 +256,29 @@ specifications.
### `@stylexjs/no-conflicting-props`

This rule disallows using `className` or `style` props on elements that spread
`stylex.props()` to avoid conflicts and unexpected behavior.
`stylex.props()` or use StyleX JSX shorthand to avoid conflicts and unexpected
behavior.

#### Invalid examples

```jsx
<div {...stylex.props(styles.foo)} className="extra" />

<div {...stylex.props(styles.foo)} style={{ color: 'red' }} />

<div sx={styles.foo} className="extra" />

<div sx={styles.foo} style={{ color: 'red' }} />
```

#### Config options

```json
{
"validImports": ["stylex", "@stylexjs/stylex"]
"validImports": ["stylex", "@stylexjs/stylex"],
"sxPropName": "sx"
}
```

Set `sxPropName` to a string to check a custom JSX shorthand prop, or `false` to
disable JSX shorthand checks.
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ eslintTester.run('stylex-no-conflicting-props', rule.default, {
}
`,
},
{
code: `
import * as stylex from '@stylexjs/stylex';
function Component() {
return <div sx className="foo" />;
}
`,
},
{
code: `
import * as stylex from '@stylexjs/stylex';
function Component() {
return <div sx="foo" className="foo" />;
}
`,
},
{
code: `
import * as stylex from '@stylexjs/stylex';
Expand Down Expand Up @@ -109,6 +125,40 @@ eslintTester.run('stylex-no-conflicting-props', rule.default, {
}
`,
},
{
code: `
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({
main: { color: 'red' },
});
function Component() {
return <div sx={styles.main} data-testid="x" />;
}
`,
},
{
code: `
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({
main: { color: 'red' },
});
function Component() {
return <CustomComponent sx={styles.main} className="foo" />;
}
`,
},
{
options: [{ sxPropName: false }],
code: `
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({
main: { color: 'red' },
});
function Component() {
return <div sx={styles.main} className="foo" />;
}
`,
},
],
invalid: [
{
Expand Down Expand Up @@ -304,5 +354,91 @@ eslintTester.run('stylex-no-conflicting-props', rule.default, {
},
],
},
{
code: `
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({
main: { color: 'red' },
});
function Component() {
return <div sx={styles.main} className="foo" />;
}
`,
errors: [
{
message:
'The `className` prop should not be used with the `sx` StyleX prop to avoid conflicts.',
},
],
},
Comment thread
mjames-c marked this conversation as resolved.
{
code: `
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({
main: { color: 'red' },
});
function Component() {
return <div className="foo" sx={styles.main} />;
}
`,
errors: [
{
message:
'The `className` prop should not be used with the `sx` StyleX prop to avoid conflicts.',
},
],
},
{
code: `
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({
main: { color: 'red' },
});
function Component() {
return <div sx={styles.main} style={{ margin: 10 }} />;
}
`,
errors: [
{
message:
'The `style` prop should not be used with the `sx` StyleX prop to avoid conflicts.',
},
],
},
{
code: `
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({
main: { color: 'red' },
});
function Component() {
return <div sx={styles.main} {...{ className: 'foo' }} />;
}
`,
errors: [
{
message:
'The `className` prop should not be used with the `sx` StyleX prop to avoid conflicts.',
},
],
},
{
options: [{ sxPropName: 'css' }],
code: `
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({
main: { color: 'red' },
});
function Component() {
return <div css={styles.main} className="foo" />;
}
`,
errors: [
{
message:
'The `className` prop should not be used with the `css` StyleX prop to avoid conflicts.',
},
],
},
],
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,15 @@ type JSXIdentifier = {
+name: string,
};

type JSXExpressionContainer = {
+type: 'JSXExpressionContainer',
+expression: Node | { +type: 'JSXEmptyExpression' },
};

type JSXAttribute = {
+type: 'JSXAttribute',
+name: JSXIdentifier | { +type: string },
+value?: Node | JSXExpressionContainer | null,
};

type JSXSpreadAttribute = {
Expand All @@ -30,15 +36,22 @@ type JSXSpreadAttribute = {

type JSXOpeningElement = {
+type: 'JSXOpeningElement',
+name: JSXIdentifier | { +type: string },
+attributes: $ReadOnlyArray<JSXAttribute | JSXSpreadAttribute>,
};

const STYLEX_PROPS_CONFLICTING_PROPS_MESSAGE =
'The `{{propName}}` prop should not be used when spreading `stylex.props()` to avoid conflicts.';

const STYLEX_SHORTHAND_CONFLICTING_PROPS_MESSAGE =
'The `{{propName}}` prop should not be used with the `{{sxPropName}}` StyleX prop to avoid conflicts.';

const stylexNoConflictingProps = {
meta: {
type: 'problem',
docs: {
description:
'Disallow using `className` or `style` props on elements that spread `stylex.props()`',
'Disallow using `className` or `style` props on elements that spread `stylex.props()` or use StyleX JSX shorthand',
category: 'Best Practices',
recommended: true,
},
Expand All @@ -62,14 +75,20 @@ const stylexNoConflictingProps = {
},
default: ['stylex', '@stylexjs/stylex'],
},
sxPropName: {
oneOf: [{ type: 'string' }, { enum: [false] }],
default: 'sx',
},
Comment thread
mjames-c marked this conversation as resolved.
},
additionalProperties: false,
},
],
},
create(context: Rule.RuleContext): { ... } {
const { validImports: importsToLookFor = ['stylex', '@stylexjs/stylex'] } =
context.options[0] || {};
const {
validImports: importsToLookFor = ['stylex', '@stylexjs/stylex'],
sxPropName = 'sx',
} = context.options[0] || {};

const importTracker = createImportTracker(importsToLookFor);

Expand All @@ -85,6 +104,30 @@ const stylexNoConflictingProps = {
);
}

function isLowercaseHostElement(node: JSXOpeningElement): boolean {
return (
node.name.type === 'JSXIdentifier' &&
typeof node.name.name === 'string' &&
node.name.name[0] === node.name.name[0].toLowerCase()
);
}
Comment on lines +107 to +113

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.

this matches the logic in the babel transformer:

node.name.type !== 'JSXIdentifier' ||
typeof node.name.name !== 'string' ||
node.name.name[0] !== node.name.name[0].toLowerCase()


function hasStylexShorthandProp(node: JSXOpeningElement): boolean {
return (
typeof sxPropName === 'string' &&
isLowercaseHostElement(node) &&
node.attributes.some(
(attr) =>
attr.type === 'JSXAttribute' &&
attr.name.type === 'JSXIdentifier' &&
attr.name.name === sxPropName &&
attr.value != null &&
attr.value.type === 'JSXExpressionContainer' &&
attr.value.expression.type !== 'JSXEmptyExpression',
)
);
}

return {
ImportDeclaration: importTracker.ImportDeclaration,

Expand All @@ -96,10 +139,16 @@ const stylexNoConflictingProps = {
isStylexPropsCallee(attr.argument.callee),
);

if (!hasStylexPropsSpread) {
const hasStylexShorthand = hasStylexShorthandProp(node);

if (!hasStylexPropsSpread && !hasStylexShorthand) {
return;
}

const message = hasStylexShorthand
? STYLEX_SHORTHAND_CONFLICTING_PROPS_MESSAGE
: STYLEX_PROPS_CONFLICTING_PROPS_MESSAGE;

for (const attr of node.attributes) {
if (
attr.type === 'JSXAttribute' &&
Expand All @@ -109,9 +158,8 @@ const stylexNoConflictingProps = {
context.report({
// $FlowFixMe[incompatible-type]
node: attr,
message:
'The `{{propName}}` prop should not be used when spreading `stylex.props()` to avoid conflicts.',
data: { propName: attr.name.name },
message,
data: { propName: attr.name.name, sxPropName },
});
} else if (
attr.type === 'JSXSpreadAttribute' &&
Expand All @@ -127,9 +175,8 @@ const stylexNoConflictingProps = {
context.report({
// $FlowFixMe[incompatible-type]
node: prop,
message:
'The `{{propName}}` prop should not be used when spreading `stylex.props()` to avoid conflicts.',
data: { propName: prop.key.name },
message,
data: { propName: prop.key.name, sxPropName },
});
}
}
Expand Down
Loading