diff --git a/lib/ConstPlugin.js b/lib/ConstPlugin.js index 3980713bd..40e96d301 100644 --- a/lib/ConstPlugin.js +++ b/lib/ConstPlugin.js @@ -314,6 +314,45 @@ class ConstPlugin { dep.loc = expression.loc; parser.state.module.addPresentationalDependency(dep); } + return keepRight; + } + } else if (expression.operator === "??") { + const param = parser.evaluateExpression(expression.left); + const keepRight = param && param.asNullish(); + if (typeof keepRight === "boolean") { + // ------------------------------------------ + // + // Given the following code: + // + // (falsy || truthy) ?? someExpression(); + // + // the generated code is: + // + // (falsy || truthy) ?? false; + // + // ------------------------------------------ + // + // Given the following code: + // + // nullable ?? someExpression(); + // + // the generated code is: + // + // null ?? someExpression(); + // + if (keepRight) { + const dep = new ConstDependency("null", param.range); + dep.loc = expression.loc; + parser.state.module.addPresentationalDependency(dep); + } else { + const dep = new ConstDependency( + "false", + expression.right.range + ); + dep.loc = expression.loc; + parser.state.module.addPresentationalDependency(dep); + } + return keepRight; } } diff --git a/lib/dependencies/CommonJsPlugin.js b/lib/dependencies/CommonJsPlugin.js index 2d33cb510..099406753 100644 --- a/lib/dependencies/CommonJsPlugin.js +++ b/lib/dependencies/CommonJsPlugin.js @@ -199,13 +199,7 @@ class CommonJsPlugin { parser.hooks.evaluateIdentifier.for("module.hot").tap( "CommonJsPlugin", - evaluateToIdentifier( - "module.hot", - "module", - () => ["hot"], - false, - true - ) + evaluateToIdentifier("module.hot", "module", () => ["hot"], null) ); new CommonJsImportsParserPlugin(options).apply(parser); diff --git a/lib/javascript/BasicEvaluatedExpression.js b/lib/javascript/BasicEvaluatedExpression.js index 98281aa01..22cb2c00f 100644 --- a/lib/javascript/BasicEvaluatedExpression.js +++ b/lib/javascript/BasicEvaluatedExpression.js @@ -98,6 +98,37 @@ class BasicEvaluatedExpression { return this.type === TypeTemplateString; } + /** + * check for "simple" types (inlined) only + * @returns {boolean} is simple type + */ + isSimpleType() { + switch (this.type) { + case TypeIdentifier: + case TypeConditional: + case TypeWrapped: + case TypeUnknown: + return false; + default: + return true; + } + } + + /** + * @param {BasicEvaluatedExpression} basicEvaluatedExpression basicEvaluatedExpression + * @returns {boolean|undefined} is same type + */ + isSameType(basicEvaluatedExpression) { + if ( + this.type === TypeUnknown || + basicEvaluatedExpression.type === TypeUnknown + ) { + return undefined; + } + + return this.type === basicEvaluatedExpression.type; + } + isTruthy() { return this.truthy; } @@ -112,7 +143,7 @@ class BasicEvaluatedExpression { asBool() { if (this.truthy) return true; - if (this.falsy) return false; + if (this.falsy || this.nullish) return false; if (this.isBoolean()) return this.bool; if (this.isNull()) return false; if (this.isUndefined()) return false; diff --git a/lib/javascript/JavascriptParser.js b/lib/javascript/JavascriptParser.js index 285d35506..601e09efe 100644 --- a/lib/javascript/JavascriptParser.js +++ b/lib/javascript/JavascriptParser.js @@ -383,21 +383,24 @@ class JavascriptParser extends Parser { } }; - const handleCompare = fn => { + const handleStrictCompare = fn => { const left = this.evaluateExpression(expr.left); const right = this.evaluateExpression(expr.right); if (!left || !right) return; - if (left.isNumber() && right.isNumber()) { + const sameType = left.isSameType(right); + if (sameType !== true) return; + + if (left.isNumber()) { res = new BasicEvaluatedExpression(); res.setBoolean(fn(left.number, right.number)); res.setRange(expr.range); return res; - } else if (left.isString() && right.isString()) { + } else if (left.isString()) { res = new BasicEvaluatedExpression(); res.setBoolean(fn(left.string, right.string)); res.setRange(expr.range); return res; - } else if (left.isBigInt() && right.isBigInt()) { + } else if (left.isBigInt()) { res = new BasicEvaluatedExpression(); res.setBoolean(fn(left.bigint, right.bigint)); res.setRange(expr.range); @@ -405,6 +408,112 @@ class JavascriptParser extends Parser { } }; + const handleStrictEqualityComparison = eql => { + left = this.evaluateExpression(expr.left); + right = this.evaluateExpression(expr.right); + if (!left || !right) return; + const sameType = left.isSameType(right); + if (sameType === undefined) return; + res = new BasicEvaluatedExpression(); + res.setRange(expr.range); + + if (sameType === true) { + // check only for types that could be compared + if (left.isString()) { + return res.setBoolean( + eql + ? left.string === right.string + : left.string !== right.string + ); + } else if (left.isNumber()) { + return res.setBoolean( + eql + ? left.number === right.number + : left.number !== right.number + ); + } else if (left.isBigInt()) { + return res.setBoolean( + eql + ? left.bigint === right.bigint + : left.bigint !== right.bigint + ); + } else if (left.isBoolean()) { + return res.setBoolean( + eql ? left.bool === right.bool : left.bool !== right.bool + ); + } else if (left.isNull() || left.isUndefined()) { + return res.setBoolean(eql); + } + } + + // check for "simple" types, e.g. + // if ([] === 1) + // if ([] === []) + // if (0 === null) + // if ("" === /a/i) + if (left.isSimpleType() && right.isSimpleType()) { + return res.setBoolean(!eql); + } + }; + + const handleAbstractEqualityComparison = eql => { + left = this.evaluateExpression(expr.left); + right = this.evaluateExpression(expr.right); + if (!left || !right) return; + const sameType = left.isSameType(right); + if (sameType === undefined) return; + res = new BasicEvaluatedExpression(); + res.setRange(expr.range); + + // use strict equality comparison + if (sameType === true) { + if (left.isString()) { + return res.setBoolean( + eql + ? left.string === right.string + : left.string !== right.string + ); + } else if (left.isNumber()) { + return res.setBoolean( + eql + ? left.number === right.number + : left.number !== right.number + ); + } else if (left.isBigInt()) { + return res.setBoolean( + eql + ? left.bigint === right.bigint + : left.bigint !== right.bigint + ); + } else if (left.isBoolean()) { + return res.setBoolean( + eql ? left.bool === right.bool : left.bool !== right.bool + ); + } else if (left.isNull() || left.isUndefined()) { + return res.setBoolean(eql); + // if ([] == []) + // if ("a,s".split(",") == ["a", "s"]) + // if (/a/ == /a/) + } else if ( + left.isArray() || + left.isConstArray() || + left.isRegExp() + ) { + return res.setBoolean(!eql); + } + } + + if ( + (left.isFalsy() && right.isTruthy()) || + (left.isTruthy() && right.isFalsy()) + ) { + return res.setBoolean(!eql); + } + + // abstract equality comparison is not fully implemented + return undefined; + }; + let left; let right; let res; @@ -551,40 +660,14 @@ class JavascriptParser extends Parser { return handleNumberOperation((l, r) => l / r); } else if (expr.operator === "**") { return handleNumberOperation((l, r) => l ** r); - } else if (expr.operator === "==" || expr.operator === "===") { - left = this.evaluateExpression(expr.left); - right = this.evaluateExpression(expr.right); - if (!left || !right) return; - res = new BasicEvaluatedExpression(); - res.setRange(expr.range); - if (left.isString() && right.isString()) { - return res.setBoolean(left.string === right.string); - } else if (left.isNumber() && right.isNumber()) { - return res.setBoolean(left.number === right.number); - } else if (left.isBigInt() && right.isBigInt()) { - return res.setBoolean(left.bigint === right.bigint); - } else if (left.isBoolean() && right.isBoolean()) { - return res.setBoolean(left.bool === right.bool); - } else if (left.isNull() && right.isNull()) { - return res.setBoolean(true); - } - } else if (expr.operator === "!=" || expr.operator === "!==") { - left = this.evaluateExpression(expr.left); - right = this.evaluateExpression(expr.right); - if (!left || !right) return; - res = new BasicEvaluatedExpression(); - res.setRange(expr.range); - if (left.isString() && right.isString()) { - return res.setBoolean(left.string !== right.string); - } else if (left.isNumber() && right.isNumber()) { - return res.setBoolean(left.number !== right.number); - } else if (left.isBigInt() && right.isBigInt()) { - return res.setBoolean(left.bigint !== right.bigint); - } else if (left.isBoolean() && right.isBoolean()) { - return res.setBoolean(left.bool !== right.bool); - } else if (left.isNull() && right.isNull()) { - return res.setBoolean(false); - } + } else if (expr.operator === "===") { + return handleStrictEqualityComparison(true); + } else if (expr.operator === "==") { + return handleAbstractEqualityComparison(true); + } else if (expr.operator === "!==") { + return handleStrictEqualityComparison(false); + } else if (expr.operator === "!=") { + return handleAbstractEqualityComparison(false); } else if (expr.operator === "&") { return handleNumberOperation((l, r) => l & r); } else if (expr.operator === "|") { @@ -605,13 +688,13 @@ class JavascriptParser extends Parser { } else if (expr.operator === "<<") { return handleNumberOperation((l, r) => l << r); } else if (expr.operator === "<") { - return handleCompare((l, r) => l < r); + return handleStrictCompare((l, r) => l < r); } else if (expr.operator === ">") { - return handleCompare((l, r) => l > r); + return handleStrictCompare((l, r) => l > r); } else if (expr.operator === "<=") { - return handleCompare((l, r) => l <= r); + return handleStrictCompare((l, r) => l <= r); } else if (expr.operator === ">=") { - return handleCompare((l, r) => l >= r); + return handleStrictCompare((l, r) => l >= r); } }); this.hooks.evaluate diff --git a/lib/javascript/JavascriptParserHelpers.js b/lib/javascript/JavascriptParserHelpers.js index be27e73a5..45089e0bd 100644 --- a/lib/javascript/JavascriptParserHelpers.js +++ b/lib/javascript/JavascriptParserHelpers.js @@ -63,28 +63,26 @@ exports.evaluateToBoolean = value => { * @param {string} identifier identifier * @param {string} rootInfo rootInfo * @param {function(): string[]} getMembers getMembers - * @param {boolean=} truthy is truthy - * @param {boolean=} nullish is nullish + * @param {boolean|null=} truthy is truthy, null if nullish * @returns {function(ExpressionNode): BasicEvaluatedExpression} callback */ -exports.evaluateToIdentifier = ( - identifier, - rootInfo, - getMembers, - truthy, - nullish -) => { +exports.evaluateToIdentifier = (identifier, rootInfo, getMembers, truthy) => { return function identifierExpression(expr) { let evaluatedExpression = new BasicEvaluatedExpression() .setIdentifier(identifier, rootInfo, getMembers) .setRange(expr.range); - if (truthy === true) { - evaluatedExpression.setTruthy(); - } else if (truthy === false) { - evaluatedExpression.setFalsy(); - } - if (nullish !== undefined) { - evaluatedExpression.setNullish(nullish); + switch (truthy) { + case true: + evaluatedExpression.setTruthy(); + evaluatedExpression.setNullish(false); + break; + case null: + evaluatedExpression.setFalsy(); + evaluatedExpression.setNullish(true); + break; + case false: + evaluatedExpression.setFalsy(); + break; } return evaluatedExpression; diff --git a/test/cases/parsing/evaluate/index.js b/test/cases/parsing/evaluate/index.js index 9655e386c..b52751aa2 100644 --- a/test/cases/parsing/evaluate/index.js +++ b/test/cases/parsing/evaluate/index.js @@ -1,17 +1,21 @@ it("should evaluate null", function() { - var y = null ? require("fail") : require("./a"); + const y = null ? require("fail") : require("./a"); if(null) require("fail"); }); it("should evaluate logical expression", function() { - var value1 = "hello" || require("fail"); - var value2 = typeof require === "function" || require("fail"); - var value3 = "" && require("fail"); - var value4 = typeof require !== "function" && require("fail"); - var value5 = "hello" && (() => "value5")(); - var value6 = "" || (() => "value6")(); - var value7 = (function () { return'value7'===typeof 'value7'&&'value7'})(); + const value1 = "hello" || require("fail"); + const value2 = typeof require === "function" || require("fail"); + const value3 = "" && require("fail"); + const value4 = typeof require !== "function" && require("fail"); + const value5 = "hello" && (() => "value5")(); + const value6 = "" || (() => "value6")(); + const value7 = (function () { return'value7'===typeof 'value7'&&'value7'})(); + const value8 = [] != [] || require("fail"); + const value9 = (null === 1) && require("fail"); + const value91 = [] === [] && require("fail"); + const value92 = /a/ === /a/ && require("fail"); expect(value1).toBe("hello"); expect(value2).toBe(true); @@ -20,32 +24,46 @@ it("should evaluate logical expression", function() { expect(value5).toBe("value5"); expect(value6).toBe("value6"); expect(value7).toBe(false); + expect(value8).toBe(true); + expect(value9).toBe(false); + expect(value91).toBe(false); + expect(value92).toBe(false); + + if (!process.version.startsWith("v14")) return; + + const value10 = "" ?? require("fail"); + const value11 = null ?? "expected"; + const value12 = ("" ?? require("fail")) && true; + + expect(value10).toBe(""); + expect(value11).toBe("expected"); + expect(value12).toBe("") }); -if("shouldn't evaluate expression", function() { - var value = ""; - var x = (value + "") ? "fail" : "ok"; +it("shouldn't evaluate expression", function() { + const value = ""; + const x = (value + "") ? "fail" : "ok"; expect(x).toBe("ok"); }); it("should short-circuit evaluating", function() { - var expr; - var a = false && expr ? require("fail") : require("./a"); - var b = true || expr ? require("./a") : require("fail"); + let expr; + const a = false && expr ? require("fail") : require("./a"); + const b = true || expr ? require("./a") : require("fail"); }); it("should evaluate __dirname and __resourceQuery with replace and substr", function() { - var result = require("./resourceQuery/index?" + __dirname); + const result = require("./resourceQuery/index?" + __dirname); expect(result).toEqual("?resourceQuery"); }); it("should evaluate __dirname and __resourceFragment with replace and substr", function() { - var result = require("./resourceFragment/index#" + __dirname); + const result = require("./resourceFragment/index#" + __dirname); expect(result).toEqual("#resourceFragment"); }); it("should allow resourceFragment in context", function() { - var fn = x => require(`./resourceFragment/${x}#..`); + const fn = x => require(`./resourceFragment/${x}#..`); expect(fn("index")).toEqual("#resourceFragment"); expect(fn("returnRF")).toBe("#..") }); diff --git a/types.d.ts b/types.d.ts index 74b1810ad..f083263b2 100644 --- a/types.d.ts +++ b/types.d.ts @@ -348,6 +348,12 @@ declare abstract class BasicEvaluatedExpression { isIdentifier(): boolean; isWrapped(): boolean; isTemplateString(): boolean; + + /** + * check for "simple" types (inlined) only + */ + isSimpleType(): boolean; + isSameType(basicEvaluatedExpression: BasicEvaluatedExpression): boolean; isTruthy(): boolean; isFalsy(): boolean; isNullish(): any;