Skip to content

Commit

Permalink
Merge pull request #17094 from webpack/thelarkinn/doc-js-parser-1
Browse files Browse the repository at this point in the history
refactor(types): Improve types coverage & docs for js parser
  • Loading branch information
TheLarkInn committed Apr 28, 2023
2 parents 102e25f + a051a7b commit 7ea3a76
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 5 deletions.
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;

/**
* 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

0 comments on commit 7ea3a76

Please sign in to comment.