diff --git a/src/ast/nodes/shared/knownGlobals.ts b/src/ast/nodes/shared/knownGlobals.ts index d7bc8b1556b..13e8294a635 100644 --- a/src/ast/nodes/shared/knownGlobals.ts +++ b/src/ast/nodes/shared/knownGlobals.ts @@ -3,7 +3,10 @@ import { doNothing } from '../../../utils/doNothing'; import type { HasEffectsContext } from '../../ExecutionContext'; import type { NodeInteractionCalled } from '../../NodeInteractions'; -import { NODE_INTERACTION_UNKNOWN_ASSIGNMENT } from '../../NodeInteractions'; +import { + NODE_INTERACTION_UNKNOWN_ACCESS, + NODE_INTERACTION_UNKNOWN_ASSIGNMENT +} from '../../NodeInteractions'; import type { ObjectPath } from '../../utils/PathTracker'; import { SymbolToStringTag, @@ -12,7 +15,7 @@ import { } from '../../utils/PathTracker'; import ArrayExpression from '../ArrayExpression'; import type { LiteralValueOrUnknown } from './Expression'; -import { UnknownTruthyValue } from './Expression'; +import { ExpressionEntity, UnknownTruthyValue } from './Expression'; const ValueProperties = Symbol('Value Properties'); @@ -52,6 +55,22 @@ const PURE_WITH_ARRAY: ValueDescription = { } }; +const GETTER_ACCESS: ValueDescription = { + deoptimizeArgumentsOnCall: doNothing, + getLiteralValue: getTruthyLiteralValue, + hasEffectsWhenCalled({ args }, context) { + const [_thisArgument, firstArgument] = args; + return ( + !(firstArgument instanceof ExpressionEntity) || + firstArgument.hasEffectsOnInteractionAtPath( + UNKNOWN_PATH, + NODE_INTERACTION_UNKNOWN_ACCESS, + context + ) + ); + } +}; + // We use shortened variables to reduce file size here /* OBJECT */ const O: GlobalDescription = { @@ -65,6 +84,12 @@ const PF: GlobalDescription = { [ValueProperties]: PURE }; +/* PURE FUNCTION IF FIRST ARG DOES NOT CONTAIN A GETTER */ +const PF_NO_GETTER: GlobalDescription = { + __proto__: null, + [ValueProperties]: GETTER_ACCESS +}; + /* FUNCTION THAT MUTATES FIRST ARG WITHOUT TRIGGERING ACCESSORS */ const MUTATES_ARG_WITHOUT_ACCESSOR: GlobalDescription = { __proto__: null, @@ -253,7 +278,8 @@ const knownGlobals: GlobalDescription = { isSealed: PF, keys: PF, fromEntries: O, - entries: PF, + entries: PF_NO_GETTER, + values: PF_NO_GETTER, prototype: O }, parseFloat: PF, @@ -350,16 +376,24 @@ const knownGlobals: GlobalDescription = { [ValueProperties]: IMPURE, Collator: INTL_MEMBER, DateTimeFormat: INTL_MEMBER, + DisplayNames: INTL_MEMBER, ListFormat: INTL_MEMBER, + Locale: INTL_MEMBER, NumberFormat: INTL_MEMBER, PluralRules: INTL_MEMBER, - RelativeTimeFormat: INTL_MEMBER + RelativeTimeFormat: INTL_MEMBER, + Segmenter: INTL_MEMBER }, setInterval: C, setTimeout: C, TextDecoder: C, TextEncoder: C, - URL: C, + URL: { + __proto__: null, + [ValueProperties]: IMPURE, + prototype: O, + canParse: PF + }, URLSearchParams: C, // Browser specific globals diff --git a/test/form/samples/tree-shake-object-with-sideeffctful-getter/_config.js b/test/form/samples/tree-shake-object-with-sideeffctful-getter/_config.js new file mode 100644 index 00000000000..e35dd23c351 --- /dev/null +++ b/test/form/samples/tree-shake-object-with-sideeffctful-getter/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'retain functions that accept a object with a getter that has side effects' +}; diff --git a/test/form/samples/tree-shake-object-with-sideeffctful-getter/_expected.js b/test/form/samples/tree-shake-object-with-sideeffctful-getter/_expected.js new file mode 100644 index 00000000000..a21f171ed64 --- /dev/null +++ b/test/form/samples/tree-shake-object-with-sideeffctful-getter/_expected.js @@ -0,0 +1,13 @@ +let effects = 0; + +const objWithEffect = { + get foo() { + effects++; + return 'foo'; + } +}; + +Object.values(objWithEffect); +Object.entries(objWithEffect); + +assert.equal(effects, 2); diff --git a/test/form/samples/tree-shake-object-with-sideeffctful-getter/main.js b/test/form/samples/tree-shake-object-with-sideeffctful-getter/main.js new file mode 100644 index 00000000000..d5a774cf8b1 --- /dev/null +++ b/test/form/samples/tree-shake-object-with-sideeffctful-getter/main.js @@ -0,0 +1,27 @@ +let effects = 0; + +const objWithEffect = { + get foo() { + effects++; + return 'foo'; + } +}; + +const objWithoutEffect1 = { + get foo() { + return 'foo'; + } +}; +const objWithoutEffect2 = { + foo: 'foo' +}; + +Object.values(objWithEffect); +Object.entries(objWithEffect); + +Object.values(objWithoutEffect1); +Object.entries(objWithoutEffect1); +Object.values(objWithoutEffect2); +Object.entries(objWithoutEffect2); + +assert.equal(effects, 2);