Skip to content

Commit 9e75ff2

Browse files
authoredNov 3, 2023
Update jsx-pragma rule to ensure it exists when using xcss prop (#1550)
* feat: update jsx-pragma rule to ensure it exists when using xcss prop * chore: changeset * chore: apply fixer * fix: use test over exec
1 parent 4de6927 commit 9e75ff2

File tree

5 files changed

+107
-51
lines changed

5 files changed

+107
-51
lines changed
 

‎.changeset/flat-forks-live.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@compiled/eslint-plugin': patch
3+
---
4+
5+
Update jsx-pragma lint rule to enforce the pragma is in scope when passing the `className` prop on host elements an output of xcss prop.

‎packages/babel-plugin/src/babel-plugin.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export default declare<State>((api) => {
9191
}
9292
}
9393

94-
if (!state.compiledImports && /(x|X)css={/.exec(file.code)) {
94+
if (!state.compiledImports && /(x|X)css={/.test(file.code)) {
9595
// xcss prop was found turn on Compiled
9696
state.compiledImports = {};
9797
}

‎packages/eslint-plugin/src/rules/jsx-pragma/__tests__/rule.test.ts

+34
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ tester.run('jsx-pragma', jsxPragmaRule, {
77
/** @jsxImportSource @compiled/react */
88
<div css={{ display: 'block' }} />
99
`,
10+
`
11+
<AnotherComponent className={xcss} />
12+
`,
13+
`
14+
<div className={anythingElse} />
15+
`,
16+
`
17+
/** @jsxImportSource @compiled/react */
18+
<div className={xcss} />
19+
`,
20+
`
21+
/** @jsxImportSource @compiled/react */
22+
<div className={innerXcss} />
23+
`,
1024
{
1125
code: `
1226
/** @jsxImportSource @compiled/react */
@@ -24,6 +38,26 @@ tester.run('jsx-pragma', jsxPragmaRule, {
2438
},
2539
],
2640
invalid: [
41+
{
42+
code: `
43+
<div className={xcss} />
44+
`,
45+
output: `
46+
/** @jsxImportSource @compiled/react */
47+
<div className={xcss} />
48+
`,
49+
errors: [{ messageId: 'missingPragmaXCSS' }],
50+
},
51+
{
52+
code: `
53+
<div className={innerXcss} />
54+
`,
55+
output: `
56+
/** @jsxImportSource @compiled/react */
57+
<div className={innerXcss} />
58+
`,
59+
errors: [{ messageId: 'missingPragmaXCSS' }],
60+
},
2761
{
2862
code: `<div css={{ display: 'block' }} />`,
2963
output: `/** @jsx jsx */

‎packages/eslint-plugin/src/rules/jsx-pragma/index.ts

+66-49
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,58 @@ const findReactDeclarationWithDefaultImport = (
2727
return undefined;
2828
};
2929

30+
function createFixer(context: Rule.RuleContext, source: SourceCode, options: Options) {
31+
return function* fix(fixer: Rule.RuleFixer) {
32+
const pragma = options.runtime === 'classic' ? '@jsx jsx' : '@jsxImportSource @compiled/react';
33+
const reactImport = findReactDeclarationWithDefaultImport(source);
34+
if (reactImport) {
35+
const [declaration, defaultImport] = reactImport;
36+
const [defaultImportVariable] = context.getDeclaredVariables(defaultImport);
37+
38+
if (defaultImportVariable && defaultImportVariable.references.length === 0) {
39+
if (declaration.specifiers.length === 1) {
40+
// Only the default specifier exists and it isn't used - remove the whole declaration!
41+
yield fixer.remove(declaration);
42+
} else {
43+
// Multiple specifiers exist but the default one isn't used - remove the default specifier!
44+
yield fixer.replaceText(declaration, removeImportFromDeclaration(declaration, []));
45+
}
46+
}
47+
}
48+
49+
yield fixer.insertTextBefore(source.ast.body[0], `/** ${pragma} */\n`);
50+
51+
const compiledImports = findCompiledImportDeclarations(context);
52+
53+
if (options.runtime === 'classic' && !findDeclarationWithImport(compiledImports, 'jsx')) {
54+
// jsx import is missing time to add one
55+
if (compiledImports.length === 0) {
56+
// No import exists, add a new one!
57+
yield fixer.insertTextBefore(
58+
source.ast.body[0],
59+
"import { jsx } from '@compiled/react';\n"
60+
);
61+
} else {
62+
// An import exists with no JSX! Let's add one to the first found.
63+
const [firstCompiledImport] = compiledImports;
64+
65+
yield fixer.replaceText(
66+
firstCompiledImport,
67+
addImportToDeclaration(firstCompiledImport, ['jsx'])
68+
);
69+
}
70+
}
71+
};
72+
}
73+
3074
export const jsxPragmaRule: Rule.RuleModule = {
3175
meta: {
3276
docs: {
3377
url: 'https://github.com/atlassian-labs/compiled/tree/master/packages/eslint-plugin/src/rules/jsx-pragma',
3478
},
3579
fixable: 'code',
3680
messages: {
81+
missingPragmaXCSS: 'Applying xcss prop to className requires the jsx pragma in scope.',
3782
missingPragma: 'To use the `css` prop you must set the {{ pragma }} pragma.',
3883
preferJsxImportSource:
3984
'Use of the jsxImportSource pragma (automatic runtime) is preferred over the jsx pragma (classic runtime).',
@@ -121,66 +166,38 @@ export const jsxPragmaRule: Rule.RuleModule = {
121166
}
122167
},
123168

169+
'JSXOpeningElement[name.name=/^[a-z]+$/] > JSXAttribute[name.name=/^className$/]': (
170+
node: Rule.Node
171+
) => {
172+
if (node.type !== 'JSXAttribute' || jsxPragma || jsxImportSourcePragma) {
173+
return;
174+
}
175+
176+
if (
177+
node.value?.type === 'JSXExpressionContainer' &&
178+
node.value.expression.type === 'Identifier' &&
179+
/[Xx]css$/.test(node.value.expression.name)
180+
) {
181+
context.report({
182+
node,
183+
messageId: 'missingPragmaXCSS',
184+
fix: createFixer(context, source, options),
185+
});
186+
}
187+
},
188+
124189
JSXAttribute(node: any) {
125190
if (jsxPragma || jsxImportSourcePragma || node.name.name !== 'css') {
126191
return;
127192
}
128193

129-
const pragma =
130-
options.runtime === 'classic' ? '@jsx jsx' : '@jsxImportSource @compiled/react';
131-
132194
context.report({
133195
messageId: 'missingPragma',
134196
data: {
135197
pragma: options.runtime === 'classic' ? 'jsx' : 'jsxImportSource',
136198
},
137199
node,
138-
*fix(fixer) {
139-
const reactImport = findReactDeclarationWithDefaultImport(source);
140-
if (reactImport) {
141-
const [declaration, defaultImport] = reactImport;
142-
const [defaultImportVariable] = context.getDeclaredVariables(defaultImport);
143-
144-
if (defaultImportVariable && defaultImportVariable.references.length === 0) {
145-
if (declaration.specifiers.length === 1) {
146-
// Only the default specifier exists and it isn't used - remove the whole declaration!
147-
yield fixer.remove(declaration);
148-
} else {
149-
// Multiple specifiers exist but the default one isn't used - remove the default specifier!
150-
yield fixer.replaceText(
151-
declaration,
152-
removeImportFromDeclaration(declaration, [])
153-
);
154-
}
155-
}
156-
}
157-
158-
yield fixer.insertTextBefore(source.ast.body[0], `/** ${pragma} */\n`);
159-
160-
const compiledImports = findCompiledImportDeclarations(context);
161-
162-
if (
163-
options.runtime === 'classic' &&
164-
!findDeclarationWithImport(compiledImports, 'jsx')
165-
) {
166-
// jsx import is missing time to add one
167-
if (compiledImports.length === 0) {
168-
// No import exists, add a new one!
169-
yield fixer.insertTextBefore(
170-
source.ast.body[0],
171-
"import { jsx } from '@compiled/react';\n"
172-
);
173-
} else {
174-
// An import exists with no JSX! Let's add one to the first found.
175-
const [firstCompiledImport] = compiledImports;
176-
177-
yield fixer.replaceText(
178-
firstCompiledImport,
179-
addImportToDeclaration(firstCompiledImport, ['jsx'])
180-
);
181-
}
182-
}
183-
},
200+
fix: createFixer(context, source, options),
184201
});
185202
},
186203
};

‎packages/eslint-plugin/src/rules/local-cx-xcss/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const localCXXCSSRule: Rule.RuleModule = {
3838
const parentJSXAttribute = getParentJSXAttribute(node);
3939
const propName = parentJSXAttribute?.name.name.toString();
4040

41-
if (propName && /[xX]css$/.exec(propName)) {
41+
if (propName && /[xX]css$/.test(propName)) {
4242
return;
4343
}
4444

0 commit comments

Comments
 (0)
Please sign in to comment.