Skip to content

Commit c8c40c8

Browse files
authored
Rework tree-shaking support for optional chaining (#4812)
1 parent 233e562 commit c8c40c8

12 files changed

Lines changed: 136 additions & 33 deletions

File tree

src/ast/nodes/CallExpression.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@ import type SpreadElement from './SpreadElement';
2121
import type Super from './Super';
2222
import CallExpressionBase from './shared/CallExpressionBase';
2323
import { type ExpressionEntity, UNKNOWN_RETURN_EXPRESSION } from './shared/Expression';
24+
import type { ChainElement } from './shared/Node';
2425
import { type ExpressionNode, INCLUDE_PARAMETERS, type IncludeChildren } from './shared/Node';
2526

26-
export default class CallExpression extends CallExpressionBase implements DeoptimizableEntity {
27+
export default class CallExpression
28+
extends CallExpressionBase
29+
implements DeoptimizableEntity, ChainElement
30+
{
2731
declare arguments: (ExpressionNode | SpreadElement)[];
2832
declare callee: ExpressionNode | Super;
2933
declare optional: boolean;
@@ -90,6 +94,14 @@ export default class CallExpression extends CallExpressionBase implements Deopti
9094
this.callee.includeCallArguments(context, this.arguments);
9195
}
9296

97+
isSkippedAsOptional(origin: DeoptimizableEntity): boolean {
98+
return (
99+
(this.callee as ExpressionNode).isSkippedAsOptional?.(origin) ||
100+
(this.optional &&
101+
this.callee.getLiteralValueAtPath(EMPTY_PATH, SHARED_RECURSION_TRACKER, origin) == null)
102+
);
103+
}
104+
93105
render(
94106
code: MagicString,
95107
options: RenderOptions,

src/ast/nodes/ChainExpression.ts

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,30 @@
11
import type { DeoptimizableEntity } from '../DeoptimizableEntity';
22
import type { HasEffectsContext } from '../ExecutionContext';
3-
import { SHARED_RECURSION_TRACKER } from '../utils/PathTracker';
4-
import { EMPTY_PATH } from '../utils/PathTracker';
3+
import type { ObjectPath, PathTracker } from '../utils/PathTracker';
54
import type CallExpression from './CallExpression';
65
import type MemberExpression from './MemberExpression';
76
import type * as NodeType from './NodeType';
87
import type { LiteralValueOrUnknown } from './shared/Expression';
9-
import { UnknownValue } from './shared/Expression';
108
import { NodeBase } from './shared/Node';
119

12-
const unset = Symbol('unset');
13-
1410
export default class ChainExpression extends NodeBase implements DeoptimizableEntity {
1511
declare expression: CallExpression | MemberExpression;
1612
declare type: NodeType.tChainExpression;
17-
private objectValue: LiteralValueOrUnknown | typeof unset = unset;
1813

19-
deoptimizeCache(): void {
20-
this.objectValue = UnknownValue;
21-
}
14+
// deoptimizations are not relevant as we are not caching values
15+
deoptimizeCache(): void {}
2216

23-
getLiteralValueAtPath(): LiteralValueOrUnknown {
24-
if (this.getObjectValue() == null) return undefined;
25-
return UnknownValue;
17+
getLiteralValueAtPath(
18+
path: ObjectPath,
19+
recursionTracker: PathTracker,
20+
origin: DeoptimizableEntity
21+
): LiteralValueOrUnknown {
22+
if (this.expression.isSkippedAsOptional(origin)) return undefined;
23+
return this.expression.getLiteralValueAtPath(path, recursionTracker, origin);
2624
}
2725

2826
hasEffects(context: HasEffectsContext): boolean {
29-
if (this.getObjectValue() == null) return false;
27+
if (this.expression.isSkippedAsOptional(this)) return false;
3028
return this.expression.hasEffects(context);
3129
}
32-
33-
private getObjectValue() {
34-
if (this.objectValue === unset) {
35-
let object =
36-
this.expression.type === 'CallExpression' ? this.expression.callee : this.expression.object;
37-
if (object.type === 'MemberExpression') object = (object as MemberExpression).object;
38-
this.objectValue = object.getLiteralValueAtPath(EMPTY_PATH, SHARED_RECURSION_TRACKER, this);
39-
}
40-
return this.objectValue;
41-
}
4230
}

src/ast/nodes/MemberExpression.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
UNKNOWN_RETURN_EXPRESSION,
4242
UnknownValue
4343
} from './shared/Expression';
44+
import type { ChainElement } from './shared/Node';
4445
import { type ExpressionNode, type IncludeChildren, NodeBase } from './shared/Node';
4546

4647
// To avoid infinite recursions
@@ -89,7 +90,10 @@ function getStringFromPath(path: PathWithPositions): string {
8990
return pathString;
9091
}
9192

92-
export default class MemberExpression extends NodeBase implements DeoptimizableEntity {
93+
export default class MemberExpression
94+
extends NodeBase
95+
implements DeoptimizableEntity, ChainElement
96+
{
9397
declare computed: boolean;
9498
declare object: ExpressionNode | Super;
9599
declare optional: boolean;
@@ -108,7 +112,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE
108112
this.bound = true;
109113
const path = getPathIfNotComputed(this);
110114
const baseVariable = path && this.scope.findVariable(path[0].key);
111-
if (baseVariable && baseVariable.isNamespace) {
115+
if (baseVariable?.isNamespace) {
112116
const resolvedVariable = resolveNamespaceVariables(
113117
baseVariable,
114118
path!.slice(1),
@@ -296,6 +300,16 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE
296300
this.accessInteraction = { thisArg: this.object, type: INTERACTION_ACCESSED };
297301
}
298302

303+
isSkippedAsOptional(origin: DeoptimizableEntity): boolean {
304+
return (
305+
!this.variable &&
306+
!this.isUndefined &&
307+
((this.object as ExpressionNode).isSkippedAsOptional?.(origin) ||
308+
(this.optional &&
309+
this.object.getLiteralValueAtPath(EMPTY_PATH, SHARED_RECURSION_TRACKER, origin) == null))
310+
);
311+
}
312+
299313
render(
300314
code: MagicString,
301315
options: RenderOptions,
@@ -440,9 +454,12 @@ function resolveNamespaceVariables(
440454
const exportName = path[0].key;
441455
const variable = (baseVariable as NamespaceVariable).context.traceExport(exportName);
442456
if (!variable) {
443-
const fileName = (baseVariable as NamespaceVariable).context.fileName;
444-
astContext.warn(errorMissingExport(exportName, astContext.module.id, fileName), path[0].pos);
445-
return 'undefined';
457+
if (path.length === 1) {
458+
const fileName = (baseVariable as NamespaceVariable).context.fileName;
459+
astContext.warn(errorMissingExport(exportName, astContext.module.id, fileName), path[0].pos);
460+
return 'undefined';
461+
}
462+
return null;
446463
}
447464
return resolveNamespaceVariables(variable, path.slice(1), astContext);
448465
}

src/ast/nodes/shared/Node.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type MagicString from 'magic-string';
44
import type { AstContext } from '../../../Module';
55
import { ANNOTATION_KEY, INVALID_COMMENT_KEY } from '../../../utils/pureComments';
66
import type { NodeRenderOptions, RenderOptions } from '../../../utils/renderHelpers';
7+
import type { DeoptimizableEntity } from '../../DeoptimizableEntity';
78
import type { Entity } from '../../Entity';
89
import {
910
createHasEffectsContext,
@@ -113,7 +114,14 @@ export interface Node extends Entity {
113114

114115
export type StatementNode = Node;
115116

116-
export interface ExpressionNode extends ExpressionEntity, Node {}
117+
export interface ExpressionNode extends ExpressionEntity, Node {
118+
isSkippedAsOptional?(origin: DeoptimizableEntity): boolean;
119+
}
120+
121+
export interface ChainElement extends ExpressionNode {
122+
optional: boolean;
123+
isSkippedAsOptional(origin: DeoptimizableEntity): boolean;
124+
}
117125

118126
export class NodeBase extends ExpressionEntity implements ExpressionNode {
119127
declare annotations?: acorn.Comment[];
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
description: 'supports optional chaining with namespace objects',
3+
expectedWarnings: ['MISSING_EXPORT']
4+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const foo = { nullVal: null };
2+
3+
foo?.x.x; // retained
4+
5+
undefined.x; // retained
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as util from './util';
2+
3+
util.foo.x; // removed
4+
util.foo.nullVal; // removed
5+
util.foo.nullVal?.x; // removed
6+
util.foo.nullVal?.x.y; // removed
7+
util.foo.nullVal?.(); // removed
8+
util.foo.nullVal?.().x(); // removed
9+
10+
util.foo?.x.x; // retained
11+
12+
util.x; // removed
13+
util.x?.x; // removed
14+
util.x?.x.y; // removed
15+
util.x?.(); // removed
16+
util.x?.().x(); // removed
17+
18+
util?.x.x; // retained
19+
20+
if (
21+
util.foo.nullVal ||
22+
util.foo.nullVal?.x ||
23+
util.foo.nullVal?.x.y ||
24+
util.foo.nullVal?.() ||
25+
util.foo.nullVal?.().x()
26+
) {
27+
console.log('removed');
28+
}
29+
30+
if (util.x || util.x?.x || util.x?.x.y || util.x?.() || util.x?.().x()) {
31+
console.log('removed');
32+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const foo = { nullVal: null };
Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1-
const obj = { __proto__: null };
2-
obj.foo?.bar;
3-
obj.foo?.();
1+
Object.defineProperty(Object.prototype, 'foo', {
2+
get bar() {
3+
console.log('effect');
4+
}
5+
});
6+
const obj2 = {};
7+
obj2.foo?.bar;
8+
obj2.foo?.();

test/form/samples/optional-chaining/main.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,12 @@ const obj = { __proto__: null };
22
obj?.foo;
33
obj.foo?.bar;
44
obj.foo?.();
5+
6+
Object.defineProperty(Object.prototype, 'foo', {
7+
get bar() {
8+
console.log('effect');
9+
}
10+
});
11+
const obj2 = {};
12+
obj2.foo?.bar;
13+
obj2.foo?.();

0 commit comments

Comments
 (0)