diff --git a/configs/eslint-config-compass/index.js b/configs/eslint-config-compass/index.js index 628e29c1ea2..afcf8079e8f 100644 --- a/configs/eslint-config-compass/index.js +++ b/configs/eslint-config-compass/index.js @@ -98,6 +98,7 @@ module.exports = { plugins: [...shared.plugins, '@mongodb-js/compass', 'chai-friendly'], rules: { ...shared.rules, + '@mongodb-js/compass/no-inline-emotion-css': 'warn', '@mongodb-js/compass/no-leafygreen-outside-compass-components': 'error', '@mongodb-js/compass/unique-mongodb-log-id': [ 'error', diff --git a/configs/eslint-plugin-compass/index.js b/configs/eslint-plugin-compass/index.js index c267f46ea2c..8b9b54c8e95 100644 --- a/configs/eslint-plugin-compass/index.js +++ b/configs/eslint-plugin-compass/index.js @@ -1,6 +1,7 @@ 'use strict'; module.exports = { rules: { + 'no-inline-emotion-css': require('./rules/no-inline-emotion-css'), 'no-leafygreen-outside-compass-components': require('./rules/no-leafygreen-outside-compass-components'), 'unique-mongodb-log-id': require('./rules/unique-mongodb-log-id'), }, diff --git a/configs/eslint-plugin-compass/rules/no-inline-emotion-css.js b/configs/eslint-plugin-compass/rules/no-inline-emotion-css.js new file mode 100644 index 00000000000..7fa4f3e2f15 --- /dev/null +++ b/configs/eslint-plugin-compass/rules/no-inline-emotion-css.js @@ -0,0 +1,84 @@ +'use strict'; + +/** + * Checks if a node is a css() call from emotion. + * @param {Object} node - AST node to check. + * @returns {boolean} - Whether the node is a css() call. + */ +function isCssCall(node) { + return ( + node && + node.type === 'CallExpression' && + node.callee && + node.callee.type === 'Identifier' && + node.callee.name === 'css' + ); +} + +/** + * Checks if a call is inside a react function. + * This only checks for JSXExpressionContainers or an uppercase function name, + * so it may miss some cases. + * @param {Object} context - ESLint context. + * @returns {boolean} - Whether we're inside a function. + */ +function isInsideReactFunction(context) { + const ancestors = context.getAncestors(); + + const hasJSXAncestor = ancestors.some( + (ancestor) => ancestor.type === 'JSXExpressionContainer' + ); + + if (hasJSXAncestor) { + return true; + } + + const currentFunction = ancestors.find( + (ancestor) => + ancestor.type === 'FunctionDeclaration' || + ancestor.type === 'FunctionExpression' || + ancestor.type === 'ArrowFunctionExpression' + ); + if (currentFunction) { + // If the function name starts with an uppercase letter maybe it's a React component. + if ( + currentFunction.type === 'FunctionDeclaration' && + currentFunction.id && + /^[A-Z]/.test(currentFunction.id.name) + ) { + return true; + } + } +} + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Disallow dynamic emotion css() calls in render methods', + }, + messages: { + noInlineCSS: + "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.", + }, + }, + + create(context) { + return { + // Check for dynamic css() calls in react rendering. + CallExpression(node) { + if (!isCssCall(node)) { + return; + } + + if (isInsideReactFunction(context)) { + context.report({ + node, + messageId: 'noInlineCSS', + }); + } + }, + }; + }, +}; diff --git a/configs/eslint-plugin-compass/rules/no-inline-emotion-css.test.js b/configs/eslint-plugin-compass/rules/no-inline-emotion-css.test.js new file mode 100644 index 00000000000..7c5330628ee --- /dev/null +++ b/configs/eslint-plugin-compass/rules/no-inline-emotion-css.test.js @@ -0,0 +1,51 @@ +'use strict'; +const { RuleTester } = require('eslint'); +const rule = require('./no-inline-emotion-css'); + +const ruleTester = new RuleTester(); + +ruleTester.run('no-inline-emotion-css', rule, { + valid: [ + { + code: "const staticSet = css({ background: 'orange' });", + parserOptions: { ecmaVersion: 2021 }, + }, + { + code: ` +const pineappleStyles = css({ background: 'purple' }); +function pineapple() { return pineappleStyles; };`, + parserOptions: { ecmaVersion: 2021 }, + }, + { + code: ` +const pineappleStyles = css({ background: 'purple' }); +function Pineapple() { return (