Skip to content

Commit 84c0ea3

Browse files
authored
Treat unknown prototype props as unknown (#4428)
* Treat unknown prototype methods as unknown * Improve coverage
1 parent 9c8894e commit 84c0ea3

24 files changed

Lines changed: 100 additions & 59 deletions

File tree

src/ast/nodes/shared/ObjectPrototype.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,47 @@
1+
import { EVENT_CALLED, NodeEvent } from '../../NodeEvents';
2+
import { ObjectPath, ObjectPathKey, UNKNOWN_PATH } from '../../utils/PathTracker';
3+
import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue } from './Expression';
14
import {
25
METHOD_RETURNS_BOOLEAN,
36
METHOD_RETURNS_STRING,
47
METHOD_RETURNS_UNKNOWN
58
} from './MethodTypes';
69
import { ObjectEntity, type PropertyMap } from './ObjectEntity';
710

11+
const isInteger = (prop: ObjectPathKey): boolean => typeof prop === 'string' && /^\d+$/.test(prop);
12+
13+
// This makes sure unknown properties are not handled as "undefined" but as
14+
// "unknown" but without access side effects. An exception is done for numeric
15+
// properties as we do not expect new builtin properties to be numbers, this
16+
// will improve tree-shaking for out-of-bounds array properties
17+
const OBJECT_PROTOTYPE_FALLBACK: ExpressionEntity =
18+
new (class ObjectPrototypeFallbackExpression extends ExpressionEntity {
19+
deoptimizeThisOnEventAtPath(
20+
event: NodeEvent,
21+
path: ObjectPath,
22+
thisParameter: ExpressionEntity
23+
): void {
24+
if (event === EVENT_CALLED && path.length === 1 && !isInteger(path[0])) {
25+
thisParameter.deoptimizePath(UNKNOWN_PATH);
26+
}
27+
}
28+
29+
getLiteralValueAtPath(path: ObjectPath): LiteralValueOrUnknown {
30+
// We ignore number properties as we do not expect new properties to be
31+
// numbers and also want to keep handling out-of-bound array elements as
32+
// "undefined"
33+
return path.length === 1 && isInteger(path[0]) ? undefined : UnknownValue;
34+
}
35+
36+
hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean {
37+
return path.length > 1;
38+
}
39+
40+
hasEffectsWhenAssignedAtPath(path: ObjectPath): boolean {
41+
return path.length > 1;
42+
}
43+
})();
44+
845
export const OBJECT_PROTOTYPE = new ObjectEntity(
946
{
1047
__proto__: null,
@@ -15,6 +52,6 @@ export const OBJECT_PROTOTYPE = new ObjectEntity(
1552
toString: METHOD_RETURNS_STRING,
1653
valueOf: METHOD_RETURNS_UNKNOWN
1754
} as unknown as PropertyMap,
18-
null,
55+
OBJECT_PROTOTYPE_FALLBACK,
1956
true
2057
);

test/cli/samples/watch/watch-config-initial-error/_config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ module.exports = {
1212
writeFileSync(configFile, 'throw new Error("Config contains initial errors");');
1313
},
1414
after() {
15-
unlinkSync(configFile);
15+
// synchronous sometimes does not seem to work, probably because the watch is not yet removed properly
16+
setTimeout(() => unlinkSync(configFile), 300);
1617
},
1718
async abortOnStderr(data) {
1819
if (data.includes('Config contains initial errors')) {

test/form/samples/avoid-unnecessary-conditional-deopt/_expected.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ console.log('not modified');
22

33
let x2 = false;
44
modifyX2();
5-
const obj2 = {};
5+
const obj2 = { modified: false };
66
(x2 ? obj2 : {}).modified = true;
77

88
if (obj2.modified) console.log('modified');
@@ -16,7 +16,7 @@ console.log('not modified');
1616

1717
let x4 = false;
1818
modifyX4();
19-
const obj4 = {};
19+
const obj4 = { modified: false };
2020
(x4 && obj4).modified = true;
2121

2222
if (obj4.modified) console.log('modified');

test/form/samples/avoid-unnecessary-conditional-deopt/main.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
let x1 = false;
22
modifyX1();
3-
const obj1 = {};
3+
const obj1 = { modified: false };
44
x1 ? obj1 : {};
55

66
if (obj1.modified) console.log('should not happen');
@@ -12,7 +12,7 @@ function modifyX1() {
1212

1313
let x2 = false;
1414
modifyX2();
15-
const obj2 = {};
15+
const obj2 = { modified: false };
1616
(x2 ? obj2 : {}).modified = true;
1717

1818
if (obj2.modified) console.log('modified');
@@ -24,7 +24,7 @@ function modifyX2() {
2424

2525
let x3 = false;
2626
modifyX3();
27-
const obj3 = {};
27+
const obj3 = { modified: false };
2828
x3 && obj3;
2929

3030
if (obj3.modified) console.log('should not happen');
@@ -36,7 +36,7 @@ function modifyX3() {
3636

3737
let x4 = false;
3838
modifyX4();
39-
const obj4 = {};
39+
const obj4 = { modified: false };
4040
(x4 && obj4).modified = true;
4141

4242
if (obj4.modified) console.log('modified');

test/form/samples/class-method-access/_expected.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ console.log('retained');
33
class Used {
44
static method() {}
55
static get getter() {
6-
return { foo: {} };
6+
return { foo: { throws: null }, throws: null };
77
}
88
}
99
Used.method.doesNotExist.throws;

test/form/samples/class-method-access/main.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ else console.log('removed');
1010
class Used {
1111
static method() {}
1212
static get getter() {
13-
return { foo: {} };
13+
return { foo: { throws: null }, throws: null };
1414
}
1515
}
1616

@@ -30,5 +30,5 @@ Used.method.reassigned = 1;
3030
Used.getter.reassigned = 2;
3131

3232
class ValueEffect {
33-
static foo
34-
}
33+
static foo;
34+
}

test/form/samples/minimal-this-mutation/_expected.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,3 @@ const obj5 = {
4040
obj5.mutateNestedProp();
4141
if (obj5.nested.prop) console.log('unimportant');
4242
else console.log('retained');
43-
44-
const obj6 = {
45-
prop: true
46-
};
47-
obj6.doesNotExist();
48-
console.log('retained');

test/form/samples/minimal-this-mutation/main.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,3 @@ const obj5 = {
4747
obj5.mutateNestedProp();
4848
if (obj5.nested.prop) console.log('unimportant');
4949
else console.log('retained');
50-
51-
const obj6 = {
52-
prop: true
53-
};
54-
obj6.doesNotExist();
55-
if (obj6.prop) console.log('retained');
56-
else console.log('removed');
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
description: 'treats mutating nested properties as side effects',
3+
options: {
4+
treeshake: { propertyReadSideEffects: false }
5+
}
6+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const obj = { __proto__: null };
2+
3+
obj.foo.bar = 'retained';

0 commit comments

Comments
 (0)