Skip to content

Commit 9a8614f

Browse files
feat: optimize in checks on namespaces to keep them treeshake-able (#6029)
* feat: optimize `in` checks on namespaces * fix: remove bad leftovers * fix: set renderedLiteralValue to UnknownValue on deopt for BinaryExpression * Adapt test for changed traceExport signature --------- Co-authored-by: Lukas Taegert-Atkinson <[email protected]> Co-authored-by: Lukas Taegert-Atkinson <[email protected]>
1 parent fdd48a9 commit 9a8614f

File tree

7 files changed

+103
-35
lines changed

7 files changed

+103
-35
lines changed

src/ast/nodes/BinaryExpression.ts

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,22 @@ import {
1212
SHARED_RECURSION_TRACKER,
1313
UNKNOWN_PATH
1414
} from '../utils/PathTracker';
15+
import { getRenderedLiteralValue } from '../utils/renderLiteralValue';
16+
import type NamespaceVariable from '../variables/NamespaceVariable';
1517
import ExpressionStatement from './ExpressionStatement';
1618
import type { LiteralValue } from './Literal';
1719
import type * as NodeType from './NodeType';
18-
import { type LiteralValueOrUnknown, UnknownValue } from './shared/Expression';
19-
import { doNotDeoptimize, type ExpressionNode, NodeBase } from './shared/Node';
20+
import {
21+
type InclusionOptions,
22+
type LiteralValueOrUnknown,
23+
UnknownValue
24+
} from './shared/Expression';
25+
import {
26+
doNotDeoptimize,
27+
type ExpressionNode,
28+
type IncludeChildren,
29+
NodeBase
30+
} from './shared/Node';
2031

2132
type Operator =
2233
| '!='
@@ -71,13 +82,18 @@ const binaryOperators: Partial<
7182
// instanceof: () => UnknownValue,
7283
};
7384

