Skip to content

Commit

Permalink
feat: add support for combining arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
merceyz committed May 5, 2019
1 parent 36107ce commit 5708852
Show file tree
Hide file tree
Showing 6 changed files with 321 additions and 9 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ npm install babel-plugin-optimize-clsx --save-dev

## Example

### Extract objects

Transforms

```javascript
Expand All @@ -30,3 +32,24 @@ to
```javascript
clsx('foo', disabled && classes.disabled, focusVisible && !disabled && classes.focusVisible, 'bar');
```

### Extract and combine

Transforms

```javascript
clsx({
[classes.disabled]: disabled,
[classes.focusVisible]: this.state.focusVisible,
[focusVisibleClassName]: this.state.focusVisible,
});
```

to

```javascript
clsx(
this.state.focusVisible && [classes.focusVisible, focusVisibleClassName],
disabled && classes.disabled,
);
```
95 changes: 95 additions & 0 deletions src/combineArguments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
const t = require('@babel/types');
const compareNodes = require('./utils/compareNodes');
const helpers = require('./utils/helpers');

module.exports = args => {
const [match, noMatch] = helpers.filterArray(args, item => {
return t.isLogicalExpression(item) && helpers.isAllLogicalAndOperators(item);
});

// Not enough items to optimize
if (match.length < 2) return args;

const operators = match.map(helpers.flattenLogicalOperator);

const node = helpers.getMostFrequentNode(operators);
const rootNode = combineOperators(operators, node);

const newAST = convertToAST(rootNode);

return [...noMatch, ...newAST];

function convertToAST(node) {
if (node.type !== 'rootNode') {
return node;
}

const result = [];

if (node.child.type === 'rootNode') {
const arr = t.arrayExpression(convertToAST(node.child));
result.push(t.logicalExpression('&&', node.node, arr));
} else {
result.push(t.logicalExpression('&&', node.node, node.child));
}

if (node.next !== undefined) {
const r = convertToAST(node.next);
if (r.push !== undefined) {
result.push(...r);
} else {
result.push(r);
}
}

return result;
}

function combineOperators(operators, node) {
const newNode = {
type: 'rootNode',
node: node,
child: [],
next: [],
};

operators.forEach(row => {
const filtered = row.filter(item => !compareNodes(item, node));
if (filtered.length === row.length) {
newNode.next.push(row);
} else {
newNode.child.push(filtered);
}
});

newNode.next = checkSub(newNode.next);
newNode.child = checkSub(newNode.child);

return newNode;

function checkSub(items) {
if (items.length === 0) return undefined;
if (items.length === 1) {
const item = items[0];

if (item.length === 1) {
return item[0];
}

let result = t.logicalExpression('&&', item.shift(), item.shift());
while (item.length > 0) {
result = t.logicalExpression('&&', result, item.shift());
}

return result;
}

const nextCheck = helpers.getMostFrequentNode(items);
if (nextCheck !== null) {
return combineOperators(items, nextCheck);
}

return t.arrayExpression(items.map(e => e[0]));
}
}
};
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const extractArguments = require('./extractArguments');
const combineArguments = require('./combineArguments');

module.exports = () => {
return {
Expand All @@ -10,6 +11,7 @@ module.exports = () => {
try {
let args = node.arguments;
args = extractArguments(args);
args = combineArguments(args);
node.arguments = args;
} catch (err) {
throw path.buildCodeFrameError(err);
Expand Down
40 changes: 40 additions & 0 deletions src/utils/compareNodes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module.exports = function compareNodes(a, b) {
if (typeof a !== typeof b) {
return false;
}

if (typeof a !== 'object') {
return a === b;
}

for (const key in a) {
if (a.hasOwnProperty(key)) {
// Ignore location data
if (key === 'start' || key === 'end' || key === 'loc') {
continue;
}

if (b.hasOwnProperty(key) === false) {
return false;
}

if (typeof a[key] === 'object') {
if (typeof b[key] !== 'object') {
return false;
}

if (compareNodes(a[key], b[key]) === false) {
return false;
}

continue;
}

if (a[key] !== b[key]) {
return false;
}
}
}

return true;
};
84 changes: 84 additions & 0 deletions src/utils/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
const t = require('@babel/types');
const compareNodes = require('./compareNodes');

function flattenLogicalOperator(node) {
if (t.isLogicalExpression(node)) {
return [...flattenLogicalOperator(node.left), node.right];
}

return [node];
}

function isAllLogicalAndOperators(node) {
if (t.isLogicalExpression(node)) {
if (node.operator !== '&&') {
return false;
}

return isAllLogicalAndOperators(node.left);
}

return true;
}

function filterArray(array, callback) {
const match = [];
const noMatch = [];

for (const item of array) {
if (callback(item) === true) {
match.push(item);
} else {
noMatch.push(item);
}
}

return [match, noMatch];
}

function getMostFrequentNode(operators) {
let maxNode = null;
let maxLength = 0;

operators.forEach(row => {
for (let x = 0; x < row.length - 1; x++) {
const item = row[x];
let length = 0;

operators.forEach(row2 => {
for (let x2 = 0; x2 < row2.length - 1; x2++) {
const item2 = row2[x2];
if (compareNodes(item, item2) === true) {
length += item.end - item.start;
}
}
});

if (length > maxLength) {
maxNode = item;
maxLength = length;
}
}
});

return maxNode;
}

function stringify(object) {
function replacer(name, val) {
if (name === 'start' || name === 'loc' || name === 'end') {
return undefined;
}
return val;
}

return JSON.stringify(object, replacer, 1);
}

module.exports = {
flattenLogicalOperator,
isAllLogicalAndOperators,
filterArray,
getMostFrequentNode,
stringify,
};
86 changes: 77 additions & 9 deletions test/objects.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import * as babel from '@babel/core';
import path from 'path';

const testCases = [
// Examples from https://github.com/lukeed/clsx
["clsx('foo', true && 'bar', 'baz');", "clsx('foo',true&&'bar','baz');"],
[
"clsx({ foo:true }, { bar:false }, null, { '--foobar':'hello' });",
"clsx(true&&foo,false&&bar,null,'hello'&&'--foobar');",
// Examples from https://github.com/lukeed/clsx
"clsx('foo', true && 'bar', 'baz');",
"clsx('foo',true&&'bar','baz');",
],
[
"clsx({ foo:true }, { bar:false }, null, { '--foobar':'hello' });",
"clsx(true&&foo,false&&bar,null,'hello'&&'--foobar');",
"clsx(null,'hello'&&'--foobar',false&&bar,true&&foo);",
],
// Snippets taken from https://github.com/mui-org/material-ui/tree/next/packages/material-ui/src
[
Expand All @@ -35,24 +34,90 @@ const testCases = [
],
[
"clsx(classes.root,{[classes.extended]: variant === 'extended',[classes.primary]: color === 'primary',[classes.secondary]: color === 'secondary',[classes[`size${capitalize(size)}`]]: size !== 'large',[classes.disabled]: disabled,[classes.colorInherit]: color === 'inherit',},className,)",
"clsx(classes.root,variant==='extended'&&classes.extended,color==='primary'&&classes.primary,color==='secondary'&&classes.secondary,size!=='large'&&classes[`size${capitalize(size)}`],disabled&&classes.disabled,color==='inherit'&&classes.colorInherit,className);",
"clsx(classes.root,className,variant==='extended'&&classes.extended,color==='secondary'&&classes.secondary,color==='primary'&&classes.primary,color==='inherit'&&classes.colorInherit,size!=='large'&&classes[`size${capitalize(size)}`],disabled&&classes.disabled);",
],
[
"clsx(classes.bar, {[classes.barColorPrimary]: color === 'primary' && variant !== 'buffer',[classes.bar2Indeterminate]: variant === 'indeterminate' || variant === 'query',})",
"clsx(classes.bar,color==='primary'&&variant!=='buffer'&&classes.barColorPrimary,(variant==='indeterminate'||variant==='query')&&classes.bar2Indeterminate);",
],
[
"clsx(classes.bar, {[classes.barColorPrimary]: color === 'primary' && variant !== 'buffer',[classes.colorPrimary]: color === 'primary' && variant === 'buffer',[classes.barColorSecondary]: color === 'secondary' && variant !== 'buffer',[classes.colorSecondary]: color === 'secondary' && variant === 'buffer',[classes.bar2Indeterminate]: variant === 'indeterminate' || variant === 'query',[classes.bar2Buffer]: variant === 'buffer',})",
"clsx(classes.bar,color==='primary'&&variant!=='buffer'&&classes.barColorPrimary,color==='primary'&&variant==='buffer'&&classes.colorPrimary,color==='secondary'&&variant!=='buffer'&&classes.barColorSecondary,color==='secondary'&&variant==='buffer'&&classes.colorSecondary,(variant==='indeterminate'||variant==='query')&&classes.bar2Indeterminate,variant==='buffer'&&classes.bar2Buffer);",
`clsx(classes.bar, {[classes.barColorPrimary]: color === 'primary' && variant !== 'buffer',[classes.colorPrimary]: color === 'primary' && variant === 'buffer',[classes.barColorSecondary]: color === 'secondary' && variant !== 'buffer',[classes.colorSecondary]: color === 'secondary' && variant === 'buffer',[classes.bar2Indeterminate]: variant === 'indeterminate' || variant === 'query',[classes.bar2Buffer]: variant === 'buffer',})`,
`clsx(classes.bar,(variant==='indeterminate'||variant==='query')&&classes.bar2Indeterminate,variant==='buffer'&&[color==='secondary'&&classes.colorSecondary,color==='primary'&&classes.colorPrimary,classes.bar2Buffer],variant!=='buffer'&&[color==='secondary'&&classes.barColorSecondary,color==='primary'&&classes.barColorPrimary]);`,
],
[
"clsx({[classes.head]: variant ? variant === 'head' : tablelvl2 && tablelvl2.variant === 'head'});",
"clsx((variant?variant==='head':tablelvl2&&tablelvl2.variant==='head')&&classes.head);",
],
[
`clsx(
foo && classes.text,
bar && classes.text,
text && classes.text,
color === "primary" && text && true && classes.text,
text && color === "primary" && classes.textPrimary,
text && color === "secondary" && classes.textSecondary
);
`,
'clsx(color==="primary"&&[text&&[true&&classes.text,classes.textPrimary]],color==="secondary"&&text&&classes.textSecondary,text&&classes.text,foo&&classes.text,bar&&classes.text);',
],
[
`clsx(
classes.root,
{
[classes.text]: text,
[classes.textPrimary]: text && color === 'primary',
[classes.textSecondary]: text && color === 'secondary',
[classes.contained]: contained,
[classes.containedPrimary]: contained && color === 'primary',
[classes.containedSecondary]: contained && color === 'secondary',
[classes.outlined]: variant === 'outlined',
[classes.outlinedPrimary]: variant === 'outlined' && color === 'primary',
[classes.outlinedSecondary]: variant === 'outlined' && color === 'secondary',
[classes.disabled]: disabled,
[classes.fullWidth]: fullWidth,
[classes.colorInherit]: color === 'inherit',
},
classNameProp,
);`,
"clsx(classes.root,classNameProp,variant==='outlined'&&[color==='secondary'&&classes.outlinedSecondary,color==='primary'&&classes.outlinedPrimary,classes.outlined],color==='secondary'&&[contained&&classes.containedSecondary,text&&classes.textSecondary],color==='primary'&&[contained&&classes.containedPrimary,text&&classes.textPrimary],color==='inherit'&&classes.colorInherit,contained&&classes.contained,fullWidth&&classes.fullWidth,disabled&&classes.disabled,text&&classes.text);",
],
[
`clsx(
classes.root,
{
[classes.selected]: selected,
[classes.iconOnly]: !showLabel && !selected,
},
className,
)`,
'clsx(classes.root,className,!showLabel&&!selected&&classes.iconOnly,selected&&classes.selected);',
],
[
`clsx(
classes.root,
{
[classes.disabled]: disabled,
[classes.focusVisible]: this.state.focusVisible,
[focusVisibleClassName]: this.state.focusVisible,
},
classNameProp,
)`,
'clsx(classes.root,classNameProp,this.state.focusVisible&&[classes.focusVisible,focusVisibleClassName],disabled&&classes.disabled);',
],
[
"clsx({[classes[`deleteIconColor${capitalize(color)}`]]: color !== 'default' && variant !== 'outlined',[classes[`deleteIconOutlinedColor${capitalize(color)}`]]: color !== 'default' && variant === 'outlined',})",
"clsx(color!=='default'&&[variant!=='outlined'&&classes[`deleteIconColor${capitalize(color)}`],variant==='outlined'&&classes[`deleteIconOutlinedColor${capitalize(color)}`]]);",
],
[
"clsx(classes.root,{[classes[`color${capitalize(color)}`]]: color !== 'default',[classes.clickable]: clickable,[classes[`clickableColor${capitalize(color)}`]]: clickable && color !== 'default',[classes.deletable]: onDelete,[classes[`deletableColor${capitalize(color)}`]]: onDelete && color !== 'default',[classes.outlined]: variant === 'outlined',[classes.outlinedPrimary]: variant === 'outlined' && color === 'primary',[classes.outlinedSecondary]: variant === 'outlined' && color === 'secondary',},classNameProp);",
"clsx(classes.root,classNameProp,variant==='outlined'&&[color==='secondary'&&classes.outlinedSecondary,color==='primary'&&classes.outlinedPrimary,classes.outlined],color!=='default'&&[clickable&&classes[`clickableColor${capitalize(color)}`],onDelete&&classes[`deletableColor${capitalize(color)}`],classes[`color${capitalize(color)}`]],clickable&&classes.clickable,onDelete&&classes.deletable);",
],
];

it('transforms objects correctly', () => {
for (const testCase of testCases) {
for (let i = 0; i < testCases.length; i++) {
const testCase = testCases[i];

const result = babel.transformSync(testCase[0], {
plugins: [path.resolve(__dirname, '..')],
babelrc: false,
Expand All @@ -63,6 +128,9 @@ it('transforms objects correctly', () => {
if (testCase.length !== 2) {
throw new Error('Missing expected result. Output:\n' + result.code);
} else {
if (result.code !== testCase[1]) {
console.log('Index of failed test: %d', i);
}
expect(result.code).toBe(testCase[1]);
}
}
Expand Down

0 comments on commit 5708852

Please sign in to comment.