diff --git a/packages/babel-plugin-minify-constant-folding/README.md b/packages/babel-plugin-minify-constant-folding/README.md index 71468ccab..6e4eeb2dd 100644 --- a/packages/babel-plugin-minify-constant-folding/README.md +++ b/packages/babel-plugin-minify-constant-folding/README.md @@ -1,6 +1,6 @@ # babel-plugin-minify-constant-folding -Tries to evaluate expressions and inline the result. For now only deals with numbers and strings. +Tries to evaluate expressions and inline the result. ## Example @@ -10,7 +10,22 @@ Tries to evaluate expressions and inline the result. For now only deals with num "a" + "b" 2 * 3; 4 | 3; -"b" + a + "c" + "d" + g + z + "f" + "h" + "z" +"b" + a + "c" + "d" + g + z + "f" + "h" + "i" + +[a, b, c].concat([d, e], f, g, [h]); +["a", "b", "c"].join(); +["a", "b", "c"].join('@'); +[1, 2, 3].length; +[1, 2, 3][1]; +[1, 2, 3].shift(); +[1, 2, 3].slice(0, 2); +[a, b, c].pop(); +[a, b, c].reverse(); +"a,b,c".split(","); +"abc"[0]; +"abc".charAt(); +"abc".charAt(1); +"abc".length; ``` **Out** @@ -19,7 +34,23 @@ Tries to evaluate expressions and inline the result. For now only deals with num "ab"; 6; 7; -"b" + a + "cd" + g + z + "fhz"; +"b" + a + "cd" + g + z + "fhi"; + +[a, b, c, d, e, f, g, h]; +"a,b,c"; +"a@b@c"; +3; +2; +2; +[1, 2]; +c; +[c, b, a]; +["a", "b", "c"]; +"a"; +"a"; +"a"; +"b"; +3; ``` ## Installation diff --git a/packages/babel-plugin-minify-constant-folding/__tests__/constant-folding-test.js b/packages/babel-plugin-minify-constant-folding/__tests__/constant-folding-test.js index f1872e0dd..705d3f85c 100644 --- a/packages/babel-plugin-minify-constant-folding/__tests__/constant-folding-test.js +++ b/packages/babel-plugin-minify-constant-folding/__tests__/constant-folding-test.js @@ -103,4 +103,142 @@ describe("constant-folding-plugin", () => { `); expect(transform(source)).toBe(expected); }); + + it("should handle Array methods on array literals", () => { + const source = unpad( + ` + [1, 2, 3].concat([4, 5, 6]); + [a, b, c].concat([d, e], f, g, [h]); + [1, 2, 3]["concat"]([4, 5, 6]); + [1, 2, 3].push([4, 5, 6]); + + [1, 2, 3].join(); + ["a", "b", "c"].join(); + ["a", "b", "c"].join("@"); + + [1, 2, 3].length; + [1, 2, 3][1]; + [1, 2, 3]["1"]; + [1, 2, 3][4]; + + [].shift(); + [1, 2, 3].shift(); + + [1, 2, 3].slice(); + [1, 2, 3].slice(1); + [1, 2, 3].slice(0, 2); + [1, 2, 3].slice(0, -1); + + [1, 2, 3].pop(); + [a, b, c].pop(); + [].pop(); + + [a, b, c].reverse(); + [1, 2, 3].reverse(); + + [1, 2, 3].splice(1); + [1, 2, 3, 4].splice(1, 2); + ` + ); + const expected = unpad( + ` + [1, 2, 3, 4, 5, 6]; + [a, b, c, d, e, f, g, h]; + [1, 2, 3, 4, 5, 6]; + 4; + + "1,2,3"; + "a,b,c"; + "a@b@c"; + + 3; + 2; + 2; + void 0; + + void 0; + 2; + + [1, 2, 3]; + [2, 3]; + [1, 2]; + [1, 2, 3].slice(0, -1); + + 3; + c; + void 0; + + [c, b, a]; + [3, 2, 1]; + + [2, 3]; + [2, 3]; + ` + ); + expect(transform(source)).toBe(expected); + }); + it("should ignore bad calls to array expression methods", () => { + const source = unpad( + ` + [1, 2, 3][concat]([4, 5, 6]); + [a, "b", "c"].join(); + ["a", "b", "c"].join(a); + [1, 2, 3].splice("a"); + ` + ); + expect(transform(source)).toBe(source); + }); + it("should ignore bad calls to string expression methods", () => { + const source = unpad( + ` + "abc".something; + "abc"["something"]; + ` + ); + expect(transform(source)).toBe(source); + }); + it("should handle String methods on string literals", () => { + const source = unpad( + ` + "a,b,c".split(","); + "a,b,c".split(""); + "a,b,c".split(); + "abc"[0]; + "abc"["0"]; + "abc"[4]; + "abc".charAt(); + "abc".charAt(1); + "abc".charCodeAt(); + "abc".charCodeAt(1); + "abc".length; + + "\u{1f44d}".charCodeAt(); + "\u{1f44d}".charCodeAt(1); + "\u{1f44d}".codePointAt(); + "\u{1f44d}".codePointAt(1); + ` + ); + + const expected = unpad( + ` + ["a", "b", "c"]; + ["a", ",", "b", ",", "c"]; + ["a,b,c"]; + "a"; + "a"; + void 0; + "a"; + "b"; + 97; + 98; + 3; + + ${0xd83d}; + ${0xdc4d}; + ${0x1f44d}; + ${0xdc4d}; + ` + ); + expect(transform(source)).toBe(expected); + }); }); diff --git a/packages/babel-plugin-minify-constant-folding/src/index.js b/packages/babel-plugin-minify-constant-folding/src/index.js index f9826e5c7..782d76be2 100644 --- a/packages/babel-plugin-minify-constant-folding/src/index.js +++ b/packages/babel-plugin-minify-constant-folding/src/index.js @@ -2,8 +2,45 @@ const evaluate = require("babel-helper-evaluate-path"); -module.exports = ({ types: t, traverse }) => { +const { FALLBACK_HANDLER } = require("./replacements"); + +function getName(member) { + if (member.computed) { + switch (member.property.type) { + case "StringLiteral": + case "NumericLiteral": + return member.property.value; + case "TemplateLiteral": + return; + } + } else { + return member.property.name; + } +} + +function swap(path, member, handlers, ...args) { + const key = getName(member); + if (key === undefined) return; + let handler = handlers[key]; + if (typeof handler !== "function") { + if (typeof handlers[FALLBACK_HANDLER] === "function") { + handler = handlers[FALLBACK_HANDLER].bind(member.object, key); + } else { + return false; + } + } + const replacement = handler.apply(member.object, args); + if (replacement) { + path.replaceWith(replacement); + return true; + } + return false; +} + +module.exports = babel => { + const replacements = require("./replacements.js")(babel); const seen = Symbol("seen"); + const { types: t, traverse } = babel; return { name: "minify-constant-folding", @@ -124,6 +161,21 @@ module.exports = ({ types: t, traverse }) => { node[seen] = true; path.replaceWith(node); } + }, + CallExpression(path) { + const { node } = path; + const { callee: member } = node; + if (t.isMemberExpression(member)) { + const helpers = replacements[member.object.type]; + if (!helpers || !helpers.calls) return; + swap(path, member, helpers.calls, ...node.arguments); + } + }, + MemberExpression(path) { + const { node: member } = path; + const helpers = replacements[member.object.type]; + if (!helpers || !helpers.members) return; + swap(path, member, helpers.members); } } }; diff --git a/packages/babel-plugin-minify-constant-folding/src/replacements.js b/packages/babel-plugin-minify-constant-folding/src/replacements.js new file mode 100644 index 000000000..d6e916da5 --- /dev/null +++ b/packages/babel-plugin-minify-constant-folding/src/replacements.js @@ -0,0 +1,133 @@ +const FALLBACK_HANDLER = Symbol("fallback handler"); + +module.exports = ({ types: t }) => { + const undef = t.unaryExpression("void", t.numericLiteral(0)); + + function isUndef(ob) { + return ( + ob === undefined || + t.isIdentifier(ob, { name: "undefined" }) || + t.isUnaryExpression(ob, { operator: "void" }) + ); + } + + function defaultZero(cb) { + return function(i = t.numericLiteral(0), ...args) { + if (t.isNumericLiteral(i)) { + return cb.call(this, this, i.value, ...args); + } + }; + } + + return { + ArrayExpression: { + members: { + length() { + return t.numericLiteral(this.elements.length); + }, + [FALLBACK_HANDLER](i) { + if (typeof i === "number" || i.match(/^\d+$/)) { + return this.elements[i] || undef; + } + } + }, + calls: { + concat(...args) { + return t.arrayExpression( + this.elements.concat( + ...args.map(arg => { + if (t.isArrayExpression(arg)) return arg.elements; + return arg; + }) + ) + ); + }, + join(sep = t.stringLiteral(",")) { + if (!t.isStringLiteral(sep)) return; + let bad = false; + const str = this.elements + .map(el => { + if (!t.isLiteral(el)) { + bad = true; + return; + } + return el.value; + }) + .join(sep.value); + return bad ? undefined : t.stringLiteral(str); + }, + push(...args) { + return t.numericLiteral(this.elements.length + args.length); + }, + shift() { + if (this.elements.length === 0) { + return undef; + } + return t.numericLiteral(this.elements.length - 1); + }, + slice(start = t.numericLiteral(0), end) { + if (!t.isNumericLiteral(start) || (end && !t.isNumericLiteral(end))) { + return; + } + return t.arrayExpression( + this.elements.slice(start.value, end && end.value) + ); + }, + pop() { + return this.elements[this.elements.length - 1] || undef; + }, + reverse() { + return t.arrayExpression(this.elements.reverse()); + }, + splice(start, end, ...args) { + if (!t.isNumericLiteral(start) || (end && !t.isNumericLiteral(end))) { + return; + } + if (end) { + args.unshift(end.value); + } + return t.arrayExpression( + this.elements.slice().splice(start.value, ...args) + ); + } + } + }, + StringLiteral: { + members: { + length() { + return t.numericLiteral(this.value.length); + }, + [FALLBACK_HANDLER](i) { + if (typeof i === "number" || i.match(/^\d+$/)) { + const ch = this.value[i]; + return ch ? t.stringLiteral(ch) : undef; + } + } + }, + calls: { + split(sep = undef) { + let realSep = null; + if (t.isStringLiteral(sep)) { + realSep = sep.value; + } + if (isUndef(sep)) { + realSep = sep; + } + if (realSep !== null) { + return t.arrayExpression( + this.value.split(realSep).map(str => t.stringLiteral(str)) + ); + } + }, + charAt: defaultZero(({ value }, i) => t.stringLiteral(value.charAt(i))), + charCodeAt: defaultZero(({ value }, i) => + t.numericLiteral(value.charCodeAt(i)) + ), + codePointAt: defaultZero(({ value }, i) => + t.numericLiteral(value.codePointAt(i)) + ) + } + } + }; +}; +module.exports.FALLBACK_HANDLER = FALLBACK_HANDLER;