From 5708852707b37550cb47caea91215e37ce883aa4 Mon Sep 17 00:00:00 2001 From: merceyz Date: Sun, 5 May 2019 22:44:38 +0200 Subject: [PATCH] feat: add support for combining arguments --- README.md | 23 ++++++++++ src/combineArguments.js | 95 +++++++++++++++++++++++++++++++++++++++ src/index.js | 2 + src/utils/compareNodes.js | 40 +++++++++++++++++ src/utils/helpers.js | 84 ++++++++++++++++++++++++++++++++++ test/objects.test.js | 86 +++++++++++++++++++++++++++++++---- 6 files changed, 321 insertions(+), 9 deletions(-) create mode 100644 src/combineArguments.js create mode 100644 src/utils/compareNodes.js create mode 100644 src/utils/helpers.js diff --git a/README.md b/README.md index f880e0f..37e3a99 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ npm install babel-plugin-optimize-clsx --save-dev ## Example +### Extract objects + Transforms ```javascript @@ -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, +); +``` diff --git a/src/combineArguments.js b/src/combineArguments.js new file mode 100644 index 0000000..95dbdb7 --- /dev/null +++ b/src/combineArguments.js @@ -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])); + } + } +}; diff --git a/src/index.js b/src/index.js index 5a05d54..7053001 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,5 @@ const extractArguments = require('./extractArguments'); +const combineArguments = require('./combineArguments'); module.exports = () => { return { @@ -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); diff --git a/src/utils/compareNodes.js b/src/utils/compareNodes.js new file mode 100644 index 0000000..d91a63a --- /dev/null +++ b/src/utils/compareNodes.js @@ -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; +}; diff --git a/src/utils/helpers.js b/src/utils/helpers.js new file mode 100644 index 0000000..e7a1d82 --- /dev/null +++ b/src/utils/helpers.js @@ -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, +}; diff --git a/test/objects.test.js b/test/objects.test.js index ed49293..e8bb775 100644 --- a/test/objects.test.js +++ b/test/objects.test.js @@ -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 [ @@ -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, @@ -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]); } }