@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
JavaScript
"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;