Skip to content

Commit c6da42a

Browse files
authored
chore(eslint-config): add dynamic css call lint rule COMPASS-9843 (#7314)
1 parent a9644ab commit c6da42a

File tree

7 files changed

+146
-13
lines changed

7 files changed

+146
-13
lines changed

configs/eslint-config-compass/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ module.exports = {
9898
plugins: [...shared.plugins, '@mongodb-js/compass', 'chai-friendly'],
9999
rules: {
100100
...shared.rules,
101+
'@mongodb-js/compass/no-inline-emotion-css': 'warn',
101102
'@mongodb-js/compass/no-leafygreen-outside-compass-components': 'error',
102103
'@mongodb-js/compass/unique-mongodb-log-id': [
103104
'error',

configs/eslint-plugin-compass/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22
module.exports = {
33
rules: {
4+
'no-inline-emotion-css': require('./rules/no-inline-emotion-css'),
45
'no-leafygreen-outside-compass-components': require('./rules/no-leafygreen-outside-compass-components'),
56
'unique-mongodb-log-id': require('./rules/unique-mongodb-log-id'),
67
},
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
'use strict';
2+
3+
/**
4+
* Checks if a node is a css() call from emotion.
5+
* @param {Object} node - AST node to check.
6+
* @returns {boolean} - Whether the node is a css() call.
7+
*/
8+
function isCssCall(node) {
9+
return (
10+
node &&
11+
node.type === 'CallExpression' &&
12+
node.callee &&
13+
node.callee.type === 'Identifier' &&
14+
node.callee.name === 'css'
15+
);
16+
}
17+
18+
/**
19+
* Checks if a call is inside a react function.
20+
* This only checks for JSXExpressionContainers or an uppercase function name,
21+
* so it may miss some cases.
22+
* @param {Object} context - ESLint context.
23+
* @returns {boolean} - Whether we're inside a function.
24+
*/
25+
function isInsideReactFunction(context) {
26+
const ancestors = context.getAncestors();
27+
28+
const hasJSXAncestor = ancestors.some(
29+
(ancestor) => ancestor.type === 'JSXExpressionContainer'
30+
);
31+
32+
if (hasJSXAncestor) {
33+
return true;
34+
}
35+
36+
const currentFunction = ancestors.find(
37+
(ancestor) =>
38+
ancestor.type === 'FunctionDeclaration' ||
39+
ancestor.type === 'FunctionExpression' ||
40+
ancestor.type === 'ArrowFunctionExpression'
41+
);
42+
if (currentFunction) {
43+
// If the function name starts with an uppercase letter maybe it's a React component.
44+
if (
45+
currentFunction.type === 'FunctionDeclaration' &&
46+
currentFunction.id &&
47+
/^[A-Z]/.test(currentFunction.id.name)
48+
) {
49+
return true;
50+
}
51+
}
52+
}
53+
54+
/** @type {import('eslint').Rule.RuleModule} */
55+
module.exports = {
56+
meta: {
57+
type: 'problem',
58+
docs: {
59+
description: 'Disallow dynamic emotion css() calls in render methods',
60+
},
61+
messages: {
62+
noInlineCSS:
63+
"Don't use a dynamic css() call in the render method, this creates a new class name every time component updates and is not performant. Static styles can be defined with css outside of render, dynamic should be passed through the style prop.",
64+
},
65+
},
66+
67+
create(context) {
68+
return {
69+
// Check for dynamic css() calls in react rendering.
70+
CallExpression(node) {
71+
if (!isCssCall(node)) {
72+
return;
73+
}
74+
75+
if (isInsideReactFunction(context)) {
76+
context.report({
77+
node,
78+
messageId: 'noInlineCSS',
79+
});
80+
}
81+
},
82+
};
83+
},
84+
};
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use strict';
2+
const { RuleTester } = require('eslint');
3+
const rule = require('./no-inline-emotion-css');
4+
5+
const ruleTester = new RuleTester();
6+
7+
ruleTester.run('no-inline-emotion-css', rule, {
8+
valid: [
9+
{
10+
code: "const staticSet = css({ background: 'orange' });",
11+
parserOptions: { ecmaVersion: 2021 },
12+
},
13+
{
14+
code: `
15+
const pineappleStyles = css({ background: 'purple' });
16+
function pineapple() { return pineappleStyles; };`,
17+
parserOptions: { ecmaVersion: 2021 },
18+
},
19+
{
20+
code: `
21+
const pineappleStyles = css({ background: 'purple' });
22+
function Pineapple() { return (<div className={pineappleStyles}>pineapples</div>); }`,
23+
parserOptions: { ecmaVersion: 2021, ecmaFeatures: { jsx: true } },
24+
},
25+
{
26+
code: "function pineapple() { const dynamicSet = css({ background: 'orange' }); }",
27+
parserOptions: { ecmaVersion: 2021 },
28+
},
29+
],
30+
invalid: [
31+
{
32+
code: `
33+
function Pineapple() {
34+
const pineappleStyles = css({ background: 'purple' });
35+
return (<div className={pineappleStyles}>pineapples</div>);
36+
}`,
37+
parserOptions: { ecmaVersion: 2021, ecmaFeatures: { jsx: true } },
38+
errors: [
39+
"Don't use a dynamic css() call in the render method, this creates a new class name every time component updates and is not performant. Static styles can be defined with css outside of render, dynamic should be passed through the style prop.",
40+
],
41+
},
42+
{
43+
code: "function Pineapple() { return (<div className={css({ background: 'purple' })}>pineapples</div>); }",
44+
parserOptions: { ecmaVersion: 2021, ecmaFeatures: { jsx: true } },
45+
46+
errors: [
47+
"Don't use a dynamic css() call in the render method, this creates a new class name every time component updates and is not performant. Static styles can be defined with css outside of render, dynamic should be passed through the style prop.",
48+
],
49+
},
50+
],
51+
});

configs/eslint-plugin-compass/rules/unique-mongodb-log-id.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const testOptions = {
1717
],
1818
};
1919

20-
ruleTester.run('no-leafygreen-outside-compass-components', rule, {
20+
ruleTester.run('unique-mongodb-log-id', rule, {
2121
valid: [
2222
{
2323
code: 'mongoLogId(10);',

packages/compass-aggregations/src/components/aggregation-side-panel/stage-wizard-use-cases/sort/sort.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
css,
77
ListEditor,
88
} from '@mongodb-js/compass-components';
9-
import React, { useMemo, useState } from 'react';
9+
import React, { useState } from 'react';
1010
import {
1111
SORT_DIRECTION_OPTIONS,
1212
getNextId,
@@ -47,6 +47,10 @@ const sortDirectionStyles = css({
4747
width: '150px',
4848
});
4949

50+
const comboboxStyles = css({
51+
minWidth: '200px',
52+
});
53+
5054
const mapSortFormDataToStageValue = (
5155
formData: SortFieldState[]
5256
): Record<string, number> => {
@@ -159,14 +163,6 @@ export const SortForm = ({ fields, onChange }: WizardComponentProps) => {
159163
onSetFormData(newData);
160164
};
161165

162-
const comboboxClassName = useMemo(() => {
163-
return css({
164-
width: `calc(${String(
165-
Math.max(...fields.map(({ name }) => name.length), 10)
166-
)}ch)`,
167-
});
168-
}, [fields]);
169-
170166
return (
171167
<div className={containerStyles}>
172168
<ListEditor
@@ -178,7 +174,7 @@ export const SortForm = ({ fields, onChange }: WizardComponentProps) => {
178174
renderItem={(item, index) => {
179175
return (
180176
<SortFormGroup
181-
comboboxClassName={comboboxClassName}
177+
comboboxClassName={comboboxStyles}
182178
index={index}
183179
sortField={item.field}
184180
sortDirection={item.direction}

packages/compass-aggregations/src/components/search-no-results.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ export default function SearchNoResults() {
3333
return (
3434
<div className={centeredContent}>
3535
<Subtitle
36-
className={css(
36+
className={
3737
darkMode ? missingAtlasIndexDarkStyles : missingAtlasIndexLightStyles
38-
)}
38+
}
3939
>
4040
No preview documents
4141
</Subtitle>

0 commit comments

Comments
 (0)