UNPKG

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

Version:

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

420 lines (413 loc) • 16.7 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _eslintCodemodUtils = require("eslint-codemod-utils"); var _eslintScope = require("eslint-scope"); var _estraverse = _interopRequireDefault(require("estraverse")); var _contextCompat = require("@atlaskit/eslint-utils/context-compat"); var _isSupportedImport = require("@atlaskit/eslint-utils/is-supported-import"); var _createRule = require("../utils/create-rule"); function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } /** * Represents some information about single line that contains some props, e.g. * the arrow function in * * color: (props) => `mix(${props.myColor}, #fff)` * * or about a destructuring assignment, e.g. the arrow function in * * color: ({ myColor, otherColor }) => someFunction(myColor, otherColor) * * or something that we don't recognise, or that we don't think is valid * * color: (props, someOtherVariable) => someOtherVariable */ var standardizePropExpression = function standardizePropExpression(sourceCode, expression) { var propsObjectName = 'props'; if (expression.type === 'Identifier') { return "".concat(propsObjectName, ".").concat(expression.name); } if (expression.type === 'MemberExpression' && expression.property.type === 'Identifier') { var propertyName = sourceCode.getText(expression.property); return "".concat(propsObjectName, ".").concat(propertyName); } // Cannot safely process the expression, bail out! return undefined; }; var parseArrowFunctionExpression = function parseArrowFunctionExpression(expression, sourceCode) { if (expression.params.length !== 1) { return { valid: false }; } var scopeManager = (0, _eslintScope.analyze)(expression, {}); var currentScope = scopeManager.acquire(expression); if (!currentScope) { return { valid: false }; } var parameter = expression.params[0]; var props = []; // Don't bother handling type annotations that appear in the parameters of arrow functions. // // This only applies if the ESLint rule is running under @typescript-eslint (not regular eslint), hence the roundabout way we use to check whether parameter.typeAnnotation exists or not. if ('typeAnnotation' in parameter && parameter.typeAnnotation) { return { valid: false }; } if (parameter.type === 'ObjectPattern') { if (parameter.properties.some(function (value) { return value.type === 'RestElement'; })) { return { valid: false }; } var properties = parameter.properties; var _iterator = _createForOfIteratorHelper(properties), _step; try { var _loop = function _loop() { var property = _step.value; var propertyValue = property.value; // Don't bother supporting other patterns like rest elements, default values... if (propertyValue.type !== 'Identifier') { return { v: { valid: false } }; } var variable = currentScope.variables.find(function (varb) { return varb.name === propertyValue.name; }); if (!variable || !variable.references || !variable.references.length) { // Something went wrong, best to just bail out. return { v: { valid: false } }; } var propName = standardizePropExpression(sourceCode, propertyValue); if (!propName) { return { v: { valid: false } }; } props.push({ propName: propName, usages: variable.references.map(function (reference) { return reference.identifier; }) }); }, _ret; for (_iterator.s(); !(_step = _iterator.n()).done;) { _ret = _loop(); if (_ret) return _ret.v; } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } } else if (parameter.type === 'Identifier') { var variable = currentScope.variables.find(function (varb) { return varb.name === parameter.name; }); if (!variable || !variable.references || !variable.references.length) { // Something went wrong, best to just bail out. return { valid: false }; } var _iterator2 = _createForOfIteratorHelper(variable.references), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var reference = _step2.value; var memberExpression = reference.identifier.parent; if (memberExpression.type !== 'MemberExpression') { // If reference.identifier.parent is not a member expression, that indicates // that we have a usage of the props argument that does NOT look like // // (props) => props.color // // e.g. we might have (props) => props['...'] or (props) => `${props}px` perhaps... // // We have no choice but to bail out because the syntax is not what we expect. return { valid: false }; } var propName = standardizePropExpression(sourceCode, memberExpression); if (!propName) { return { valid: false }; } props.push({ propName: propName, usages: [memberExpression] }); } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } } else { // Bail out for any syntax we don't recognise. return { valid: false }; } return { arrowFunctionExpression: expression, props: props, valid: true }; }; var lintArguments = function lintArguments(context, args) { var _iterator3 = _createForOfIteratorHelper(args), _step3; try { var _loop2 = function _loop2() { var argument = _step3.value; if (argument.type === 'ArrowFunctionExpression') { // styled.div((props) => ({ ... })) // styled.div(({ color, height }) => ({ ... })) // // Already in the form we want. return 0; // continue } else if (argument.type === 'ObjectExpression') { var allProps = []; // vvvvvvv // styled.div({ ... }) var _iterator4 = _createForOfIteratorHelper(argument.properties), _step4; try { for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { var property = _step4.value; if (property.type === 'SpreadElement') { // e.g. // styled.div({ color: 'blue', ...someStyles }) // // If we have a spread element, we can no longer safely // transform this styled/css call without a lot more effort. // // Best to give up... return { v: void 0 }; } else { _estraverse.default.traverse(property.value, { enter: function enter(node, _parent) { if (node.type === 'ArrowFunctionExpression') { var propNames = parseArrowFunctionExpression(node, (0, _contextCompat.getSourceCode)(context)); if (!propNames) { // We've encountered a CSS value we don't recognise, skip. return; } allProps.push(propNames); } }, /** * This is needed to handle unknown node types. Otherwise an error is thrown. */ fallback: 'iteration' }); } } } catch (err) { _iterator4.e(err); } finally { _iterator4.f(); } if (!allProps.length) { return 0; // continue } if (allProps.some(function (propUsage) { return !propUsage.valid; })) { context.report({ node: argument, messageId: 'unsupported-prop-syntax-no-autofixer' }); return { v: void 0 }; } context.report({ node: argument, messageId: 'unsupported-prop-syntax', fix: function fix(fixer) { var sourceCode = (0, _contextCompat.getSourceCode)(context); return fixPropsObjectUsages(fixer, sourceCode, argument, // The // allProps.some((propUsage) => !propUsage.valid) // condition above should have made this type assertion unnecessary... allProps); } }); } }, _ret2; for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { _ret2 = _loop2(); if (_ret2 === 0) continue; if (_ret2) return _ret2.v; } } catch (err) { _iterator3.e(err); } finally { _iterator3.f(); } }; var convertPropUsage = function convertPropUsage(propUsage, sourceCode) { var arrowFunctionOffset = propUsage.arrowFunctionExpression.body.loc ? { startIndex: sourceCode.getIndexFromLoc(propUsage.arrowFunctionExpression.body.loc.start), endIndex: sourceCode.getIndexFromLoc(propUsage.arrowFunctionExpression.body.loc.end) } : undefined; if (!arrowFunctionOffset) { return; } var sectionsToUpdate = []; var newCode = []; var _iterator5 = _createForOfIteratorHelper(propUsage.props), _step5; try { for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) { var prop = _step5.value; var _iterator6 = _createForOfIteratorHelper(prop.usages), _step6; try { for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) { var usage = _step6.value; if (!usage.loc) { return; } sectionsToUpdate.push({ identifier: usage, newCode: prop.propName, startIndex: sourceCode.getIndexFromLoc(usage.loc.start), endIndex: sourceCode.getIndexFromLoc(usage.loc.end) }); } } catch (err) { _iterator6.e(err); } finally { _iterator6.f(); } } } catch (err) { _iterator5.e(err); } finally { _iterator5.f(); } sectionsToUpdate.sort(function (a, b) { if (!a.identifier.loc || !b.identifier.loc) { return 0; } return a.identifier.loc.start.line - b.identifier.loc.start.line || a.identifier.loc.start.column - b.identifier.loc.start.column; }); var prevEndIndex = arrowFunctionOffset.startIndex; for (var _i = 0, _sectionsToUpdate = sectionsToUpdate; _i < _sectionsToUpdate.length; _i++) { var section = _sectionsToUpdate[_i]; if (prevEndIndex > section.startIndex || prevEndIndex >= section.endIndex) { // Props overlap! This is unexpected, bail out. return; } newCode.push(sourceCode.getText().slice(prevEndIndex, section.startIndex)); newCode.push(section.newCode); prevEndIndex = section.endIndex; } newCode.push(sourceCode.getText().slice(prevEndIndex, arrowFunctionOffset.endIndex)); return newCode.join(''); }; var fixPropsObjectUsages = function fixPropsObjectUsages(fixer, sourceCode, expression, allProps) { var fixers = [ // Wrap the object (containing the CSS properties and values) in an arrow function, // so it becomes something like // // styled.div( // (props) => ({ // color: ... // }) // ); fixer.insertTextBefore(expression, '(props) => ('), fixer.insertTextAfter(expression, ')')]; var _iterator7 = _createForOfIteratorHelper(allProps), _step7; try { for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) { var propUsage = _step7.value; var arrowFunctionExpression = propUsage.arrowFunctionExpression; var newCode = convertPropUsage(propUsage, sourceCode); if (newCode) { fixers.push(fixer.replaceText(arrowFunctionExpression, newCode)); } } } catch (err) { _iterator7.e(err); } finally { _iterator7.f(); } return fixers; }; var rule = (0, _createRule.createLintRule)({ meta: { name: 'convert-props-syntax', docs: { description: 'Convert props syntax that is unsupported by styled-components@<4 or @emotion/styled to props syntax that is supported. This is useful when used in conjunction with `no-styled-tagged-template-expression`, as output from the latter may use props syntax unsupported by those libraries.', recommended: true, severity: 'error' }, fixable: 'code', messages: { 'unsupported-prop-syntax': 'This syntax is not supported by styled-components@<4 or @emotion/styled and will fail at runtime!\n\nTo fix this, you can either migrate this to `@compiled/react`, or use the autofixer to switch the syntax from the `styled.div({ property: (props) => props.value })` syntax to the `styled.div(props => ({ property: props.value }))` syntax.', 'unsupported-prop-syntax-no-autofixer': 'This syntax is not supported by styled-components@<4 or @emotion/styled and will fail at runtime!\n\nTo fix this, you can either migrate this to `@compiled/react`, or manually migrate the syntax from the `styled.div({ property: (props) => props.value })` syntax to the `styled.div(props => ({ property: props.value }))` syntax.\n\nCheck out the rule documentation for examples.' }, type: 'problem' }, create: function create(context) { return { CallExpression: function CallExpression(node) { var _node$parent, _node$parent2; var _getScope = (0, _contextCompat.getScope)(context, node), references = _getScope.references; // Rule should ignore .attrs() calls if (node.callee.type === 'MemberExpression' && node.callee.property.type === 'Identifier' && node.callee.property.name === 'attrs') { return; } var callee = node.callee.type === 'MemberExpression' ? node.callee.object : node.callee; if (!(0, _isSupportedImport.isStyledComponents)(callee, references) && !(0, _isSupportedImport.isEmotion)(callee, references)) { return; } /** * We only want to lint the second set of arguments in the following cases: * * 1. styled(BaseComponent)({ ... }) * 2. styled(BaseComponent)`` */ if ( // Using this type assertion to cast into a more useful type // For some reason the estree types are missing useful things like `parent` (0, _eslintCodemodUtils.isNodeOfType)(node, 'CallExpression') && (((_node$parent = node.parent) === null || _node$parent === void 0 ? void 0 : _node$parent.type) === 'CallExpression' || ((_node$parent2 = node.parent) === null || _node$parent2 === void 0 ? void 0 : _node$parent2.type) === 'TaggedTemplateExpression')) { return; } lintArguments(context, node.arguments); } }; } }); var _default = exports.default = rule;