Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(types): Improve types coverage & docs for js parser #17094

Merged
merged 2 commits into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/javascript/BasicEvaluatedExpression.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ class BasicEvaluatedExpression {
return this.sideEffects;
}

/**
* Creates a boolean representation of this evaluated expression.
* @returns {boolean | undefined} true: truthy, false: falsy, undefined: unknown
*/
asBool() {
if (this.truthy) return true;
if (this.falsy || this.nullish) return false;
Expand All @@ -247,6 +251,10 @@ class BasicEvaluatedExpression {
return undefined;
}

/**
* Creates a nullish coalescing representation of this evaluated expression.
* @returns {boolean | undefined} true: nullish, false: not nullish, undefined: unknown
*/
asNullish() {
const nullish = this.isNullish();

Expand Down
127 changes: 123 additions & 4 deletions lib/javascript/JavascriptParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,46 @@ class VariableInfo {
* @property {boolean} inTry
*/

/**
* Helper function for joining two ranges into a single range. This is useful
* when working with AST nodes, as it allows you to combine the ranges of child nodes
* to create the range of the _parent node_.
*
* @param {[number, number]} startRange start range to join
* @param {[number, number]} endRange end range to join
* @returns {[number, number]} joined range
*
* @example
* ```js
* const startRange = [0, 5];
* const endRange = [10, 15];
* const joinedRange = joinRanges(startRange, endRange);
* console.log(joinedRange); // [0, 15]
* ```
*
*/
const joinRanges = (startRange, endRange) => {
if (!endRange) return startRange;
if (!startRange) return endRange;
return [startRange[0], endRange[1]];
};

/**
* Helper function used to generate a string representation of a
* [member expression](https://github.com/estree/estree/blob/master/es5.md#memberexpression).
*
* @param {string} object object to name
* @param {string[]} membersReversed reversed list of members
* @returns {string} member expression as a string
* @example
* ```js
* const membersReversed = ["property1", "property2", "property3"]; // Members parsed from the AST
* const name = objectAndMembersToName("myObject", membersReversed);
*
* console.log(name); // "myObject.property1.property2.property3"
* ```
*
*/
const objectAndMembersToName = (object, membersReversed) => {
let name = object;
for (let i = membersReversed.length - 1; i >= 0; i--) {
Expand All @@ -116,6 +150,16 @@ const objectAndMembersToName = (object, membersReversed) => {
return name;
};

/**
* Grabs the name of a given expression and returns it as a string or undefined. Has particular
* handling for [Identifiers](https://github.com/estree/estree/blob/master/es5.md#identifier),
* [ThisExpressions](https://github.com/estree/estree/blob/master/es5.md#identifier), and
* [MetaProperties](https://github.com/estree/estree/blob/master/es2015.md#metaproperty) which is
* specifically for handling the `new.target` meta property.
*
* @param {ExpressionNode | SuperNode} expression expression
* @returns {string | "this" | undefined} name or variable info
*/
const getRootName = expression => {
switch (expression.type) {
case "Identifier":
Expand Down Expand Up @@ -469,6 +513,49 @@ class JavascriptParser extends Parser {
}
});

/**
* In simple logical cases, we can use valueAsExpression to assist us in evaluating the expression on
* either side of a [BinaryExpression](https://github.com/estree/estree/blob/master/es5.md#binaryexpression).
* This supports scenarios in webpack like conditionally `import()`'ing modules based on some simple evaluation:
*
* ```js
* if (1 === 3) {
* import("./moduleA"); // webpack will auto evaluate this and not import the modules
* }
* ```
*
* Additional scenarios include evaluation of strings inside of dynamic import statements:
*
* ```js
* const foo = "foo";
* const bar = "bar";
*
* import("./" + foo + bar); // webpack will auto evaluate this into import("./foobar")
* ```
* @param {boolean | number | BigInt | string} value the value to convert to an expression
* @param {BinaryExpressionNode | UnaryExpressionNode} expr the expression being evaluated
* @param {boolean} sideEffects whether the expression has side effects
* @returns {BasicEvaluatedExpression} the evaluated expression
* @example
*
* ```js
* const binaryExpr = new BinaryExpressionNode("+",
* { type: "Literal", value: 2 },
* { type: "Literal", value: 3 }
* );
*
* const leftValue = 2;
* const rightValue = 3;
*
* const leftExpr = valueAsExpression(leftValue, binaryExpr.left, false);
* const rightExpr = valueAsExpression(rightValue, binaryExpr.right, false);
* const result = new BasicEvaluatedExpression()
* .setNumber(leftExpr.number + rightExpr.number)
* .setRange(binaryExpr.range);
*
* console.log(result.number); // Output: 5
* ```
*/
const valueAsExpression = (value, expr, sideEffects) => {
switch (typeof value) {
case "boolean":
Expand Down Expand Up @@ -499,14 +586,21 @@ class JavascriptParser extends Parser {
.tap("JavascriptParser", _expr => {
const expr = /** @type {BinaryExpressionNode} */ (_expr);

const handleConstOperation = fn => {
/**
* Evaluates a binary expression if and only if it is a const operation (e.g. 1 + 2, "a" + "b", etc.).
*
* @template T
* @param {(leftOperand: T, rightOperand: T) => boolean | number | BigInt | string} operandHandler the handler for the operation (e.g. (a, b) => a + b)
* @returns {BasicEvaluatedExpression | undefined} the evaluated expression
*/
const handleConstOperation = operandHandler => {
const left = this.evaluateExpression(expr.left);
if (!left.isCompileTimeValue()) return;

const right = this.evaluateExpression(expr.right);
if (!right.isCompileTimeValue()) return;

const result = fn(
const result = operandHandler(
left.asCompileTimeValue(),
right.asCompileTimeValue()
);
Expand All @@ -517,6 +611,14 @@ class JavascriptParser extends Parser {
);
};

/**
* Helper function to determine if two booleans are always different. This is used in `handleStrictEqualityComparison`
* to determine if an expressions boolean or nullish conversion is equal or not.
*
* @param {boolean} a first boolean to compare
* @param {boolean} b second boolean to compare
* @returns {boolean} true if the two booleans are always different, false otherwise
*/
const isAlwaysDifferent = (a, b) =>
(a === true && b === false) || (a === false && b === true);

Expand Down Expand Up @@ -560,6 +662,11 @@ class JavascriptParser extends Parser {
}
};

/**
* Helper function to handle BinaryExpressions using strict equality comparisons (e.g. "===" and "!==").
* @param {boolean} eql true for "===" and false for "!=="
* @returns {BasicEvaluatedExpression | undefined} the evaluated expression
*/
const handleStrictEqualityComparison = eql => {
const left = this.evaluateExpression(expr.left);
const right = this.evaluateExpression(expr.right);
Expand Down Expand Up @@ -613,6 +720,11 @@ class JavascriptParser extends Parser {
}
};

/**
* Helper function to handle BinaryExpressions using abstract equality comparisons (e.g. "==" and "!=").
* @param {boolean} eql true for "==" and false for "!="
* @returns {BasicEvaluatedExpression | undefined} the evaluated expression
*/
const handleAbstractEqualityComparison = eql => {
const left = this.evaluateExpression(expr.left);
const right = this.evaluateExpression(expr.right);
Expand Down Expand Up @@ -825,10 +937,17 @@ class JavascriptParser extends Parser {
.tap("JavascriptParser", _expr => {
const expr = /** @type {UnaryExpressionNode} */ (_expr);

const handleConstOperation = fn => {
/**
* Evaluates a UnaryExpression if and only if it is a basic const operator (e.g. +a, -a, ~a).
*
* @template T
* @param {(operand: T) => boolean | number | BigInt | string} operandHandler handler for the operand
* @returns {BasicEvaluatedExpression | undefined} evaluated expression
*/
const handleConstOperation = operandHandler => {
const argument = this.evaluateExpression(expr.argument);
if (!argument.isCompileTimeValue()) return;
const result = fn(argument.asCompileTimeValue());
const result = operandHandler(argument.asCompileTimeValue());
return valueAsExpression(
result,
expr,
Expand Down
22 changes: 21 additions & 1 deletion types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,15 @@ declare abstract class BasicEvaluatedExpression {
* Can this expression have side effects?
*/
couldHaveSideEffects(): boolean;
asBool(): any;
TheLarkInn marked this conversation as resolved.
Show resolved Hide resolved

/**
* Creates a boolean representation of this evaluated expression.
*/
asBool(): undefined | boolean;

/**
* Creates a nullish coalescing representation of this evaluated expression.
*/
asNullish(): undefined | boolean;
asString(): any;
setString(string?: any): BasicEvaluatedExpression;
Expand Down Expand Up @@ -10938,6 +10946,12 @@ declare interface RuntimeValueOptions {
buildDependencies?: string[];
version?: string | (() => string);
}

/**
* Helper function for joining two ranges into a single range. This is useful
* when working with AST nodes, as it allows you to combine the ranges of child nodes
* to create the range of the _parent node_.
*/
declare interface ScopeInfo {
definitions: StackedMap<string, ScopeInfo | VariableInfo>;
topLevelScope: boolean | "arrow";
Expand Down Expand Up @@ -11975,6 +11989,12 @@ declare interface SyntheticDependencyLocation {
declare const TOMBSTONE: unique symbol;
declare const TRANSITIVE: unique symbol;
declare const TRANSITIVE_ONLY: unique symbol;

/**
* Helper function for joining two ranges into a single range. This is useful
* when working with AST nodes, as it allows you to combine the ranges of child nodes
* to create the range of the _parent node_.
*/
declare interface TagInfo {
tag: any;
data: any;
Expand Down