Skip to content

Commit 351dbc2

Browse files
authoredNov 2, 2023
No suppress XCSS prop lint rule (#1545)
* chore: init new lint rule * feat: add no-suppress-xcss rule logic * fix: lint indiviudal properties * chore: resolve code review comments * chore: changeset
1 parent e51db08 commit 351dbc2

File tree

7 files changed

+268
-9
lines changed

7 files changed

+268
-9
lines changed
 

‎.changeset/early-lions-prove.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@compiled/eslint-plugin': patch
3+
---
4+
5+
Adds a new supplementary rule for xcss prop — `no-suppress-xcss`.
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
export const recommended = {
22
plugins: ['@compiled'],
33
rules: {
4+
'@compiled/no-css-prop-without-css-function': 'error',
45
'@compiled/no-css-tagged-template-expression': 'error',
56
'@compiled/no-exported-css': 'error',
67
'@compiled/no-exported-keyframes': 'error',
8+
'@compiled/no-invalid-css-map': 'error',
79
'@compiled/no-keyframes-tagged-template-expression': 'error',
810
'@compiled/no-styled-tagged-template-expression': 'error',
9-
'@compiled/no-css-prop-without-css-function': 'error',
10-
'@compiled/no-invalid-css-map': 'error',
11+
'@compiled/no-suppress-xcss': 'error',
1112
},
1213
};

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

+5-3
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@ import { noExportedKeyframesRule } from './rules/no-exported-keyframes';
88
import { noInvalidCssMapRule } from './rules/no-invalid-css-map';
99
import { noKeyframesTaggedTemplateExpressionRule } from './rules/no-keyframes-tagged-template-expression';
1010
import { noStyledTaggedTemplateExpressionRule } from './rules/no-styled-tagged-template-expression';
11+
import { noSuppressXCSS } from './rules/no-suppress-xcss';
1112

1213
export const rules = {
1314
'jsx-pragma': jsxPragmaRule,
15+
'no-css-prop-without-css-function': noCssPropWithoutCssFunctionRule,
1416
'no-css-tagged-template-expression': noCssTaggedTemplateExpressionRule,
17+
'no-emotion-css': noEmotionCssRule,
1518
'no-exported-css': noExportedCssRule,
1619
'no-exported-keyframes': noExportedKeyframesRule,
17-
'no-emotion-css': noEmotionCssRule,
20+
'no-invalid-css-map': noInvalidCssMapRule,
1821
'no-keyframes-tagged-template-expression': noKeyframesTaggedTemplateExpressionRule,
1922
'no-styled-tagged-template-expression': noStyledTaggedTemplateExpressionRule,
20-
'no-css-prop-without-css-function': noCssPropWithoutCssFunctionRule,
21-
'no-invalid-css-map': noInvalidCssMapRule,
23+
'no-suppress-xcss': noSuppressXCSS,
2224
};
2325

2426
export const configs = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# `no-suppress-xcss`
2+
3+
Disallows supressing type violations when using the xcss prop. Supressing type violations will cause incidents and unexpected behaviour when code changes in the future.
4+
5+
Components that use xcss prop have explicitly declared what styles they should and should not accept, consumers must adhere to this API.
6+
7+
👎 Examples of **incorrect** code for this rule:
8+
9+
```js
10+
// @ts-expect-error
11+
<Button xcss={{ fill: 'var(--ds-text)' }} />
12+
```
13+
14+
👍 Examples of **correct** code for this rule:
15+
16+
```js
17+
<Button xcss={{ color: 'var(--ds-text)' }} />
18+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { typeScriptTester as tester } from '../../../test-utils';
2+
import { noSuppressXCSS } from '../index';
3+
4+
tester.run('no-styled-tagged-template-expression', noSuppressXCSS, {
5+
valid: [
6+
`
7+
<Component xcss={{ fill: 'red' }} />
8+
`,
9+
`
10+
<Component innerXcss={{ fill: 'red' }} />
11+
`,
12+
`
13+
// @ts-expect-error
14+
function Foo() {
15+
// @ts-ignore
16+
return (
17+
<>
18+
<Component xcss={{ fill: 'red' }} />
19+
</>
20+
);
21+
}
22+
`,
23+
],
24+
invalid: [
25+
{
26+
code: `
27+
<Component
28+
xcss={{
29+
// @ts-expect-error
30+
fill: 'red'
31+
}}
32+
/>
33+
`,
34+
errors: [{ messageId: 'no-suppress-xcss' }],
35+
},
36+
{
37+
code: `
38+
<Component
39+
xcss={{
40+
// @ts-ignore
41+
fill: 'red'
42+
}}
43+
/>
44+
`,
45+
errors: [{ messageId: 'no-suppress-xcss' }],
46+
},
47+
{
48+
code: `
49+
<Component
50+
xcss={{
51+
// @ts-nocheck
52+
fill: 'red'
53+
}}
54+
/>
55+
`,
56+
errors: [{ messageId: 'no-suppress-xcss' }],
57+
},
58+
{
59+
code: `
60+
// @ts-expect-error
61+
<Component xcss={{ fill: 'red' }} />
62+
`,
63+
errors: [{ messageId: 'no-suppress-xcss' }],
64+
},
65+
{
66+
code: `
67+
// @ts-expect-error
68+
<Component innerXcss={{ fill: 'red' }} />
69+
`,
70+
errors: [{ messageId: 'no-suppress-xcss' }],
71+
},
72+
{
73+
code: `
74+
// @ts-ignore
75+
<Component xcss={{ fill: 'red' }} />
76+
`,
77+
errors: [{ messageId: 'no-suppress-xcss' }],
78+
},
79+
{
80+
code: `
81+
82+
<Component
83+
// @ts-expect-error
84+
innerXcss={{ fill: 'red' }}
85+
/>
86+
`,
87+
errors: [{ messageId: 'no-suppress-xcss' }],
88+
},
89+
{
90+
code: `
91+
92+
<Component
93+
// @ts-ignore
94+
xcss={{ fill: 'red' }}
95+
/>
96+
`,
97+
errors: [{ messageId: 'no-suppress-xcss' }],
98+
},
99+
{
100+
code: `
101+
102+
<Component
103+
// @ts-nocheck
104+
xcss={{ fill: 'red' }}
105+
/>
106+
`,
107+
errors: [{ messageId: 'no-suppress-xcss' }],
108+
},
109+
{
110+
code: `
111+
// @ts-ignore
112+
<Component innerXcss={{ fill: 'red' }} />
113+
`,
114+
errors: [{ messageId: 'no-suppress-xcss' }],
115+
},
116+
{
117+
code: `
118+
function Foo() {
119+
return (
120+
<>
121+
{/* @ts-ignore */}
122+
<Component xcss={{ fill: 'red' }} />
123+
</>
124+
);
125+
}
126+
`,
127+
errors: [{ messageId: 'no-suppress-xcss' }],
128+
},
129+
{
130+
code: `
131+
function Foo() {
132+
return (
133+
<>
134+
{/* @ts-expect-error */}
135+
<Component xcss={{ fill: 'red' }} />
136+
</>
137+
);
138+
}
139+
`,
140+
errors: [{ messageId: 'no-suppress-xcss' }],
141+
},
142+
],
143+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type { Rule } from 'eslint';
2+
3+
function nodeIsTypeSuppressed(context: Rule.RuleContext, node: Rule.Node) {
4+
if (!node.loc) {
5+
return;
6+
}
7+
8+
const comments = context.sourceCode.getAllComments();
9+
10+
for (const comment of comments) {
11+
if (!comment.loc) {
12+
continue;
13+
}
14+
15+
const commentLine = comment.loc.start.line;
16+
const nodeLine = node.loc.start.line;
17+
const isCommentOnPreviousLine = nodeLine - 1 === commentLine;
18+
19+
if (
20+
isCommentOnPreviousLine &&
21+
['@ts-expect-error', '@ts-ignore', '@ts-nocheck'].some((tag) => comment.value.includes(tag))
22+
) {
23+
return true;
24+
}
25+
}
26+
27+
return false;
28+
}
29+
30+
function getParentJSXAttribute(node: Rule.Node) {
31+
let parent: Rule.Node | null = node.parent;
32+
33+
while (parent && parent.type !== 'JSXAttribute') {
34+
parent = parent.parent;
35+
}
36+
37+
return parent;
38+
}
39+
40+
export const noSuppressXCSS: Rule.RuleModule = {
41+
meta: {
42+
docs: {
43+
recommended: true,
44+
description:
45+
'The xcss prop is predicated on adhering to the type contract. Supressing it breaks this contract and thus is not allowed.',
46+
url: 'https://github.com/atlassian-labs/compiled/tree/master/packages/eslint-plugin/src/rules/no-supress-xcss',
47+
},
48+
messages: {
49+
'no-suppress-xcss':
50+
'Supressing type violations inside xcss risks incidents and unintended behaviour when code changes — only declare allowed values',
51+
},
52+
type: 'problem',
53+
},
54+
create(context) {
55+
const violations = new WeakSet<Rule.Node>();
56+
57+
return {
58+
'JSXAttribute[name.name=/[xX]css$/] Property': (node: Rule.Node) => {
59+
const parent = getParentJSXAttribute(node);
60+
61+
if (!violations.has(parent) && nodeIsTypeSuppressed(context, node)) {
62+
context.report({
63+
node: node,
64+
messageId: 'no-suppress-xcss',
65+
});
66+
}
67+
},
68+
'JSXAttribute[name.name=/[xX]css$/]': (node: Rule.Node) => {
69+
if (node.type === 'JSXAttribute' && nodeIsTypeSuppressed(context, node)) {
70+
violations.add(node);
71+
context.report({
72+
node: node.name,
73+
messageId: 'no-suppress-xcss',
74+
});
75+
}
76+
},
77+
};
78+
},
79+
};

‎packages/react/src/xcss-prop/__tests__/xcss-prop.test.tsx

+15-4
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,14 @@ describe('xcss prop', () => {
165165
).toBeObject();
166166
expectTypeOf(
167167
<CSSPropComponent
168-
// @ts-expect-error — Types of property 'backgroundColor' are incompatible.
169-
xcss={{ color: 'red', '&:hover': { color: 'red', backgroundColor: 'red' } }}
168+
xcss={{
169+
color: 'red',
170+
'&:hover': {
171+
color: 'red',
172+
// @ts-expect-error — Types of property 'backgroundColor' are incompatible.
173+
backgroundColor: 'red',
174+
},
175+
}}
170176
/>
171177
).toBeObject();
172178
});
@@ -188,8 +194,13 @@ describe('xcss prop', () => {
188194
).toBeObject();
189195
expectTypeOf(
190196
<CSSPropComponent
191-
// @ts-expect-error — Type '{ screen: { color: string; backgroundColor: string; }; }' is not assignable to type 'undefined'.
192-
xcss={{ color: 'red', '@media': { screen: { color: 'red', backgroundColor: 'red' } } }}
197+
xcss={{
198+
color: 'red',
199+
// @ts-expect-error — Type '{ screen: { color: string; backgroundColor: string; }; }' is not assignable to type 'undefined'.
200+
'@media': {
201+
screen: { color: 'red', backgroundColor: 'red' },
202+
},
203+
}}
193204
/>
194205
).toBeObject();
195206
});

0 commit comments

Comments
 (0)
Please sign in to comment.