Skip to content

Commit 157e7ee

Browse files
authoredNov 2, 2023
XCSS prop local cx lint (#1548)
* chore: init * chore: add to configs * feat: add local-cx-xcss rule * chore: changeset * chore: fix * chore: fix readme * chore: now * chore: fix incorrect names * chore: resolve code review comments * chore: fix white space
1 parent 2010cde commit 157e7ee

File tree

9 files changed

+191
-3
lines changed

9 files changed

+191
-3
lines changed
 

‎.changeset/light-shirts-destroy.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@compiled/eslint-plugin': patch
3+
---
4+
5+
Add supplementary lint rule for xcss prop `local-cx-xcss`.

‎packages/eslint-plugin/src/configs/recommended.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export const recommended = {
22
plugins: ['@compiled'],
33
rules: {
4+
'@compiled/local-cx-xcss': 'error',
45
'@compiled/no-css-prop-without-css-function': 'error',
56
'@compiled/no-css-tagged-template-expression': 'error',
67
'@compiled/no-exported-css': 'error',

‎packages/eslint-plugin/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { recommended } from './configs/recommended';
22
import { jsxPragmaRule } from './rules/jsx-pragma';
3+
import { localCXXCSSRule } from './rules/local-cx-xcss';
34
import { noCssPropWithoutCssFunctionRule } from './rules/no-css-prop-without-css-function';
45
import { noCssTaggedTemplateExpressionRule } from './rules/no-css-tagged-template-expression';
56
import { noEmotionCssRule } from './rules/no-emotion-css';
@@ -13,6 +14,7 @@ import { noSuppressXCSS } from './rules/no-suppress-xcss';
1314

1415
export const rules = {
1516
'jsx-pragma': jsxPragmaRule,
17+
'local-cx-xcss': localCXXCSSRule,
1618
'no-css-prop-without-css-function': noCssPropWithoutCssFunctionRule,
1719
'no-css-tagged-template-expression': noCssTaggedTemplateExpressionRule,
1820
'no-emotion-css': noEmotionCssRule,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# `local-cx-xcss`
2+
3+
This rule ensures the `cx()` function is only used within the `xcss` prop. This aids tracking what styles are applied to a jsx element.
4+
5+
👎 Examples of **incorrect** code for this rule:
6+
7+
```js
8+
import { cx, cssMap } from '@compiled/react';
9+
10+
const styles = cssMap({
11+
text: { color: 'red' },
12+
bg: { background: 'blue' },
13+
});
14+
15+
const joinedStyles = cx(styles.text, styles.bg);
16+
17+
<Button xcss={joinedStyles} />;
18+
```
19+
20+
👍 Examples of **correct** code for this rule:
21+
22+
```js
23+
import { cx, cssMap } from '@compiled/react';
24+
25+
const styles = cssMap({
26+
text: { color: 'red' },
27+
bg: { background: 'blue' },
28+
});
29+
30+
<Button xcss={cx(styles.text, styles.bg)} />;
31+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { typeScriptTester as tester } from '../../../test-utils';
2+
import { localCXXCSSRule } from '../index';
3+
4+
tester.run('local-cx-xcss', localCXXCSSRule, {
5+
valid: [
6+
`
7+
import { cx } from '@compiled/react';
8+
9+
<Component xcss={cx({})} />
10+
`,
11+
`
12+
import { cx } from '@compiled/react';
13+
14+
<Component innerXcss={cx({})} />
15+
`,
16+
`
17+
import { cx, cssMap } from '@compiled/react';
18+
19+
const styles = cssMap({
20+
text: { color: 'red' },
21+
bg: { background: 'blue' },
22+
});
23+
24+
<Button innerXcss={cx(styles.text, styles.bg)} />;
25+
`,
26+
`
27+
// Ignore cx usage not from compiled
28+
const styles = cssMap({
29+
text: { color: 'red' },
30+
bg: { background: 'blue' },
31+
});
32+
33+
const joinedStyles = cx(styles.text, styles.bg);
34+
35+
<Button xcss={joinedStyles} />;
36+
`,
37+
`
38+
// Ignore cx usage not from compiled
39+
const styles = cx({});
40+
41+
<Component xcss={styles} />
42+
`,
43+
],
44+
invalid: [
45+
{
46+
code: `
47+
import { cx } from '@compiled/react';
48+
const styles = cx({});
49+
50+
<Component xcss={styles} />
51+
`,
52+
errors: [{ messageId: 'local-cx-xcss' }],
53+
},
54+
{
55+
code: `
56+
import { cx, cssMap } from '@compiled/react';
57+
58+
const styles = cssMap({
59+
text: { color: 'red' },
60+
bg: { background: 'blue' },
61+
});
62+
63+
const joinedStyles = cx(styles.text, styles.bg);
64+
65+
<Button xcss={joinedStyles} />;
66+
`,
67+
errors: [{ messageId: 'local-cx-xcss' }],
68+
},
69+
{
70+
code: `
71+
import { cx } from '@compiled/react';
72+
const styles = cx({});
73+
74+
<Component innerXcss={styles} />
75+
`,
76+
errors: [{ messageId: 'local-cx-xcss' }],
77+
},
78+
{
79+
code: `
80+
import { cx, cssMap} from '@compiled/react';
81+
82+
const styles = cssMap({
83+
text: { color: 'red' },
84+
bg: { background: 'blue' },
85+
});
86+
87+
const joinedStyles = cx(styles.text, styles.bg);
88+
89+
<Button innerXcss={joinedStyles} />;
90+
`,
91+
errors: [{ messageId: 'local-cx-xcss' }],
92+
},
93+
],
94+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { Rule } from 'eslint';
2+
3+
import { isCxFunction } from '../../utils';
4+
5+
function getParentJSXAttribute(node: Rule.Node) {
6+
let parent: Rule.Node | null = node.parent;
7+
8+
while (parent && parent.type !== 'JSXAttribute') {
9+
parent = parent.parent;
10+
}
11+
12+
if (parent && parent.type === 'JSXAttribute') {
13+
return parent;
14+
}
15+
16+
return null;
17+
}
18+
19+
export const localCXXCSSRule: Rule.RuleModule = {
20+
meta: {
21+
docs: {
22+
recommended: true,
23+
url: 'https://github.com/atlassian-labs/compiled/tree/master/packages/eslint-plugin/src/rules/local-cx-xcss',
24+
},
25+
messages: {
26+
'local-cx-xcss':
27+
'The cx function should only be declared inside the xcss prop to simplify tracking styles that are applied to a jsx element',
28+
},
29+
type: 'problem',
30+
},
31+
create(context) {
32+
return {
33+
'CallExpression[callee.name="cx"]': (node: Rule.Node) => {
34+
if (
35+
node.type === 'CallExpression' &&
36+
isCxFunction(node.callee as Rule.Node, context.getScope().references)
37+
) {
38+
const parentJSXAttribute = getParentJSXAttribute(node);
39+
const propName = parentJSXAttribute?.name.name.toString();
40+
41+
if (propName && /[xX]css$/.exec(propName)) {
42+
return;
43+
}
44+
45+
context.report({
46+
node,
47+
messageId: 'local-cx-xcss',
48+
});
49+
}
50+
},
51+
};
52+
},
53+
};

‎packages/eslint-plugin/src/utils/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ export { createNoExportedRule } from './create-no-exported-rule';
22
export { checkIfCompiledExport as validateDefinition } from './create-no-exported-rule/check-if-compiled-export';
33
export { createNoTaggedTemplateExpressionRule } from './create-no-tagged-template-expression-rule';
44
export { CssMapObjectChecker, getCssMapObject } from './css-map';
5-
export { isCss, isCssMap, isKeyframes } from './is-compiled-import';
5+
export { isCss, isCssMap, isKeyframes, isCxFunction } from './is-compiled-import';

‎packages/eslint-plugin/src/utils/is-compiled-import.ts

+1
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,6 @@ const isCompiledImport = (name: string): CompiledNameChecker => {
2828
};
2929

3030
export const isCss = isCompiledImport('css');
31+
export const isCxFunction = isCompiledImport('cx');
3132
export const isCssMap = isCompiledImport('cssMap');
3233
export const isKeyframes = isCompiledImport('keyframes');

‎packages/react/src/css-map/index.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,12 @@ type ExtendedSelectors = {
9191
*
9292
* @example
9393
* ```
94-
* const borderStyleMap = cssMap({
94+
* const styles = cssMap({
9595
* none: { borderStyle: 'none' },
9696
* solid: { borderStyle: 'solid' },
9797
* });
98-
* const Component = ({ borderStyle }) => <div css={css(borderStyleMap[borderStyle])} />
98+
*
99+
* const Component = ({ borderStyle }) => <div css={styles[borderStyle]} />
99100
*
100101
* <Component borderStyle="solid" />
101102
* ```

0 commit comments

Comments
 (0)
Please sign in to comment.