refactor equality comparison, support nullish coalescing in ConstPlugin

- add handleStrictEqualityComparison callback
- add handleAbstractEqualityComparison callback
- rework evaluateIdentifier interface
- add tests
This commit is contained in:
Ivan Kopeykin 2020-07-18 15:06:17 +03:00 committed by Tobias Koppers
parent 3ecc87889c
commit 5ec7dfd6ac
7 changed files with 252 additions and 83 deletions

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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("#..")
});

6
types.d.ts vendored
View File

@ -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;