85+
const UNASSIGNED = Symbol('Unassigned');
86+
7487
export default class BinaryExpression extends NodeBase implements DeoptimizableEntity {
7588
declare left: ExpressionNode;
7689
declare operator: keyof typeof binaryOperators;
7790
declare right: ExpressionNode;
7891
declare type: NodeType.tBinaryExpression;
92+
renderedLiteralValue: string | typeof UnknownValue | typeof UNASSIGNED = UNASSIGNED;
7993

80-
deoptimizeCache(): void {}
94+
deoptimizeCache(): void {
95+
this.renderedLiteralValue = UnknownValue;
96+
}
8197

8298
getLiteralValueAtPath(
8399
path: ObjectPath,
@@ -88,6 +104,13 @@ export default class BinaryExpression extends NodeBase implements DeoptimizableE
88104
const leftValue = this.left.getLiteralValueAtPath(EMPTY_PATH, recursionTracker, origin);
89105
if (typeof leftValue === 'symbol') return UnknownValue;
90106

107+
// Optimize `'export' in namespace`
108+
if (this.operator === 'in' && this.right.variable?.isNamespace) {
109+
return (
110+
(this.right.variable as NamespaceVariable).context.traceExport(String(leftValue))[0] != null
111+
);
112+
}
113+
91114
const rightValue = this.right.getLiteralValueAtPath(EMPTY_PATH, recursionTracker, origin);
92115
if (typeof rightValue === 'symbol') return UnknownValue;
93116

@@ -97,6 +120,16 @@ export default class BinaryExpression extends NodeBase implements DeoptimizableE
97120
return operatorFunction(leftValue, rightValue);
98121
}
99122

123+
getRenderedLiteralValue() {
124+
// Only optimize `'export' in ns`
125+
if (this.operator !== 'in' || !this.right.variable?.isNamespace) return UnknownValue;
126+
127+
if (this.renderedLiteralValue !== UNASSIGNED) return this.renderedLiteralValue;
128+
return (this.renderedLiteralValue = getRenderedLiteralValue(
129+
this.getLiteralValueAtPath(EMPTY_PATH, SHARED_RECURSION_TRACKER, this)
130+
));
131+
}
132+
100133
hasEffects(context: HasEffectsContext): boolean {
101134
// support some implicit type coercion runtime errors
102135
if (
@@ -113,9 +146,20 @@ export default class BinaryExpression extends NodeBase implements DeoptimizableE
113146
return type !== INTERACTION_ACCESSED || path.length > 1;
114147
}
115148

149+
include(
150+
context: InclusionContext,
151+
includeChildrenRecursively: IncludeChildren,
152+
_options?: InclusionOptions
153+
) {
154+
this.included = true;
155+
if (typeof this.getRenderedLiteralValue() === 'symbol') {
156+
super.include(context, includeChildrenRecursively, _options);
157+
}
158+
}
159+
116160
includeNode(context: InclusionContext) {
117161
this.included = true;
118-
if (this.operator === 'in') {
162+
if (this.operator === 'in' && typeof this.getRenderedLiteralValue() === 'symbol') {
119163
this.right.includePath(UNKNOWN_PATH, context);
120164
}
121165
}
@@ -129,8 +173,13 @@ export default class BinaryExpression extends NodeBase implements DeoptimizableE
129173
options: RenderOptions,
130174
{ renderedSurroundingElement }: NodeRenderOptions = BLANK
131175
): void {
132-
this.left.render(code, options, { renderedSurroundingElement });
133-
this.right.render(code, options);
176+
const renderedLiteralValue = this.getRenderedLiteralValue();
177+
if (typeof renderedLiteralValue !== 'symbol') {
178+
code.overwrite(this.start, this.end, renderedLiteralValue);
179+
} else {
180+
this.left.render(code, options, { renderedSurroundingElement });
181+
this.right.render(code, options);
182+
}
134183
}
135184
}
136185

src/ast/nodes/UnaryExpression.ts

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
type ObjectPath,
1111
SHARED_RECURSION_TRACKER
1212
} from '../utils/PathTracker';
13+
import { getRenderedLiteralValue } from '../utils/renderLiteralValue';
1314
import Identifier from './Identifier';
1415
import type { LiteralValue } from './Literal';
1516
import type * as NodeType from './NodeType';
@@ -142,33 +143,4 @@ export default class UnaryExpression extends NodeBase {
142143

143144
const CHARACTERS_THAT_DO_NOT_REQUIRE_SPACE = /[\s([=%&*+-/<>^|,?:;]/;
144145

145-
function getRenderedLiteralValue(value: unknown) {
146-
if (value === undefined) {
147-
// At the moment, the undefined only happens when the operator is void
148-
return 'void 0';
149-
}
150-
if (typeof value === 'boolean') {
151-
return String(value);
152-
}
153-
if (typeof value === 'string') {
154-
return JSON.stringify(value);
155-
}
156-
if (typeof value === 'number') {
157-
return getSimplifiedNumber(value);
158-
}
159-
return UnknownValue;
160-
}
161-
162-
function getSimplifiedNumber(value: number) {
163-
if (Object.is(-0, value)) {
164-
return '-0';
165-
}
166-
const exp = value.toExponential();
167-
const [base, exponent] = exp.split('e');
168-
const floatLength = base.split('.')[1]?.length || 0;
169-
const finalizedExp = `${base.replace('.', '')}e${parseInt(exponent) - floatLength}`;
170-
const stringifiedValue = String(value).replace('+', '');
171-
return finalizedExp.length < stringifiedValue.length ? finalizedExp : stringifiedValue;
172-
}
173-
174146
UnaryExpression.prototype.includeNode = onlyIncludeSelf;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { UnknownValue } from '../nodes/shared/Expression';
2+
3+
export function getRenderedLiteralValue(value: unknown) {
4+
if (value === undefined) {
5+
return 'void 0';
6+
}
7+
if (typeof value === 'boolean') {
8+
return String(value);
9+
}
10+
if (typeof value === 'string') {
11+
return JSON.stringify(value);
12+
}
13+
if (typeof value === 'number') {
14+
return getSimplifiedNumber(value);
15+
}
16+
return UnknownValue;
17+
}
18+
19+
function getSimplifiedNumber(value: number) {
20+
if (Object.is(-0, value)) {
21+
return '-0';
22+
}
23+
const exp = value.toExponential();
24+
const [base, exponent] = exp.split('e');
25+
const floatLength = base.split('.')[1]?.length || 0;
26+
const finalizedExp = `${base.replace('.', '')}e${parseInt(exponent) - floatLength}`;
27+
const stringifiedValue = String(value).replace('+', '');
28+
return finalizedExp.length < stringifiedValue.length ? finalizedExp : stringifiedValue;
29+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = defineTest({
2+
description:
3+
'it does static optimization of internal namespaces when checking whether an export exists',
4+
expectedWarnings: [
5+
// That's a bit of an annoyance that Rollup still complains despite the if gate...
6+
'MISSING_EXPORT'
7+
]
8+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
function c() {}
2+
3+
console.log(c());
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function a() {}
2+
export function b() {}
3+
export function c() {}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import * as foo from './foo';
2+
3+
if ('d' in foo) console.log(foo.d())
4+
if ('c' in foo) console.log(foo.c())

0 commit comments

Comments
 (0)