Skip to content

Commit

Permalink
Statically evaluate mathematical binary expressions (#1139)
Browse files Browse the repository at this point in the history
  • Loading branch information
at-nathan committed Mar 16, 2022
1 parent 08c588d commit 73821f2
Show file tree
Hide file tree
Showing 18 changed files with 242 additions and 111 deletions.
5 changes: 5 additions & 0 deletions .changeset/selfish-zoos-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@compiled/babel-plugin': minor
---

Statically evaluate mathematical binary expressions
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 0 additions & 21 deletions packages/babel-plugin/src/__tests__/css-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,25 +51,4 @@ describe('css builder', () => {
"
`);
});

it('calculates a negative variable separately from a positive variable of the same value', () => {
const actual = transform(`
import { styled } from '@compiled/react';
const size = () => 8
const gridSize = size();
const LayoutRight = styled.aside\`
margin-right: -\${gridSize * 5}px;
margin-left: \${gridSize * 5}px;
\`;
<LayoutRight>Layout Right</LayoutRight>;
`);

expect(actual).toIncludeMultiple([
'margin-left:var(--_1l3fmvo)',
'margin-right:var(--_1cakqv5)',
'"--_1cakqv5": ix(-gridSize * 5, "px")',
'"--_1l3fmvo": ix(gridSize * 5, "px")',
'ax(["_2hwxsxb8 _18u01xn1", props.className]',
]);
});
});
83 changes: 83 additions & 0 deletions packages/babel-plugin/src/__tests__/expression-evaluation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,4 +360,87 @@ describe('import specifiers', () => {
'"--_12w6gfj": ix(getLineHeight())',
]);
});

describe('binary expresssions', () => {
it('statically evaluates calculated value with identifier', () => {
const actual = transform(`
import '@compiled/react';
const spacing = 8;
<div css={{ marginTop: spacing * 2 }} />
`);

expect(actual).toIncludeMultiple(['._19pkexct{margin-top:16px}', 'ax(["_19pkexct"])']);
});

it('statically evaluates calculated value with nested binary', () => {
const actual = transform(`
import '@compiled/react';
const spacing = 8;
<div css={{ marginTop: spacing * 2 / 2 }} />
`);

expect(actual).toIncludeMultiple(['._19pkftgi{margin-top:8px}', 'ax(["_19pkftgi"])']);
});

it('statically evaluates calculated value with multiple identifiers', () => {
const actual = transform(`
import '@compiled/react';
const one = 1;
const two = 2;
const three = 3;
<div css={{ marginTop: one + two - three }} />
`);

expect(actual).toIncludeMultiple(['._19pkidpf{margin-top:0}', 'ax(["_19pkidpf"])']);
});

it('statically evaluates calculated value within calc utility', () => {
const actual = transform(`
import '@compiled/react';
const spacing = 8;
<div css={{ width: \`calc(100% - \${spacing * 2}px)\` }} />
`);

expect(actual).toIncludeMultiple([
'._1bsbj0q6{width:calc(100% - 16px)}',
'ax(["_1bsbj0q6"])',
]);
});

it('statically evaluates calculated value with string literal containing numeric value', () => {
const actual = transform(`
import '@compiled/react';
const stringSpacing = '8';
<div css={{ marginTop: stringSpacing * 2 }} />
`);

expect(actual).toIncludeMultiple(['._19pkexct{margin-top:16px}', 'ax(["_19pkexct"])']);
});

it('falls back to dynamic evaluation when non static value used', () => {
const actual = transform(`
import '@compiled/react';
const getSpacing = () => Math.random();
<div css={{ marginTop: getSpacing() * 2 }} />
`);

expect(actual).toIncludeMultiple([
'._19pk19vg{margin-top:var(--_lb6tu)}',
'"--_lb6tu": ix(getSpacing() * 2)',
'ax(["_19pk19vg"])',
]);
});
});
});
22 changes: 21 additions & 1 deletion packages/babel-plugin/src/styled/__tests__/behaviour.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,27 @@ describe('styled component behaviour', () => {
\`;
`);

expect(actual).toInclude('"--_1p69eoh":ix(props.color,"px","-")');
expect(actual).toInclude('"--_1p69eoh-":ix(props.color,"px","-")');
});

it('creates a separate var name for positive and negative values of the same interpolation', () => {
const actual = transform(`
import { styled } from '@compiled/react';
const random = Math.random;
const LayoutRight = styled.aside\`
margin-right: -\${random() * 5}px;
margin-left: \${random() * 5}px;
\`;
`);

expect(actual).toIncludeMultiple([
'._2hwxjtuq{margin-right:var(--_1hnpmp1-)}',
'._18u01s7m{margin-left:var(--_1hnpmp1)}',
'"--_1hnpmp1-":ix(random()*5,"px","-")',
'"--_1hnpmp1":ix(random()*5,"px")',
'ax(["_2hwxjtuq _18u01s7m",props.className]',
]);
});

