Skip to content

Commit d75db85

Browse files
authored
Fix some false positives with ternary operators and cssMap (#1781)
* Fix some false positives with ternary operators and cssMap * Add changeset * Remove unnecessary spread operator * Improve naming in union function
1 parent a2eb963 commit d75db85

File tree

3 files changed

+214
-39
lines changed

3 files changed

+214
-39
lines changed

.changeset/few-camels-compare.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@compiled/eslint-plugin': patch
3+
---
4+
5+
Fix some false positives in `shorthand-property-sorting` with css and cssMap

packages/eslint-plugin/src/rules/shorthand-property-sorting/__tests__/rule.test.ts

Lines changed: 152 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,88 @@ tester.run('shorthand-property-sorting', shorthandFirst, {
270270
`,
271271
},
272272

273+
//
274+
// styles from ternary operators should not cause false positives, if the same properties
275+
// are defined in each style object
276+
//
277+
278+
{
279+
name: `styles from ternary operators should not cause false positive, if the same properties are defined (css, ${imp})`,
280+
code: `
281+
import { css } from '${imp}';
282+
const oldStyles = css({
283+
font: '...',
284+
fontWeight: '...',
285+
});
286+
const newStyles = css({
287+
font: '...',
288+
fontWeight: '...',
289+
});
290+
const someCondition = true;
291+
export const EmphasisText = ({ children }) => <span css={[someCondition ? oldStyles : newStyles]}>{children}</span>;
292+
`,
293+
},
294+
{
295+
name: `pseudo-classes from ternary operators should not cause false positive, if the same properties are defined (css, ${imp})`,
296+
code: `
297+
import { css } from '${imp}';
298+
const oldStyles = css({
299+
'&:hover': {
300+
font: '...',
301+
fontWeight: '...',
302+
}
303+
});
304+
const newStyles = css({
305+
'&:hover': {
306+
font: '...',
307+
fontWeight: '...',
308+
},
309+
});
310+
const someCondition = true;
311+
export const EmphasisText = ({ children }) => <span css={[someCondition ? oldStyles : newStyles]}>{children}</span>;
312+
`,
313+
},
314+
{
315+
name: `styles from ternary operators should not cause false positive, if the same properties are defined (cssMap, ${imp})`,
316+
code: `
317+
import { css } from '${imp}';
318+
const myMap = cssMap({
319+
warning: {
320+
font: '...',
321+
fontWeight: '...',
322+
},
323+
normal: {
324+
font: '...',
325+
fontWeight: '...',
326+
}
327+
});
328+
const someCondition = true;
329+
export const EmphasisText = ({ appearance, children }) => <span css={myMap[apperance]}>{children}</span>;
330+
`,
331+
},
332+
{
333+
name: `pseudo-classes from ternary operators should not cause false positive, if the same properties are defined (cssMap, ${imp})`,
334+
code: `
335+
import { cssMap } from '${imp}';
336+
const myMap = cssMap({
337+
warning: {
338+
'&:hover': {
339+
font: '...',
340+
fontWeight: '...',
341+
},
342+
},
343+
normal: {
344+
'&:hover': {
345+
font: '...',
346+
fontWeight: '...',
347+
},
348+
}
349+
});
350+
const someCondition = true;
351+
export const EmphasisText = ({ appearance, children }) => <span css={myMap[apperance]}>{children}</span>;
352+
`,
353+
},
354+
273355
//
274356
// has a valid sorting for borderInlineStart and borderInlineEnd
275357
//
@@ -310,6 +392,54 @@ tester.run('shorthand-property-sorting', shorthandFirst, {
310392
export const EmphasisText = ({ children }) => <span css={styles}>{children}</span>;
311393
`,
312394
},
395+
396+
// false negative: styles in pseudo-classes are not checked when using style composition
397+
398+
{
399+
// Currently not handled due to the added complexity of handling selectors.
400+
// Feel free to update the rule to handle this, if it becomes a problem
401+
name: `false negative: styles in pseudo-classes are not checked when using style composition (css, ${imp})`,
402+
code: `
403+
import { css } from '${imp}';
404+
const baseStyles = css({
405+
'&:hover': {
406+
paddingLeft: '...',
407+
}
408+
});
409+
const newStyles = css({
410+
'&:hover': {
411+
padding: '...',
412+
}
413+
});
414+
export const EmphasisText = ({ children }) => <span css={[baseStyles, newStyles]}>{children}</span>;
415+
`,
416+
},
417+
418+
//
419+
// depth in correct order for shorthand properties in the same bucket for cssMap AND if key not static
420+
//
421+
422+
{
423+
name: `depth in correct order for shorthand properties in the same bucket for cssMap AND if key not static (${imp})`,
424+
code: outdent`
425+
import { cssMap } from '${imp}';
426+
const styles = cssMap({
427+
warning: {
428+
borderColor: '...', // 2
429+
borderTop: '...', // 1
430+
},
431+
debug: {
432+
borderColor: '...', // 2
433+
borderTop: '...', // 1
434+
},
435+
normal: {
436+
borderColor: '...', // 2
437+
borderTop: '...', // 1
438+
},
439+
});
440+
export const EmphasisText = ({ children, appearance }) => <span css={styles[appearance]}>{children}</span>;
441+
`,
442+
},
313443
]),
314444

