@atlaskit/eslint-plugin-ui-styling-standard
Version:
The eslint plugin to enforce and educate on Atlassian's UI Styling Standard
316 lines (302 loc) • 13.3 kB
JavaScript
import { isNodeOfType } from 'eslint-codemod-utils';
import { getAllowedFunctionCalls, isAllowListedVariable } from '@atlaskit/eslint-utils/allowed-function-calls';
import { getSourceCode } from '@atlaskit/eslint-utils/context-compat';
import { findVariable } from '@atlaskit/eslint-utils/find-variable';
import { createLintRule } from '../utils/create-rule';
const isIdentifierAllowed = (context, identifier) => {
var _variable$identifiers, _variable$identifiers2;
const variable = findVariable({
sourceCode: 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 ? void 0 : (_variable$identifiers2 = _variable$identifiers.parent) === null || _variable$identifiers2 === void 0 ? void 0 : _variable$identifiers2.type) === 'RestElement') {
return false;
}
const 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;
}
const 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)
let 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') {
const variable = findVariable({
sourceCode: getSourceCode(context),
identifier: property.value.callee
});
// Anything returned by getAllowedFunctionCalls should be used in css prop instead
const functionAllowList = getAllowedFunctionCalls(context.options);
if (!variable || isAllowListedVariable({
allowList: functionAllowList,
variable
})) {
return false;
}
} else if (property.value.type === 'MemberExpression' && property.value.object.type === 'Identifier') {
var _findVariable;
const definitions = (_findVariable = findVariable({
sourceCode: getSourceCode(context),
identifier: property.value.object
})) === null || _findVariable === void 0 ? void 0 : _findVariable.defs;
if (!definitions || !definitions.length) {
return false;
}
const variable = definitions[0];
if (!variable) {
return false;
}
const varType = variable.type;
if (varType === 'Variable') {
if (!variable.node.init) {
return false;
}
const 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) {
const objectPattern = property.parent;
if (!isNodeOfType(objectPattern, 'ObjectPattern')) {
return false;
}
return isNodeOfType(objectPattern.parent, 'VariableDeclarator') || isNodeOfType(objectPattern.parent, 'FunctionDeclaration') || isNodeOfType(objectPattern.parent, 'FunctionExpression') || isNodeOfType(objectPattern.parent, 'ArrowFunctionExpression');
}
function isAllowedName(name) {
return name.name === 'style';
}
function isAllowedDefinition(definition) {
const name = definition.name;
const {
parent
} = name;
if (!isNodeOfType(parent, 'Property')) {
return false;
}
if (!isNodeOfType(parent.key, 'Identifier') || !isNodeOfType(parent.value, 'Identifier') || !isAllowedName(parent.key) || !isAllowedName(parent.value)) {
return false;
}
if (!isShallowProperty(parent)) {
return false;
}
return true;
}
function isFirstParameter(identifier) {
if (!isNodeOfType(identifier.parent, 'FunctionDeclaration') && !isNodeOfType(identifier.parent, 'ArrowFunctionExpression') && !isNodeOfType(identifier.parent, 'FunctionExpression')) {
return false;
}
return identifier.parent.params[0] === identifier;
}
const rule = 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(context) {
return {
'JSXAttribute[name.name="style"] > JSXExpressionContainer': node => {
var _node$expression$expr, _expression;
if (!isNodeOfType(node, 'JSXExpressionContainer')) {
return;
}
// we've reached an attribute named style
let expression = null;
let 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, _variable$defs2$, _variable$defs2$$node;
/*
* Example:
* ```tsx
* const style = { width: 100 };
* <div style={style} />
* ```
*/
const variable = findVariable({
sourceCode: getSourceCode(context),
identifier: node.expression
});
const definition = variable === null || variable === void 0 ? 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') {
const sourceVariable = findVariable({
identifier: definition.node.init,
sourceCode: getSourceCode(context)
});
const 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…
const hopefulObjectExpression = variable === null || variable === void 0 ? void 0 : (_variable$defs2 = variable.defs) === null || _variable$defs2 === void 0 ? void 0 : (_variable$defs2$ = _variable$defs2[0]) === null || _variable$defs2$ === void 0 ? void 0 : (_variable$defs2$$node = _variable$defs2$.node) === null || _variable$defs2$$node === void 0 ? void 0 : _variable$defs2$$node.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 _variable$defs3;
const variable = findVariable({
sourceCode: getSourceCode(context),
identifier: node.expression.object
});
const definition = variable === null || variable === void 0 ? void 0 : (_variable$defs3 = variable.defs) === null || _variable$defs3 === void 0 ? void 0 : _variable$defs3[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(value => !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(value => {
if (!isPropertyValueAllowed(value, context)) {
context.report({
node: value,
messageId: 'enforce-style-prop'
});
}
});
}
}
};
}
});
export default rule;