@atlaskit/eslint-plugin-ui-styling-standard
Version:
The eslint plugin to enforce and educate on Atlassian's UI Styling Standard
414 lines (407 loc) • 16.3 kB
JavaScript
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; }
import { isNodeOfType } from 'eslint-codemod-utils';
import { analyze } from 'eslint-scope';
import ESTraverse from 'estraverse';
import { getScope, getSourceCode } from '@atlaskit/eslint-utils/context-compat';
import { isEmotion, isStyledComponents } from '@atlaskit/eslint-utils/is-supported-import';
import { createLintRule } from '../utils/create-rule';
/**
* 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 = 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.traverse(property.value, {
enter: function enter(node, _parent) {
if (node.type === 'ArrowFunctionExpression') {
var propNames = parseArrowFunctionExpression(node, 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 = 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 = 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 = 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 (!isStyledComponents(callee, references) && !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`
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);
}
};
}
});
export default rule;