315445
invalid: includedImports.flatMap((imp) => [
@@ -796,39 +926,42 @@ tester.run('shorthand-property-sorting', shorthandFirst, {
796926
},
797927

798928
//
799-
// false positives for cssMap
929+
// known false positives for css
800930
//
801931

802932
{
803-
name: `false positive: shorthands in different selectors for cssMap if key not static (${imp})`,
804-
code: outdent`
805-
import { cssMap } from '${imp}';
806-
const styles = cssMap({
807-
warning: {
808-
borderTop: '...',
809-
},
810-
normal: {
811-
border: '...',
812-
}
933+
// I don't see a good way for the ESLint rule to handle this, without running the
934+
// rule multiple times to handle each branch of the ternary operator (which can be
935+
// exponentially expensive the more ternary operators we have)
936+
name: 'false positive: styles from ternary operators, if different properties are defined',
937+
code: `
938+
import { css } from '${imp}';
939+
const oldStyles = css({
940+
fontWeight: '...',
813941
});
814-
export const EmphasisText = ({ children, appearance }) => <span css={styles[appearance]}>{children}</span>;
942+
const newStyles = css({
943+
font: '...',
944+
});
945+
const someCondition = true;
946+
export const EmphasisText = ({ children }) => <span css={[someCondition ? oldStyles : newStyles]}>{children}</span>;
815947
`,
816948
errors: [{ messageId: 'shorthand-first' }, { messageId: 'shorthand-first' }],
817949
},
950+
951+
//
952+
// known false positives for cssMap
953+
//
954+
818955
{
819-
// this is a false positive, but making an exception for this would involve
820-
// some extra logic... maybe we can revisit this if it becomes a common situation.
821-
name: `false positive, if depth in correct order for shorthand properties in the same bucket for cssMap AND if key not static (${imp})`,
956+
name: `false positive: shorthands in different selectors for cssMap if key not static (${imp})`,
822957
code: outdent`
823958
import { cssMap } from '${imp}';
824959
const styles = cssMap({
825960
warning: {
826-
borderColor: '...', // 2
827-
borderTop: '...', // 1
961+
borderTop: '...',
828962
},
829963
normal: {
830-
borderColor: '...', // 2
831-
borderTop: '...', // 1
964+
border: '...',
832965
}
833966
});
834967
export const EmphasisText = ({ children, appearance }) => <span css={styles[appearance]}>{children}</span>;

packages/eslint-plugin/src/rules/shorthand-property-sorting/index.ts

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,45 @@ const getObjectCSSProperties = (
116116
return properties;
117117
};
118118

119+
// Given two (or more) arrays of properties, concatenate them in such a way that any
120+
// repeated properties are de-duplicated.
121+
//
122+
// Nested arrays (nested selectors, pseudo-selectors, etc.) are not de-duplicated.
123+
const union = (...arrays: PropertyArray[]): PropertyArray => {
124+
const newArray = [];
125+
126+
if (arrays.length === 0) {
127+
return [];
128+
}
129+
130+
const arrayA = arrays[0];
131+
const propertiesInArrayA = new Set<string>();
132+
133+
for (const elementA of arrayA) {
134+
newArray.push(elementA);
135+
if (!Array.isArray(elementA)) {
136+
propertiesInArrayA.add(elementA.name);
137+
}
138+
}
139+
140+
for (const arrayB of arrays.slice(1)) {
141+
for (const elementB of arrayB) {
142+
if (Array.isArray(elementB)) {
143+
newArray.push(elementB);
144+
continue;
145+
}
146+
147+
if (propertiesInArrayA.has(elementB.name)) {
148+
continue;
149+
}
150+
151+
newArray.push(elementB);
152+
}
153+
}
154+
155+
return newArray;
156+
};
157+
119158
const parseCssArrayElement = (
120159
context: Rule.RuleContext,
121160
element: ArrayExpression['elements'][number]
@@ -135,10 +174,10 @@ const parseCssArrayElement = (
135174
} else if (element.type === 'ConditionalExpression') {
136175
// Covers the case:
137176
// someCondition ? someStyles : someOtherStyles
138-
return [
139-
...parseCssArrayElement(context, element.consequent),
140-
...parseCssArrayElement(context, element.alternate),
141-
];
177+
return union(
178+
parseCssArrayElement(context, element.consequent),
179+
parseCssArrayElement(context, element.alternate)
180+
);
142181
} else if (element.type === 'MemberExpression' && element.object.type === 'Identifier') {
143182
// Covers cssMap usages
144183
functionCall = getVariableDefinition(context, element.object);
@@ -275,7 +314,7 @@ const parseCssMap = (
275314
context: Rule.RuleContext,
276315
{ node, key }: { node: CallExpression; key?: string | Literal['value'] }
277316
): PropertyArray => {
278-
const properties: PropertyArray = [];
317+
const properties: PropertyArray[] = [];
279318
const { references } = context.sourceCode.getScope(node);
280319
if (!isCssMap(node.callee as Rule.Node, references)) {
281320
return [];
@@ -303,26 +342,24 @@ const parseCssMap = (
303342
// If we know what key in the cssMap function call to traverse,
304343
// we can make sure we only traverse that.
305344
if (property.key.type === 'Literal' && key === property.key.value) {
306-
properties.push(...getObjectCSSProperties(context, property.value));
307-
break;
345+
return getObjectCSSProperties(context, property.value);
308346
} else if (property.key.type === 'Identifier' && key === property.key.name) {
309-
properties.push(...getObjectCSSProperties(context, property.value));
310-
break;
347+
return getObjectCSSProperties(context, property.value);
311348
}
312-
} else {
313-
// We cannot determine which key in the cssMap function call to traverse,
314-
// so we have no choice but to unconditionally traverse through the whole
315-
// cssMap object.
316-
//
317-
// Not very performant and can give false positives, but considering that
318-
// the cssMap key can be dynamic, we at least avoid any false negatives.
319-
//
320-
// (https://compiledcssinjs.com/docs/api-cssmap#dynamic-declarations)
321-
properties.push(...getObjectCSSProperties(context, property.value));
322349
}
350+
351+
// We cannot determine which key in the cssMap function call to traverse,
352+
// so we have no choice but to unconditionally traverse through the whole
353+
// cssMap object.
354+
//
355+
// Not very performant and can give false positives, but considering that
356+
// the cssMap key can be dynamic, we at least avoid any false negatives.
357+
//
358+
// (https://compiledcssinjs.com/docs/api-cssmap#dynamic-declarations)
359+
properties.push(getObjectCSSProperties(context, property.value));
323360
}
324361

325-
return properties;
362+
return union(...properties);
326363
};
327364

328365
const parseStyled = (context: Rule.RuleContext, node: CallExpression): PropertyArray => {

0 commit comments

Comments
 (0)