Skip to content

Commit f3a1fa3

Browse files
authored
Handle getters on functions and improve property deoptimization (#4493)
* Do not make Object.defineProperty/ies a side effect by default * Detect side effects for getters on functions * Less deoptimization for non-accessor assignments * Use ObjectEntity for arrow function properties * Share code between functions and arrow functions * Use new path key for defineProperty side effects * Enable custom call-effect detection per global * Improve coverage
1 parent 8c6e0f3 commit f3a1fa3

File tree

25 files changed

+429
-257
lines changed

25 files changed

+429
-257
lines changed

src/ast/Entity.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ export interface WritableEntity extends Entity {
1212
* expression of this node is reassigned as well.
1313
*/
1414
deoptimizePath(path: ObjectPath): void;
15+
1516
hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean;
1617
}
Lines changed: 16 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,40 @@
1-
import type { NormalizedTreeshakingOptions } from '../../rollup/types';
2-
import { type CallOptions, NO_ARGS } from '../CallOptions';
3-
import {
4-
BROKEN_FLOW_NONE,
5-
type HasEffectsContext,
6-
type InclusionContext
7-
} from '../ExecutionContext';
1+
import { type CallOptions } from '../CallOptions';
2+
import { type HasEffectsContext, InclusionContext } from '../ExecutionContext';
83
import ReturnValueScope from '../scopes/ReturnValueScope';
94
import type Scope from '../scopes/Scope';
10-
import { type ObjectPath, UNKNOWN_PATH, UnknownKey } from '../utils/PathTracker';
5+
import { type ObjectPath } from '../utils/PathTracker';
116
import BlockStatement from './BlockStatement';
127
import Identifier from './Identifier';
138
import * as NodeType from './NodeType';
14-
import RestElement from './RestElement';
15-
import type SpreadElement from './SpreadElement';
16-
import { type ExpressionEntity, UNKNOWN_EXPRESSION } from './shared/Expression';
17-
import {
18-
type ExpressionNode,
19-
type GenericEsTreeNode,
20-
type IncludeChildren,
21-
NodeBase
22-
} from './shared/Node';
9+
import FunctionBase from './shared/FunctionBase';
10+
import { type ExpressionNode, IncludeChildren } from './shared/Node';
11+
import { ObjectEntity } from './shared/ObjectEntity';
12+
import { OBJECT_PROTOTYPE } from './shared/ObjectPrototype';
2313
import type { PatternNode } from './shared/Pattern';
2414

25-
export default class ArrowFunctionExpression extends NodeBase {
15+
export default class ArrowFunctionExpression extends FunctionBase {
2616
declare async: boolean;
2717
declare body: BlockStatement | ExpressionNode;
2818
declare params: readonly PatternNode[];
2919
declare preventChildBlockScope: true;
3020
declare scope: ReturnValueScope;
3121
declare type: NodeType.tArrowFunctionExpression;
32-
private deoptimizedReturn = false;
22+
protected objectEntity: ObjectEntity | null = null;
3323

3424
createScope(parentScope: Scope): void {
3525
this.scope = new ReturnValueScope(parentScope, this.context);
3626
}
3727

38-
deoptimizePath(path: ObjectPath): void {
39-
// A reassignment of UNKNOWN_PATH is considered equivalent to having lost track
40-
// which means the return expression needs to be reassigned
41-
if (path.length === 1 && path[0] === UnknownKey) {
42-
this.scope.getReturnExpression().deoptimizePath(UNKNOWN_PATH);
43-
}
44-
}
45-
46-
// Arrow functions do not mutate their context
47-
deoptimizeThisOnEventAtPath(): void {}
48-
49-
getReturnExpressionWhenCalledAtPath(path: ObjectPath): ExpressionEntity {
50-
if (path.length !== 0) {
51-
return UNKNOWN_EXPRESSION;
52-
}
53-
if (this.async) {
54-
if (!this.deoptimizedReturn) {
55-
this.deoptimizedReturn = true;
56-
this.scope.getReturnExpression().deoptimizePath(UNKNOWN_PATH);
57-
this.context.requestTreeshakingPass();
58-
}
59-
return UNKNOWN_EXPRESSION;
60-
}
61-
return this.scope.getReturnExpression();
62-
}
63-
6428
hasEffects(): boolean {
6529
return false;
6630
}
6731

68-
hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean {
69-
return path.length > 1;
70-
}
71-
72-
hasEffectsWhenAssignedAtPath(path: ObjectPath): boolean {
73-
return path.length > 1;
74-
}
75-
7632
hasEffectsWhenCalledAtPath(
7733
path: ObjectPath,
78-
_callOptions: CallOptions,
34+
callOptions: CallOptions,
7935
context: HasEffectsContext
8036
): boolean {
81-
if (path.length > 0) return true;
82-
if (this.async) {
83-
const { propertyReadSideEffects } = this.context.options
84-
.treeshake as NormalizedTreeshakingOptions;
85-
const returnExpression = this.scope.getReturnExpression();
86-
if (
87-
returnExpression.hasEffectsWhenCalledAtPath(
88-
['then'],
89-
{ args: NO_ARGS, thisParam: null, withNew: false },
90-
context
91-
) ||
92-
(propertyReadSideEffects &&
93-
(propertyReadSideEffects === 'always' ||
94-
returnExpression.hasEffectsWhenAccessedAtPath(['then'], context)))
95-
) {
96-
return true;
97-
}
98-
}
99-
for (const param of this.params) {
100-
if (param.hasEffects(context)) return true;
101-
}
37+
if (super.hasEffectsWhenCalledAtPath(path, callOptions, context)) return true;
10238
const { ignore, brokenFlow } = context;
10339
context.ignore = {
10440
breaks: false,
@@ -113,43 +49,18 @@ export default class ArrowFunctionExpression extends NodeBase {
11349
}
11450

11551
include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void {
116-
this.included = true;
52+
super.include(context, includeChildrenRecursively);
11753
for (const param of this.params) {
11854
if (!(param instanceof Identifier)) {
11955
param.include(context, includeChildrenRecursively);
12056
}
12157
}
122-
const { brokenFlow } = context;
123-
context.brokenFlow = BROKEN_FLOW_NONE;
124-
this.body.include(context, includeChildrenRecursively);
125-
context.brokenFlow = brokenFlow;
126-
}
127-
128-
includeCallArguments(
129-
context: InclusionContext,
130-
args: readonly (ExpressionNode | SpreadElement)[]
131-
): void {
132-
this.scope.includeCallArguments(context, args);
133-
}
134-
135-
initialise(): void {
136-
this.scope.addParameterVariables(
137-
this.params.map(param => param.declare('parameter', UNKNOWN_EXPRESSION)),
138-
this.params[this.params.length - 1] instanceof RestElement
139-
);
140-
if (this.body instanceof BlockStatement) {
141-
this.body.addImplicitReturnExpressionToScope();
142-
} else {
143-
this.scope.addReturnExpression(this.body);
144-
}
14558
}
14659

147-
parseNode(esTreeNode: GenericEsTreeNode): void {
148-
if (esTreeNode.body.type === NodeType.BlockStatement) {
149-
this.body = new BlockStatement(esTreeNode.body, this, this.scope.hoistedBodyVarScope);
60+
protected getObjectEntity(): ObjectEntity {
61+
if (this.objectEntity !== null) {
62+
return this.objectEntity;
15063
}
151-
super.parseNode(esTreeNode);
64+
return (this.objectEntity = new ObjectEntity([], OBJECT_PROTOTYPE));
15265
}
15366
}
154-
155-
ArrowFunctionExpression.prototype.preventChildBlockScope = true;

src/ast/nodes/MemberExpression.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import {
1414
type PathTracker,
1515
SHARED_RECURSION_TRACKER,
1616
UNKNOWN_PATH,
17-
UnknownKey
17+
UnknownKey,
18+
UnknownNonAccessorKey
1819
} from '../utils/PathTracker';
1920
import ExternalVariable from '../variables/ExternalVariable';
2021
import type NamespaceVariable from '../variables/NamespaceVariable';
@@ -128,7 +129,11 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE
128129
this.variable.deoptimizePath(path);
129130
} else if (!this.replacement) {
130131
if (path.length < MAX_PATH_DEPTH) {
131-
this.object.deoptimizePath([this.getPropertyKey(), ...path]);
132+
const propertyKey = this.getPropertyKey();
133+
this.object.deoptimizePath([
134+
propertyKey === UnknownKey ? UnknownNonAccessorKey : propertyKey,
135+
...path
136+
]);
132137
}
133138
}
134139
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import type { NormalizedTreeshakingOptions } from '../../../rollup/types';
2+
import { type CallOptions, NO_ARGS } from '../../CallOptions';
3+
import { DeoptimizableEntity } from '../../DeoptimizableEntity';
4+
import {
5+
BROKEN_FLOW_NONE,
6+
type HasEffectsContext,
7+
type InclusionContext
8+
} from '../../ExecutionContext';
9+
import { NodeEvent } from '../../NodeEvents';
10+
import ReturnValueScope from '../../scopes/ReturnValueScope';
11+
import { type ObjectPath, PathTracker, UNKNOWN_PATH, UnknownKey } from '../../utils/PathTracker';
12+
import BlockStatement from '../BlockStatement';
13+
import * as NodeType from '../NodeType';
14+
import RestElement from '../RestElement';
15+
import type SpreadElement from '../SpreadElement';
16+
import { type ExpressionEntity, LiteralValueOrUnknown, UNKNOWN_EXPRESSION } from './Expression';
17+
import {
18+
type ExpressionNode,
19+
type GenericEsTreeNode,
20+
type IncludeChildren,
21+
NodeBase
22+
} from './Node';
23+
import { ObjectEntity } from './ObjectEntity';
24+
import type { PatternNode } from './Pattern';
25+
26+
export default abstract class FunctionBase extends NodeBase {
27+
declare async: boolean;
28+
declare body: BlockStatement | ExpressionNode;
29+
declare params: readonly PatternNode[];
30+
declare preventChildBlockScope: true;
31+
declare scope: ReturnValueScope;
32+
protected objectEntity: ObjectEntity | null = null;
33+
private deoptimizedReturn = false;
34+
35+
deoptimizePath(path: ObjectPath): void {
36+
this.getObjectEntity().deoptimizePath(path);
37+
if (path.length === 1 && path[0] === UnknownKey) {
38+
// A reassignment of UNKNOWN_PATH is considered equivalent to having lost track
39+
// which means the return expression needs to be reassigned
40+
this.scope.getReturnExpression().deoptimizePath(UNKNOWN_PATH);
41+
}
42+
}
43+
44+
deoptimizeThisOnEventAtPath(
45+
event: NodeEvent,
46+
path: ObjectPath,
47+
thisParameter: ExpressionEntity,
48+
recursionTracker: PathTracker
49+
): void {
50+
if (path.length > 0) {
51+
this.getObjectEntity().deoptimizeThisOnEventAtPath(
52+
event,
53+
path,
54+
thisParameter,
55+
recursionTracker
56+
);
57+
}
58+
}
59+
60+
getLiteralValueAtPath(
61+
path: ObjectPath,
62+
recursionTracker: PathTracker,
63+
origin: DeoptimizableEntity
64+
): LiteralValueOrUnknown {
65+
return this.getObjectEntity().getLiteralValueAtPath(path, recursionTracker, origin);
66+
}
67+
68+
getReturnExpressionWhenCalledAtPath(
69+
path: ObjectPath,
70+
callOptions: CallOptions,
71+
recursionTracker: PathTracker,
72+
origin: DeoptimizableEntity
73+
): ExpressionEntity {
74+
if (path.length > 0) {
75+
return this.getObjectEntity().getReturnExpressionWhenCalledAtPath(
76+
path,
77+
callOptions,
78+
recursionTracker,
79+
origin
80+
);
81+
}
82+
if (this.async) {
83+
if (!this.deoptimizedReturn) {
84+
this.deoptimizedReturn = true;
85+
this.scope.getReturnExpression().deoptimizePath(UNKNOWN_PATH);
86+
this.context.requestTreeshakingPass();
87+
}
88+
return UNKNOWN_EXPRESSION;
89+
}
90+
return this.scope.getReturnExpression();
91+
}
92+
93+
hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean {
94+
return this.getObjectEntity().hasEffectsWhenAccessedAtPath(path, context);
95+
}
96+
97+
hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean {
98+
return this.getObjectEntity().hasEffectsWhenAssignedAtPath(path, context);
99+
}
100+
101+
hasEffectsWhenCalledAtPath(
102+
path: ObjectPath,
103+
callOptions: CallOptions,
104+
context: HasEffectsContext
105+
): boolean {
106+
if (path.length > 0) {
107+
return this.getObjectEntity().hasEffectsWhenCalledAtPath(path, callOptions, context);
108+
}
109+
if (this.async) {
110+
const { propertyReadSideEffects } = this.context.options
111+
.treeshake as NormalizedTreeshakingOptions;
112+
const returnExpression = this.scope.getReturnExpression();
113+
if (
114+
returnExpression.hasEffectsWhenCalledAtPath(
115+
['then'],
116+
{ args: NO_ARGS, thisParam: null, withNew: false },
117+
context
118+
) ||
119+
(propertyReadSideEffects &&
120+
(propertyReadSideEffects === 'always' ||
121+
returnExpression.hasEffectsWhenAccessedAtPath(['then'], context)))
122+
) {
123+
return true;
124+
}
125+
}
126+
for (const param of this.params) {
127+
if (param.hasEffects(context)) return true;
128+
}
129+
return false;
130+
}
131+
132+
include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void {
133+
this.included = true;
134+
const { brokenFlow } = context;
135+
context.brokenFlow = BROKEN_FLOW_NONE;
136+
this.body.include(context, includeChildrenRecursively);
137+
context.brokenFlow = brokenFlow;
138+
}
139+
140+
includeCallArguments(
141+
context: InclusionContext,
142+
args: readonly (ExpressionNode | SpreadElement)[]
143+
): void {
144+
this.scope.includeCallArguments(context, args);
145+
}
146+
147+
initialise(): void {
148+
this.scope.addParameterVariables(
149+
this.params.map(param => param.declare('parameter', UNKNOWN_EXPRESSION)),
150+
this.params[this.params.length - 1] instanceof RestElement
151+
);
152+
if (this.body instanceof BlockStatement) {
153+
this.body.addImplicitReturnExpressionToScope();
154+
} else {
155+
this.scope.addReturnExpression(this.body);
156+
}
157+
}
158+
159+
parseNode(esTreeNode: GenericEsTreeNode): void {
160+
if (esTreeNode.body.type === NodeType.BlockStatement) {
161+
this.body = new BlockStatement(esTreeNode.body, this, this.scope.hoistedBodyVarScope);
162+
}
163+
super.parseNode(esTreeNode);
164+
}
165+
166+
protected abstract getObjectEntity(): ObjectEntity;
167+
}
168+
169+
FunctionBase.prototype.preventChildBlockScope = true;

0 commit comments

Comments
 (0)