refactor evaluation logic

This commit is contained in:
Tobias Koppers 2020-07-20 12:07:08 +02:00
parent 5876cf6210
commit bc1c5a8f23
5 changed files with 255 additions and 199 deletions

View File

@ -175,6 +175,7 @@
"darkgreen",
"darkred",
"eqeqeq",
"boolish",
"webassemblyjs",
"fsevents",

View File

@ -24,22 +24,38 @@ class BasicEvaluatedExpression {
constructor() {
this.type = TypeUnknown;
this.range = undefined;
/** @type {boolean} */
this.falsy = false;
/** @type {boolean} */
this.truthy = false;
/** @type {boolean | undefined} */
this.nullish = undefined;
/** @type {boolean | undefined} */
this.bool = undefined;
/** @type {number | undefined} */
this.number = undefined;
/** @type {bigint | undefined} */
this.bigint = undefined;
/** @type {RegExp | undefined} */
this.regExp = undefined;
/** @type {string | undefined} */
this.string = undefined;
/** @type {BasicEvaluatedExpression[] | undefined} */
this.quasis = undefined;
/** @type {BasicEvaluatedExpression[] | undefined} */
this.parts = undefined;
/** @type {any[] | undefined} */
this.array = undefined;
/** @type {BasicEvaluatedExpression[] | undefined} */
this.items = undefined;
/** @type {BasicEvaluatedExpression[] | undefined} */
this.options = undefined;
/** @type {BasicEvaluatedExpression | undefined} */
this.prefix = undefined;
/** @type {BasicEvaluatedExpression | undefined} */
this.postfix = undefined;
this.wrappedInnerExpressions = undefined;
/** @type {string | undefined} */
this.identifier = undefined;
this.rootInfo = undefined;
this.getMembers = undefined;
@ -99,34 +115,83 @@ class BasicEvaluatedExpression {
}
/**
* check for "simple" types (inlined) only
* @returns {boolean} is simple type
* Is expression a primitive or an object type value?
* @returns {boolean | undefined} true: primitive type, false: object type, undefined: unknown/runtime-defined
*/
isSimpleType() {
isPrimitiveType() {
switch (this.type) {
case TypeIdentifier:
case TypeConditional:
case TypeUndefined:
case TypeNull:
case TypeString:
case TypeNumber:
case TypeBoolean:
case TypeBigInt:
case TypeWrapped:
case TypeUnknown:
case TypeTemplateString:
return true;
case TypeRegExp:
case TypeArray:
case TypeConstArray:
return false;
default:
return true;
return undefined;
}
}
/**
* @param {BasicEvaluatedExpression} basicEvaluatedExpression basicEvaluatedExpression
* @returns {boolean|undefined} is same type
* Is expression a runtime or compile-time value?
* @returns {boolean} true: compile time value, false: runtime value
*/
isSameType(basicEvaluatedExpression) {
if (
this.type === TypeUnknown ||
basicEvaluatedExpression.type === TypeUnknown
) {
return undefined;
isCompileTimeValue() {
switch (this.type) {
case TypeUndefined:
case TypeNull:
case TypeString:
case TypeNumber:
case TypeBoolean:
case TypeRegExp:
case TypeConstArray:
case TypeBigInt:
return true;
case TypeWrapped:
case TypeIdentifier:
case TypeConditional:
case TypeArray:
case TypeTemplateString:
case TypeUnknown:
return false;
default:
return false;
}
}
return this.type === basicEvaluatedExpression.type;
/**
* Gets the compile-time value of the expression
* @returns {any} the javascript value
*/
asCompileTimeValue() {
switch (this.type) {
case TypeUndefined:
return undefined;
case TypeNull:
return null;
case TypeString:
return this.string;
case TypeNumber:
return this.number;
case TypeBoolean:
return this.bool;
case TypeRegExp:
return this.regExp;
case TypeConstArray:
return this.array;
case TypeBigInt:
return this.bigint;
default:
throw new Error(
"asCompileTimeValue must only be called for compile-time values"
);
}
}
isTruthy() {

View File

@ -333,32 +333,27 @@ class JavascriptParser extends Parser {
.tap("JavascriptParser", _expr => {
const expr = /** @type {LogicalExpressionNode} */ (_expr);
let left;
let leftAsBool;
let right;
if (expr.operator === "&&") {
left = this.evaluateExpression(expr.left);
leftAsBool = left && left.asBool();
const left = this.evaluateExpression(expr.left);
const leftAsBool = left && left.asBool();
if (leftAsBool === false) return left.setRange(expr.range);
if (leftAsBool !== true) return;
right = this.evaluateExpression(expr.right);
const right = this.evaluateExpression(expr.right);
return right.setRange(expr.range);
} else if (expr.operator === "||") {
left = this.evaluateExpression(expr.left);
leftAsBool = left && left.asBool();
const left = this.evaluateExpression(expr.left);
const leftAsBool = left && left.asBool();
if (leftAsBool === true) return left.setRange(expr.range);
if (leftAsBool !== false) return;
right = this.evaluateExpression(expr.right);
const right = this.evaluateExpression(expr.right);
return right.setRange(expr.range);
} else if (expr.operator === "??") {
left = this.evaluateExpression(expr.left);
const left = this.evaluateExpression(expr.left);
const leftAsNullish = left && left.asNullish();
if (leftAsNullish === false) return left.setRange(expr.range);
if (leftAsNullish === true) {
right = this.evaluateExpression(expr.right);
}
return right && right.setRange(expr.range);
if (leftAsNullish !== true) return;
const right = this.evaluateExpression(expr.right);
return right.setRange(expr.range);
}
});
this.hooks.evaluate
@ -366,162 +361,149 @@ class JavascriptParser extends Parser {
.tap("JavascriptParser", _expr => {
const expr = /** @type {BinaryExpressionNode} */ (_expr);
const handleNumberOperation = fn => {
const handleConstOperation = fn => {
const left = this.evaluateExpression(expr.left);
if (!left || !left.isCompileTimeValue()) return;
const right = this.evaluateExpression(expr.right);
if (!left || !right) return;
if (left.isNumber() && right.isNumber()) {
res = new BasicEvaluatedExpression();
res.setNumber(fn(left.number, right.number));
res.setRange(expr.range);
return res;
} else if (left.isBigInt() && right.isBigInt()) {
res = new BasicEvaluatedExpression();
res.setBigInt(fn(left.bigint, right.bigint));
res.setRange(expr.range);
return res;
if (!right || !right.isCompileTimeValue()) return;
const result = fn(
left.asCompileTimeValue(),
right.asCompileTimeValue()
);
switch (typeof result) {
case "boolean":
return new BasicEvaluatedExpression()
.setBoolean(result)
.setRange(expr.range);
case "number":
return new BasicEvaluatedExpression()
.setNumber(result)
.setRange(expr.range);
case "bigint":
return new BasicEvaluatedExpression()
.setBigInt(result)
.setRange(expr.range);
case "string":
return new BasicEvaluatedExpression()
.setString(result)
.setRange(expr.range);
}
};
const handleStrictCompare = fn => {
const left = this.evaluateExpression(expr.left);
const right = this.evaluateExpression(expr.right);
if (!left || !right) return;
const sameType = left.isSameType(right);
if (sameType !== true) return;
const isAlwaysDifferent = (a, b) =>
(a === true && b === false) || (a === false && b === true);
if (left.isNumber()) {
res = new BasicEvaluatedExpression();
res.setBoolean(fn(left.number, right.number));
res.setRange(expr.range);
return res;
} else if (left.isString()) {
res = new BasicEvaluatedExpression();
res.setBoolean(fn(left.string, right.string));
res.setRange(expr.range);
return res;
} else if (left.isBigInt()) {
res = new BasicEvaluatedExpression();
res.setBoolean(fn(left.bigint, right.bigint));
res.setRange(expr.range);
return res;
const handleTemplateStringCompare = (left, right, res, eql) => {
const getPrefix = parts => {
let value = "";
for (const p of parts) {
const v = p.asString();
if (v !== undefined) value += v;
else break;
}
return value;
};
const getSuffix = parts => {
let value = "";
for (let i = parts.length - 1; i >= 0; i--) {
const v = parts[i].asString();
if (v !== undefined) value = v + value;
else break;
}
return value;
};
const leftPrefix = getPrefix(left.parts);
const rightPrefix = getPrefix(right.parts);
const leftSuffix = getSuffix(left.parts);
const rightSuffix = getSuffix(right.parts);
const lenPrefix = Math.min(leftPrefix.length, rightPrefix.length);
const lenSuffix = Math.min(leftSuffix.length, rightSuffix.length);
if (
leftPrefix.slice(0, lenPrefix) !==
rightPrefix.slice(0, lenPrefix) ||
leftSuffix.slice(-lenSuffix) !== rightSuffix.slice(-lenSuffix)
) {
return res.setBoolean(!eql);
}
};
const handleStrictEqualityComparison = eql => {
left = this.evaluateExpression(expr.left);
right = this.evaluateExpression(expr.right);
const left = this.evaluateExpression(expr.left);
const right = this.evaluateExpression(expr.right);
if (!left || !right) return;
const sameType = left.isSameType(right);
if (sameType === undefined) return;
res = new BasicEvaluatedExpression();
const 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);
}
const leftConst = left.isCompileTimeValue();
const rightConst = right.isCompileTimeValue();
if (leftConst && rightConst) {
return res.setBoolean(
eql === (left.asCompileTimeValue() === right.asCompileTimeValue())
);
}
// check for "simple" types, e.g.
// if ([] === 1)
// if ([] === [])
// if (0 === null)
// if ("" === /a/i)
if (left.isSimpleType() && right.isSimpleType()) {
if (left.isArray() && right.isArray()) {
return res.setBoolean(!eql);
}
if (left.isTemplateString() && right.isTemplateString()) {
return handleTemplateStringCompare(left, right, res, eql);
}
const leftPrimitive = left.isPrimitiveType();
const rightPrimitive = right.isPrimitiveType();
// Primitive !== Object or
// compile-time object types are never equal to something at runtime
if (
(leftPrimitive === false &&
(leftConst || rightPrimitive === true)) ||
(rightPrimitive === false && (rightConst || leftPrimitive === true))
) {
return res.setBoolean(!eql);
}
// Different nullish or boolish status also means not equal
if (
isAlwaysDifferent(left.asBool(), right.asBool()) ||
isAlwaysDifferent(left.asNullish(), right.asNullish())
) {
return res.setBoolean(!eql);
}
};
const handleAbstractEqualityComparison = eql => {
left = this.evaluateExpression(expr.left);
right = this.evaluateExpression(expr.right);
const left = this.evaluateExpression(expr.left);
const right = this.evaluateExpression(expr.right);
if (!left || !right) return;
const sameType = left.isSameType(right);
if (sameType === undefined) return;
res = new BasicEvaluatedExpression();
const 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);
}
const leftConst = left.isCompileTimeValue();
const rightConst = right.isCompileTimeValue();
if (leftConst && rightConst) {
return res.setBoolean(
// eslint-disable-next-line eqeqeq
eql === (left.asCompileTimeValue() == right.asCompileTimeValue())
);
}
if (
(left.isFalsy() && right.isTruthy()) ||
(left.isTruthy() && right.isFalsy())
) {
if (left.isArray() && right.isArray()) {
return res.setBoolean(!eql);
}
// abstract equality comparison is not fully implemented
return undefined;
if (left.isTemplateString() && right.isTemplateString()) {
return handleTemplateStringCompare(left, right, res, eql);
}
};
let left;
let right;
let res;
if (expr.operator === "+") {
left = this.evaluateExpression(expr.left);
right = this.evaluateExpression(expr.right);
const left = this.evaluateExpression(expr.left);
const right = this.evaluateExpression(expr.right);
if (!left || !right) return;
res = new BasicEvaluatedExpression();
const res = new BasicEvaluatedExpression();
if (left.isString()) {
if (right.isString()) {
res.setString(left.string + right.string);
@ -653,13 +635,13 @@ class JavascriptParser extends Parser {
res.setRange(expr.range);
return res;
} else if (expr.operator === "-") {
return handleNumberOperation((l, r) => l - r);
return handleConstOperation((l, r) => l - r);
} else if (expr.operator === "*") {
return handleNumberOperation((l, r) => l * r);
return handleConstOperation((l, r) => l * r);
} else if (expr.operator === "/") {
return handleNumberOperation((l, r) => l / r);
return handleConstOperation((l, r) => l / r);
} else if (expr.operator === "**") {
return handleNumberOperation((l, r) => l ** r);
return handleConstOperation((l, r) => l ** r);
} else if (expr.operator === "===") {
return handleStrictEqualityComparison(true);
} else if (expr.operator === "==") {
@ -669,32 +651,25 @@ class JavascriptParser extends Parser {
} else if (expr.operator === "!=") {
return handleAbstractEqualityComparison(false);
} else if (expr.operator === "&") {
return handleNumberOperation((l, r) => l & r);
return handleConstOperation((l, r) => l & r);
} else if (expr.operator === "|") {
return handleNumberOperation((l, r) => l | r);
return handleConstOperation((l, r) => l | r);
} else if (expr.operator === "^") {
return handleNumberOperation((l, r) => l ^ r);
return handleConstOperation((l, r) => l ^ r);
} else if (expr.operator === ">>>") {
const left = this.evaluateExpression(expr.left);
const right = this.evaluateExpression(expr.right);
if (!left || !right) return;
if (!left.isNumber() || !right.isNumber()) return;
res = new BasicEvaluatedExpression();
res.setNumber(left.number >>> right.number);
res.setRange(expr.range);
return res;
return handleConstOperation((l, r) => l >>> r);
} else if (expr.operator === ">>") {
return handleNumberOperation((l, r) => l >> r);
return handleConstOperation((l, r) => l >> r);
} else if (expr.operator === "<<") {
return handleNumberOperation((l, r) => l << r);
return handleConstOperation((l, r) => l << r);
} else if (expr.operator === "<") {
return handleStrictCompare((l, r) => l < r);
return handleConstOperation((l, r) => l < r);
} else if (expr.operator === ">") {
return handleStrictCompare((l, r) => l > r);
return handleConstOperation((l, r) => l > r);
} else if (expr.operator === "<=") {
return handleStrictCompare((l, r) => l <= r);
return handleConstOperation((l, r) => l <= r);
} else if (expr.operator === ">=") {
return handleStrictCompare((l, r) => l >= r);
return handleConstOperation((l, r) => l >= r);
}
});
this.hooks.evaluate
@ -960,11 +935,11 @@ class JavascriptParser extends Parser {
let arg1 = this.evaluateExpression(expr.arguments[0]);
let arg2 = this.evaluateExpression(expr.arguments[1]);
if (!arg1.isString() && !arg1.isRegExp()) return;
arg1 = arg1.regExp || arg1.string;
const arg1Value = arg1.regExp || arg1.string;
if (!arg2.isString()) return;
arg2 = arg2.string;
const arg2Value = arg2.string;
return new BasicEvaluatedExpression()
.setString(param.string.replace(arg1, arg2))
.setString(param.string.replace(arg1Value, arg2Value))
.setRange(expr.range);
});
["substr", "substring"].forEach(fn => {

View File

@ -17,6 +17,10 @@ it("should evaluate logical expression", function () {
const value9 = null === 1 && require("fail");
const value91 = [] === [] && require("fail");
const value92 = /a/ === /a/ && require("fail");
const value93 =
`hello${Math.random()}` === `world${Math.random()}` && require("fail");
const value94 =
`${Math.random()}hello` != `${Math.random()}world` || require("fail");
expect(value1).toBe("hello");
expect(value2).toBe(true);
@ -29,6 +33,8 @@ it("should evaluate logical expression", function () {
expect(value9).toBe(false);
expect(value91).toBe(false);
expect(value92).toBe(false);
expect(value93).toBe(false);
expect(value94).toBe(true);
});
it("shouldn't evaluate expression", function () {

47
types.d.ts vendored
View File

@ -317,21 +317,21 @@ declare abstract class BasicEvaluatedExpression {
range: any;
falsy: boolean;
truthy: boolean;
nullish: any;
bool: any;
number: any;
bigint: any;
regExp: any;
string: any;
quasis: any;
parts: any;
array: any;
items: any;
options: any;
prefix: any;
postfix: any;
nullish: boolean;
bool: boolean;
number: number;
bigint: bigint;
regExp: RegExp;
string: string;
quasis: BasicEvaluatedExpression[];
parts: BasicEvaluatedExpression[];
array: any[];
items: BasicEvaluatedExpression[];
options: BasicEvaluatedExpression[];
prefix: BasicEvaluatedExpression;
postfix: BasicEvaluatedExpression;
wrappedInnerExpressions: any;
identifier: any;
identifier: string;
rootInfo: any;
getMembers: any;
expression: any;
@ -350,13 +350,22 @@ declare abstract class BasicEvaluatedExpression {
isTemplateString(): boolean;
/**
* check for "simple" types (inlined) only
* Is expression a primitive or an object type value?
*/
isSimpleType(): boolean;
isSameType(basicEvaluatedExpression: BasicEvaluatedExpression): boolean;
isPrimitiveType(): boolean;
/**
* Is expression a runtime or compile-time value?
*/
isCompileTimeValue(): boolean;
/**
* Gets the compile-time value of the expression
*/
asCompileTimeValue(): any;
isTruthy(): boolean;
isFalsy(): boolean;
isNullish(): any;
isNullish(): boolean;
asBool(): any;
asNullish(): boolean;
asString(): any;
@ -3321,7 +3330,7 @@ declare abstract class JavascriptParser extends Parser {
statementStartPos: any;
currentTagData: any;
initializeEvaluating(): void;
getRenameIdentifier(expr?: any): any;
getRenameIdentifier(expr?: any): string;
walkClass(classy: ClassExpression | ClassDeclaration): void;
walkMethodDefinition(methodDefinition?: any): void;
preWalkStatements(statements?: any): void;