UNPKG

@atlaskit/eslint-plugin-ui-styling-standard

Version:

The eslint plugin to enforce and educate on Atlassian's UI Styling Standard

322 lines (307 loc) • 14.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _eslintCodemodUtils = require("eslint-codemod-utils"); var _allowedFunctionCalls = require("@atlaskit/eslint-utils/allowed-function-calls"); var _contextCompat = require("@atlaskit/eslint-utils/context-compat"); var _findVariable2 = require("@atlaskit/eslint-utils/find-variable"); var _createRule = require("../utils/create-rule"); var _isIdentifierAllowed = function isIdentifierAllowed(context, identifier) { var _variable$identifiers; var variable = (0, _findVariable2.findVariable)({ sourceCode: (0, _contextCompat.getSourceCode)(context), identifier: identifier }); if (!variable) { return false; } // If identifier is a RestElement, report warning if (((_variable$identifiers = variable.identifiers[0]) === null || _variable$identifiers === void 0 || (_variable$identifiers = _variable$identifiers.parent) === null || _variable$identifiers === void 0 ? void 0 : _variable$identifiers.type) === 'RestElement') { return false; } var firstDef = variable.defs[0]; if (firstDef.type === 'Parameter') { // This ultimately comes from a function parameter, eg. `const value = props.value` // ultimately finds this `function (props)` or `function({ width })` and those are parameters return true; } if (firstDef.type !== 'Variable') { // If this isn't a variable, eg. not `const value = props.value`, it's not an identifier we can track return false; } var variableDefinition = firstDef.node; if (variableDefinition.type !== 'VariableDeclarator') { return false; } // Checking if identifier has been destructured from props within function // If it's a `let var = …`, ignore, we assume it's dynamically set (but it could be static) if ('kind' in variableDefinition.parent && variableDefinition.parent.kind === 'let') { return true; } // Allow for `const { width } = props as Props` (etc) var init = variableDefinition.init; // This is not typed for TS types if (init.type === 'TSAsExpression' && 'expression' in init && typeof init.expression !== 'boolean') { init = init.expression; } // If it's initialised by a call expression, ignore if (init.type === 'CallExpression') { return true; } if (init.type === 'BinaryExpression') { // If it's initialized like `const var = 1 * props.value`, validate either side if (init.left.type === 'MemberExpression') { return _isIdentifierAllowed(context, init.left.object); } if (init.right.type === 'MemberExpression') { return _isIdentifierAllowed(context, init.right.object); } } if (init.type === 'ConditionalExpression') { // If it's initialized like `const var = props.value ? 42 : 1`, validate either side if (init.test.type === 'MemberExpression') { return _isIdentifierAllowed(context, init.test.object); } if (init.consequent.type === 'MemberExpression') { return _isIdentifierAllowed(context, init.consequent.object); } if (init.alternate.type === 'MemberExpression') { return _isIdentifierAllowed(context, init.alternate.object); } } // Destructured from member expression // e.g. const width = props.width if (init.type === 'MemberExpression') { return _isIdentifierAllowed(context, init.object); } // Destructured from object pattern // e.g. const { width } = props return _isIdentifierAllowed(context, init); }; function isPropertyValueAllowed(property, context) { if (property.type === 'SpreadElement') { return false; } // property.type === 'Property', moving on to check if value is static if (property.value.type === 'Literal') { return false; } else if (property.value.type === 'Identifier') { return _isIdentifierAllowed(context, property.value); } else if (property.value.type === 'CallExpression' && property.value.callee.type === 'Identifier') { var variable = (0, _findVariable2.findVariable)({ sourceCode: (0, _contextCompat.getSourceCode)(context), identifier: property.value.callee }); // Anything returned by getAllowedFunctionCalls should be used in css prop instead var functionAllowList = (0, _allowedFunctionCalls.getAllowedFunctionCalls)(context.options); if (!variable || (0, _allowedFunctionCalls.isAllowListedVariable)({ allowList: functionAllowList, variable: variable })) { return false; } } else if (property.value.type === 'MemberExpression' && property.value.object.type === 'Identifier') { var _findVariable; var definitions = (_findVariable = (0, _findVariable2.findVariable)({ sourceCode: (0, _contextCompat.getSourceCode)(context), identifier: property.value.object })) === null || _findVariable === void 0 ? void 0 : _findVariable.defs; if (!definitions || !definitions.length) { return false; } var _variable = definitions[0]; if (!_variable) { return false; } var varType = _variable.type; if (varType === 'Variable') { if (!_variable.node.init) { return false; } var varSource = _variable.node.init.type; // Variables initialised by a call expression or object expression are ok if (varSource === 'CallExpression' || varSource === 'ObjectExpression') { return true; } // Check if member expression matches with function argument } else if (varType !== 'Parameter') { return false; } } return true; } /** * Returns true if the property is not nested within another property. */ function isShallowProperty(property) { var objectPattern = property.parent; if (!(0, _eslintCodemodUtils.isNodeOfType)(objectPattern, 'ObjectPattern')) { return false; } return (0, _eslintCodemodUtils.isNodeOfType)(objectPattern.parent, 'VariableDeclarator') || (0, _eslintCodemodUtils.isNodeOfType)(objectPattern.parent, 'FunctionDeclaration') || (0, _eslintCodemodUtils.isNodeOfType)(objectPattern.parent, 'FunctionExpression') || (0, _eslintCodemodUtils.isNodeOfType)(objectPattern.parent, 'ArrowFunctionExpression'); } function isAllowedName(name) { return name.name === 'style'; } function isAllowedDefinition(definition) { var name = definition.name; var parent = name.parent; if (!(0, _eslintCodemodUtils.isNodeOfType)(parent, 'Property')) { return false; } if (!(0, _eslintCodemodUtils.isNodeOfType)(parent.key, 'Identifier') || !(0, _eslintCodemodUtils.isNodeOfType)(parent.value, 'Identifier') || !isAllowedName(parent.key) || !isAllowedName(parent.value)) { return false; } if (!isShallowProperty(parent)) { return false; } return true; } function isFirstParameter(identifier) { if (!(0, _eslintCodemodUtils.isNodeOfType)(identifier.parent, 'FunctionDeclaration') && !(0, _eslintCodemodUtils.isNodeOfType)(identifier.parent, 'ArrowFunctionExpression') && !(0, _eslintCodemodUtils.isNodeOfType)(identifier.parent, 'FunctionExpression')) { return false; } return identifier.parent.params[0] === identifier; } var rule = (0, _createRule.createLintRule)({ meta: { name: 'enforce-style-prop', docs: { description: 'Disallows usage of static styles in the `style` attribute in components', recommended: true, severity: 'error' }, messages: { 'enforce-style-prop': "Move static style values into module-scoped cssMap() or css() declarations and apply them with css or xcss; keep only runtime values in style." }, type: 'problem' }, create: function create(context) { return { 'JSXAttribute[name.name="style"] > JSXExpressionContainer': function JSXAttributeNameNameStyle__JSXExpressionContainer(node) { var _node$expression$expr, _expression; if (!(0, _eslintCodemodUtils.isNodeOfType)(node, 'JSXExpressionContainer')) { return; } // we've reached an attribute named style var expression = null; var reportOnNodeExpression = false; if ( // @ts-expect-error -- TSAsExpression is not valid in estree node.expression.type === 'TSAsExpression' && // @ts-expect-error -- TSAsExpression is not valid in estree ((_node$expression$expr = node.expression.expression) === null || _node$expression$expr === void 0 ? void 0 : _node$expression$expr.type) === 'ObjectExpression') { // This is a `style={{ … } as React.CSSProperties}` scenario // @ts-expect-error -- TSAsExpression is not valid in estree expression = node.expression.expression; } else if (node.expression.type === 'ObjectExpression') { // Typical `style={{ … }}` scenario expression = node.expression; } else if (node.expression.type === 'Identifier') { var _variable$defs, _definition$node$init, _variable$defs2; /* * Example: * ```tsx * const style = { width: 100 }; * <div style={style} /> * ``` */ var variable = (0, _findVariable2.findVariable)({ sourceCode: (0, _contextCompat.getSourceCode)(context), identifier: node.expression }); var definition = variable === null || variable === void 0 || (_variable$defs = variable.defs) === null || _variable$defs === void 0 ? void 0 : _variable$defs[0]; /** * Allow passing a function parameter directly to the style prop * * @example * function Component({ style }) { return <div style={style} />; } * * @example * <Popper> * {({ ref, style }) => <div ref={ref} style={style} />} * </Popper> */ if ((definition === null || definition === void 0 ? void 0 : definition.type) === 'Parameter' && isAllowedDefinition(definition)) { return; } /** * Allow passing a value destructured in the body from a function parameter to the style prop * * @example * function Component(props) { * const { style } = props; * return <div style={style} />; * } */ if ((definition === null || definition === void 0 ? void 0 : definition.type) === 'Variable' && ((_definition$node$init = definition.node.init) === null || _definition$node$init === void 0 ? void 0 : _definition$node$init.type) === 'Identifier') { var sourceVariable = (0, _findVariable2.findVariable)({ identifier: definition.node.init, sourceCode: (0, _contextCompat.getSourceCode)(context) }); var sourceDefinition = sourceVariable === null || sourceVariable === void 0 ? void 0 : sourceVariable.defs[0]; if ((sourceDefinition === null || sourceDefinition === void 0 ? void 0 : sourceDefinition.type) === 'Parameter' && isAllowedDefinition(definition)) { return; } } // Types are alluding us here, but we really only want an ObjectExpression here // Additionally, if we have multiple variable definitions, we're in some unhandled scenario // so we're only going to look at the first one… var hopefulObjectExpression = variable === null || variable === void 0 || (_variable$defs2 = variable.defs) === null || _variable$defs2 === void 0 || (_variable$defs2 = _variable$defs2[0]) === null || _variable$defs2 === void 0 || (_variable$defs2 = _variable$defs2.node) === null || _variable$defs2 === void 0 ? void 0 : _variable$defs2.init; if ((hopefulObjectExpression === null || hopefulObjectExpression === void 0 ? void 0 : hopefulObjectExpression.type) === 'ObjectExpression') { expression = hopefulObjectExpression; } reportOnNodeExpression = true; } else if (node.expression.type === 'MemberExpression') { /** * Allow passing `props.style` to the style prop */ if (node.expression.object.type === 'Identifier' && node.expression.property.type === 'Identifier' && isAllowedName(node.expression.property)) { var _variable2$defs; var _variable2 = (0, _findVariable2.findVariable)({ sourceCode: (0, _contextCompat.getSourceCode)(context), identifier: node.expression.object }); var _definition = _variable2 === null || _variable2 === void 0 || (_variable2$defs = _variable2.defs) === null || _variable2$defs === void 0 ? void 0 : _variable2$defs[0]; if ((_definition === null || _definition === void 0 ? void 0 : _definition.type) === 'Parameter' && isFirstParameter(_definition.name)) { return; } } } if (!((_expression = expression) !== null && _expression !== void 0 && _expression.properties)) { context.report({ node: node, messageId: 'enforce-style-prop' }); return; } if (reportOnNodeExpression) { // In some cases we want to report on the `node.expression` itself, eg. // `styles` in `style={styles}` should get the error if (expression.properties.some(function (value) { return !isPropertyValueAllowed(value, context); })) { context.report({ node: node.expression, messageId: 'enforce-style-prop' }); } } else { // Typically we want to report on the individual styles, eg. // `color: 'red'` in `style={{ color: 'red' }}` should get the error expression.properties.forEach(function (value) { if (!isPropertyValueAllowed(value, context)) { context.report({ node: value, messageId: 'enforce-style-prop' }); } }); } } }; } }); var _default = exports.default = rule;