it('should compose a component using tagged template expression', () => {
Expand Down
73 changes: 5 additions & 68 deletions packages/babel-plugin/src/utils/css-builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,64 +405,6 @@ const extractLogicalExpression = (node: t.ArrowFunctionExpression, meta: Metadat
return { css: mergeSubsequentUnconditionalCssItems(css), variables };
};

const createNegativeUnaryExpression = (node: t.Identifier) => ({
type: 'UnaryExpression',
start: node.start,
end: node.end,
operator: '-',
prefix: true,
argument: {
type: 'Identifier',
start: node.start,
end: node.end,
name: node.name,
},
});

/**
* Manipulates the AST to ensure that CSS variables are generated correctly for negative values
*
* Consider we have the following:
*
* const gridSize = 8;
*
* const LayoutRight = styled.aside`
* margin-right: -${gridSize * 5}px; // A
* margin-left: ${gridSize * 5}px; // B
* top: -${gridSize * 5}px; // A
* left: -${gridSize * 8}px; // C
* right: ${gridSize * 8}px; // D
* color: red;
* `;
*
* In order to calculate the correct CSS custom properties, items labeled A & C need to be converted to UnaryExpressions
* This essentially changes these to ${-gridSize * 5}px; & ${-gridSize * 8}px respectively - resulting in correct CSS
* generation for these negative values.
*
* Without this manipulation you would end up with:
* -40px for A & B
* -64px for C & D
*
* With this manipulation you end up with:
* -40px for A
* 40px for B
* -64px for C
* 64px for D
*
* @param nodeExpression Node we're interested in manipulating
*/
const convertNegativeCssValuesToUnaryExpression = (nodeExpression: t.Expression): t.Expression => {
if (t.isBinaryExpression(nodeExpression)) {
return {
...nodeExpression,
left: createNegativeUnaryExpression(nodeExpression.left as t.Identifier),
} as t.Expression;
} else if (t.isIdentifier(nodeExpression)) {
return createNegativeUnaryExpression(nodeExpression) as t.Expression;
}
return nodeExpression;
};

/*
* Extracts the keyframes CSS from the `@compiled/react` keyframes usage.
*
Expand Down Expand Up @@ -614,7 +556,7 @@ const extractTemplateLiteral = (node: t.TemplateLiteral, meta: Metadata): CSSOut

// Quasis are the string pieces of the template literal - the parts around the interpolations
const literalResult = node.quasis.reduce<string>((acc, quasi, index): string => {
let nodeExpression = node.expressions[index] as t.Expression | undefined;
const nodeExpression = node.expressions[index] as t.Expression | undefined;

if (
!nodeExpression ||
Expand All @@ -624,14 +566,6 @@ const extractTemplateLiteral = (node: t.TemplateLiteral, meta: Metadata): CSSOut
return acc + quasi.value.raw + suffix;
}

// Deal with any negative values such as:
// margin: -${gridSize}, top: -${gridSize} etc.
if (quasi.value.raw.endsWith('-') && !t.isArrowFunctionExpression(nodeExpression)) {
// Remove the '-' from the quasi, as it will soon be moved to a unary expression
quasi.value.raw = quasi.value.raw.slice(0, -1);
nodeExpression = convertNegativeCssValuesToUnaryExpression(nodeExpression);
}

/**
* Manipulate the node if the CSS property, pseudo classes or pseudo elements are defined
* within a template element rather than in a expression.
Expand Down Expand Up @@ -701,9 +635,12 @@ const extractTemplateLiteral = (node: t.TemplateLiteral, meta: Metadata): CSSOut
// E.g. `font-size: ${fontSize}px` will end up needing to look like:
// `font-size: var(--_font-size)`, with the suffix moved to inline styles
// style={{ '--_font-size': fontSize + 'px' }}
const name = `--_${hash(variableName)}`;
const nextQuasis = node.quasis[index + 1];
const [before, after] = cssAffixInterpolation(quasi.value.raw, nextQuasis.value.raw);
// Create a different CSS var name for negative value version
const name = `--_${hash(variableName)}${
before.variablePrefix === '-' ? before.variablePrefix : ''
}`;

// Removes any suffixes from the next quasis.
nextQuasis.value.raw = after.css;
Expand Down
9 changes: 8 additions & 1 deletion packages/babel-plugin/src/utils/evaluate-expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getPathOfNode } from './ast';
import { createResultPair } from './create-result-pair';
import { isCompiledKeyframesCallExpression } from './is-compiled';
import {
traverseBinaryExpression,
traverseCallExpression,
traverseFunction,
traverseIdentifier,
Expand Down Expand Up @@ -89,7 +90,7 @@ const babelEvaluateExpression = (
}

const result = path.evaluate();
if (result.value) {
if (result.value != null) {
switch (typeof result.value) {
case 'string':
return t.stringLiteral(result.value);
Expand Down Expand Up @@ -140,6 +141,12 @@ export const evaluateExpression = (
({ value, meta: updatedMeta } = traverseFunction(expression, updatedMeta));
} else if (t.isCallExpression(expression)) {
({ value, meta: updatedMeta } = traverseCallExpression(expression, updatedMeta));
} else if (t.isBinaryExpression(expression)) {
({ value, meta: updatedMeta } = traverseBinaryExpression(
expression,
updatedMeta,
evaluateExpression
));
}

if (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { traverseBinaryExpression } from './traverse-binary-expression';
export * from './traverse-function';
export * from './traverse-call-expression';
export * from './traverse-identifier';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as t from '@babel/types';

import type { Metadata } from '../../types';
import { createResultPair } from '../create-result-pair';

type EvaluateExpression = (
expression: t.Expression,
meta: Metadata
) => ReturnType<typeof createResultPair>;

const hasNumericValue = (expression: t.Expression): boolean =>
t.isNumericLiteral(expression) ||
(t.isStringLiteral(expression) && !Number.isNaN(Number(expression.value)));

export const traverseBinaryExpression = (
expression: t.BinaryExpression,
meta: Metadata,
evaluateExpression: EvaluateExpression
): ReturnType<typeof createResultPair> => {
if (!t.isPrivateName(expression.left)) {
const { value: left } = evaluateExpression(expression.left, meta);
const { value: right } = evaluateExpression(expression.right, meta);

if (hasNumericValue(left) && hasNumericValue(right)) {
return createResultPair(t.binaryExpression(expression.operator, left, right), meta);
}
}

return createResultPair(expression, meta);
};

0 comments on commit 73821f2

Please sign in to comment.