From d73b2ace7aa352980920745690a01c0190d6f4a7 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Sun, 23 May 2021 07:06:41 +0200 Subject: [PATCH] Class method effects (#4018) * Move logic from ClassBody into ClassNode So that it sits in one place and is easier to extend. * Track static class fields and improve handling of class getters/setters This aims to improve tree-shaking of code that uses static class properties (#3989) and to improve detection of side effects through class getters/setters (#4016). The first part works by keeping a map of positively known static properties (methods and simple getters) in `ClassNode.staticPropertyMap`, along with a flag (`ClassNode.deoptimizedStatic`) that indicates that something happened that removed our confidence that we know anything about the class object. Access and calls to these known static properties are handled by routing the calls to `getLiteralValueAtPath`, `getReturnExpressionWhenCalledAtPath`, and `hasEffectsWhenCalledAtPath` to the known values in the properties. In contrast to `ObjectExpression`, this class does not try to keep track of multiple expressions associated with a property, since that doesn't come up a lot on classes. The handling of side effect detection through getters and setters is done by, _if_ the entire class object (or its prototype in case of access to the prototype) hasn't been deoptimized, scanning through the directly defined getters and setters to see if one exists (calling through to superclasses as appropriate). I believe that this is solid because any code that would be able to change the set of getters and setters on a class would cause the entire object to be deoptimized. * Remove ClassNode.deoptimizeCache * Keep a table for class property effects * Add comment explaining property map * Fix types * Make getReturnExpression and getLiteralValue more similar for objects * Use common logic for return expression and literal value * Use common logic for return access and call effects * Extract shared logic from ObjectExpression * Use an object for better performance * Simplify handling for setters and other properties * Small simplification * Work towards better class handling * merge ObjectPathHandler into ObjectEntity * Slightly refactor default values * Separate unknown nodes from other Nodes to avoid future circular dependencies * Introduce new prototype tracking * Improve coverage * Fix class deoptimization in arrow functions via this/super * Simplify and merge property and method definition * Improve coverage * Replace deoptimizeProperties by deoptimizing a double unknown path * Assume functions can add getters to parameters * Improve class.prototype handling * Assume created instance getters have side-effects * Base all expressions on a base class * Only deoptimize necessary paths when deoptimizing "this" * Handle deoptimizing "this" in getters * Handle deoptimizing "this" in setters * Unify this deoptimization * Simplify recursion tracking * Get rid of deoptimizations during bind phase * Get rid of unneeded double-binding checks * Inline deoptimizations into NodeBase for simple cases * Add more efficient way to create object entities * Add thisParameter to CallOptions * Move NodeEvents to separate file * Track array elements * Simplify namespace handling * Use Object.values and Object.entries instead of Object.keys where useful * Improve code and simplify literal handling * Improve coverage * Improve coverage * Improve coverage in conditional and logical expressions * Improve coverage * 2.49.0-0 * Fix test to support pre-release versions * Fix failed deoptimization of array props * 2.49.0-1 Co-authored-by: Lukas Taegert-Atkinson Co-authored-by: Lukas Taegert-Atkinson --- cli/run/build.ts | 4 +- cli/run/watch-cli.ts | 4 +- package-lock.json | 2 +- package.json | 2 +- src/Bundle.ts | 22 +- src/Chunk.ts | 4 +- src/Graph.ts | 20 +- src/Module.ts | 3 +- src/ast/CallOptions.ts | 1 + src/ast/Entity.ts | 6 +- src/ast/NodeEvents.ts | 4 + src/ast/keys.ts | 2 +- src/ast/nodes/ArrayExpression.ts | 90 +++- src/ast/nodes/ArrayPattern.ts | 2 +- src/ast/nodes/ArrowFunctionExpression.ts | 9 +- src/ast/nodes/AssignmentExpression.ts | 4 +- src/ast/nodes/AssignmentPattern.ts | 13 +- src/ast/nodes/BinaryExpression.ts | 2 +- src/ast/nodes/BlockStatement.ts | 2 +- src/ast/nodes/CallExpression.ts | 207 ++++---- src/ast/nodes/CatchClause.ts | 2 +- src/ast/nodes/ClassBody.ts | 46 +- src/ast/nodes/ConditionalExpression.ts | 115 +++-- src/ast/nodes/ForInStatement.ts | 16 +- src/ast/nodes/ForOfStatement.ts | 16 +- src/ast/nodes/Identifier.ts | 55 +- src/ast/nodes/IfStatement.ts | 2 +- src/ast/nodes/ImportDeclaration.ts | 1 + src/ast/nodes/Literal.ts | 10 +- src/ast/nodes/LogicalExpression.ts | 83 +-- src/ast/nodes/MemberExpression.ts | 127 +++-- src/ast/nodes/MethodDefinition.ts | 23 +- src/ast/nodes/NewExpression.ts | 22 +- src/ast/nodes/ObjectExpression.ts | 316 ++---------- src/ast/nodes/Property.ts | 149 +----- src/ast/nodes/PropertyDefinition.ts | 60 +++ src/ast/nodes/RestElement.ts | 24 +- src/ast/nodes/ReturnStatement.ts | 2 +- src/ast/nodes/SequenceExpression.ts | 14 +- src/ast/nodes/SpreadElement.ts | 15 +- src/ast/nodes/Super.ts | 19 + src/ast/nodes/TaggedTemplateExpression.ts | 1 + src/ast/nodes/TemplateElement.ts | 1 + src/ast/nodes/TemplateLiteral.ts | 2 +- src/ast/nodes/ThisExpression.ts | 44 +- src/ast/nodes/UnaryExpression.ts | 18 +- src/ast/nodes/UpdateExpression.ts | 25 +- src/ast/nodes/YieldExpression.ts | 14 +- src/ast/nodes/shared/ArrayPrototype.ts | 144 ++++++ src/ast/nodes/shared/ClassNode.ts | 140 ++++- src/ast/nodes/shared/Expression.ts | 81 ++- src/ast/nodes/shared/FunctionNode.ts | 27 +- src/ast/nodes/shared/MethodBase.ts | 131 +++++ src/ast/nodes/shared/MethodTypes.ts | 132 +++++ src/ast/nodes/shared/MultiExpression.ts | 22 +- src/ast/nodes/shared/Node.ts | 65 +-- src/ast/nodes/shared/ObjectEntity.ts | 477 ++++++++++++++++++ src/ast/nodes/shared/ObjectMember.ts | 64 +++ src/ast/nodes/shared/ObjectPrototype.ts | 26 + src/ast/scopes/BlockScope.ts | 3 +- src/ast/scopes/ClassBodyScope.ts | 18 + src/ast/scopes/ParameterScope.ts | 2 +- src/ast/scopes/ReturnValueScope.ts | 3 +- src/ast/utils/PathTracker.ts | 46 +- src/ast/values.ts | 303 ++--------- src/ast/variables/ArgumentsVariable.ts | 2 +- src/ast/variables/LocalVariable.ts | 131 ++--- src/ast/variables/NamespaceVariable.ts | 18 +- src/ast/variables/ThisVariable.ts | 67 ++- src/ast/variables/UndefinedVariable.ts | 2 +- src/ast/variables/Variable.ts | 57 +-- src/rollup/rollup.ts | 13 +- src/utils/FileEmitter.ts | 10 +- src/utils/chunkAssignment.ts | 4 +- src/utils/options/normalizeInputOptions.ts | 4 +- src/utils/timers.ts | 6 +- .../watch/watch-config-no-update/_config.js | 2 +- .../array-elements/_config.js | 3 + .../array-elements/_expected.js | 4 + .../array-elements/main.js | 10 + .../array-mutation/_config.js | 3 + .../array-mutation/_expected.js | 10 + .../array-mutation/main.js | 11 + .../array-spread/_config.js | 3 + .../array-spread/_expected.js | 9 + .../array-spread/main.js | 11 + .../spread-element-deoptimization/_config.js | 3 + .../_expected.js | 3 + .../spread-element-deoptimization/main.js | 3 + .../arrow-function-return-values/_expected.js | 4 - .../array-expression/_expected.js | 87 +++- .../array-expression/main.js | 179 +++++-- .../builtin-prototypes/literal/_expected.js | 10 + .../builtin-prototypes/literal/main.js | 31 +- .../object-expression/_expected.js | 11 - .../samples/class-method-access/_config.js | 3 + .../samples/class-method-access/_expected.js | 16 + test/form/samples/class-method-access/main.js | 34 ++ .../conditional-expression-paths/_expected.js | 11 - .../conditional-expression-paths/main.js | 4 +- .../function-body-return-values/_expected.js | 6 - .../literals-from-class-statics/_config.js | 3 + .../literals-from-class-statics/_expected.js | 13 + .../literals-from-class-statics/main.js | 21 + .../samples/minimal-this-mutation/_config.js | 3 + .../minimal-this-mutation/_expected.js | 48 ++ .../samples/minimal-this-mutation/main.js | 56 ++ .../modify-class-prototype/_expected.js | 8 + .../samples/modify-class-prototype/main.js | 15 + .../_config.js | 4 + .../_expected.js | 5 + .../namespace-missing-export-effects/main.js | 7 + .../namespace-missing-export-effects/other.js | 0 .../{_expected/es.js => _expected.js} | 0 .../namespace-optimization/_expected/amd.js | 7 - .../namespace-optimization/_expected/cjs.js | 5 - .../namespace-optimization/_expected/iife.js | 8 - .../_expected/system.js | 12 - .../namespace-optimization/_expected/umd.js | 10 - .../samples/namespace-optimization/bar.js | 2 +- .../samples/namespace-optimization/foo.js | 2 +- .../samples/nested-deoptimization/_config.js | 3 + .../nested-deoptimization/_expected.js | 11 + .../samples/nested-deoptimization/main.js | 13 + .../form/samples/nested-member-access/main.js | 7 - .../return-expressions/_expected.js | 2 - .../shadowed-setters/_config.js | 3 + .../shadowed-setters/_expected.js | 8 + .../shadowed-setters/main.js | 17 + .../form/samples/recursive-calls/_expected.js | 10 - test/form/samples/recursive-calls/main.js | 7 - .../_config.js | 3 + .../_expected.js | 38 ++ .../main.js | 78 +++ .../_expected.js | 26 +- .../side-effects-getters-and-setters/main.js | 54 +- .../_config.js | 3 +- .../side-effects-static-methods/_config.js | 3 + .../side-effects-static-methods/_expected.js | 38 ++ .../side-effects-static-methods/main.js | 43 ++ .../{_expected/es.js => _expected.js} | 0 .../skips-dead-branches/_expected/amd.js | 9 - .../skips-dead-branches/_expected/cjs.js | 7 - .../skips-dead-branches/_expected/iife.js | 10 - .../skips-dead-branches/_expected/system.js | 14 - .../skips-dead-branches/_expected/umd.js | 12 - .../static-class-property-calls/_config.js | 3 + .../static-class-property-calls/_expected.js | 21 + .../static-class-property-calls/main.js | 23 + .../super-class-prototype-access/_config.js | 3 + .../super-class-prototype-access/_expected.js | 8 + .../super-class-prototype-access/main.js | 11 + .../_config.js | 3 + .../_expected.js | 31 ++ .../super-class-prototype-assignment/main.js | 40 ++ .../super-class-prototype-calls/_config.js | 3 + .../super-class-prototype-calls/_expected.js | 23 + .../super-class-prototype-calls/main.js | 25 + .../super-class-prototype-values/_config.js | 3 + .../super-class-prototype-values/_expected.js | 15 + .../super-class-prototype-values/main.js | 28 + .../samples/this-in-class-body/_config.js | 3 + .../samples/this-in-class-body/_expected.js | 37 ++ test/form/samples/this-in-class-body/main.js | 47 ++ .../samples/undefined-properties/_config.js | 3 +- .../samples/undefined-properties/_expected.js | 9 + .../form/samples/undefined-properties/main.js | 7 +- .../samples/access-instance-prop/_config.js | 3 + .../samples/access-instance-prop/main.js | 13 + test/function/samples/array-getter/_config.js | 3 + test/function/samples/array-getter/main.js | 14 + .../samples/array-mutation/_config.js | 3 + test/function/samples/array-mutation/main.js | 4 + .../conditionals-deoptimization/_config.js | 8 + .../conditionals-deoptimization/main.js | 21 + .../deoptimize-assumes-getters/_config.js | 3 + .../deoptimize-assumes-getters/main.js | 14 + .../samples/deoptimize-class/_config.js | 3 + .../function/samples/deoptimize-class/main.js | 20 + .../deoptimize-computed-class-keys/_config.js | 4 + .../deoptimize-computed-class-keys/main.js | 17 + .../deoptimize-computed-keys/_config.js | 3 + .../samples/deoptimize-computed-keys/main.js | 14 + .../samples/deoptimize-object/_config.js | 3 + .../samples/deoptimize-object/main.js | 13 + .../deoptimize-this-parameters/_config.js | 3 + .../deoptimize-this-parameters/main.js | 21 + .../modify-object-via-this-d/_config.js | 3 + .../samples/modify-object-via-this-d/main.js | 13 + .../deoptimized-props-with-getter/_config.js | 11 + .../deoptimized-props-with-getter/main.js | 17 + .../getter-in-assignment/_config.js | 3 + .../getter-in-assignment/main.js | 14 + .../getters-on-this/_config.js | 3 + .../getters-on-this/main.js | 39 ++ .../known-getter/_config.js | 3 + .../known-getter/main.js | 12 + .../known-super-prop/_config.js | 4 + .../known-super-prop/main.js | 14 + .../unknown-prop-getter/_config.js | 11 + .../unknown-prop-getter/main.js | 14 + .../unknown-prop-unknown-access/_config.js | 11 + .../unknown-prop-unknown-access/main.js | 14 + .../unknown-property-access/_config.js | 11 + .../unknown-property-access/main.js | 14 + .../unknown-super-prop/_config.js | 12 + .../unknown-super-prop/main.js | 16 + .../deoptimized-props-with-setter/_config.js | 11 + .../deoptimized-props-with-setter/main.js | 17 + .../known-setter/_config.js | 3 + .../known-setter/main.js | 12 + .../known-super-prop/_config.js | 4 + .../known-super-prop/main.js | 14 + .../unknown-prop-setter/_config.js | 11 + .../unknown-prop-setter/main.js | 14 + .../unknown-prop-unknown-access/_config.js | 11 + .../unknown-prop-unknown-access/main.js | 14 + .../unknown-super-prop/_config.js | 12 + .../unknown-super-prop/main.js | 16 + 219 files changed, 3952 insertions(+), 1701 deletions(-) create mode 100644 src/ast/NodeEvents.ts create mode 100644 src/ast/nodes/shared/ArrayPrototype.ts create mode 100644 src/ast/nodes/shared/MethodBase.ts create mode 100644 src/ast/nodes/shared/MethodTypes.ts create mode 100644 src/ast/nodes/shared/ObjectEntity.ts create mode 100644 src/ast/nodes/shared/ObjectMember.ts create mode 100644 src/ast/nodes/shared/ObjectPrototype.ts create mode 100644 test/form/samples/array-element-tracking/array-elements/_config.js create mode 100644 test/form/samples/array-element-tracking/array-elements/_expected.js create mode 100644 test/form/samples/array-element-tracking/array-elements/main.js create mode 100644 test/form/samples/array-element-tracking/array-mutation/_config.js create mode 100644 test/form/samples/array-element-tracking/array-mutation/_expected.js create mode 100644 test/form/samples/array-element-tracking/array-mutation/main.js create mode 100644 test/form/samples/array-element-tracking/array-spread/_config.js create mode 100644 test/form/samples/array-element-tracking/array-spread/_expected.js create mode 100644 test/form/samples/array-element-tracking/array-spread/main.js create mode 100644 test/form/samples/array-element-tracking/spread-element-deoptimization/_config.js create mode 100644 test/form/samples/array-element-tracking/spread-element-deoptimization/_expected.js create mode 100644 test/form/samples/array-element-tracking/spread-element-deoptimization/main.js create mode 100644 test/form/samples/class-method-access/_config.js create mode 100644 test/form/samples/class-method-access/_expected.js create mode 100644 test/form/samples/class-method-access/main.js create mode 100644 test/form/samples/literals-from-class-statics/_config.js create mode 100644 test/form/samples/literals-from-class-statics/_expected.js create mode 100644 test/form/samples/literals-from-class-statics/main.js create mode 100644 test/form/samples/minimal-this-mutation/_config.js create mode 100644 test/form/samples/minimal-this-mutation/_expected.js create mode 100644 test/form/samples/minimal-this-mutation/main.js create mode 100644 test/form/samples/namespace-missing-export-effects/_config.js create mode 100644 test/form/samples/namespace-missing-export-effects/_expected.js create mode 100644 test/form/samples/namespace-missing-export-effects/main.js create mode 100644 test/form/samples/namespace-missing-export-effects/other.js rename test/form/samples/namespace-optimization/{_expected/es.js => _expected.js} (100%) delete mode 100644 test/form/samples/namespace-optimization/_expected/amd.js delete mode 100644 test/form/samples/namespace-optimization/_expected/cjs.js delete mode 100644 test/form/samples/namespace-optimization/_expected/iife.js delete mode 100644 test/form/samples/namespace-optimization/_expected/system.js delete mode 100644 test/form/samples/namespace-optimization/_expected/umd.js create mode 100644 test/form/samples/nested-deoptimization/_config.js create mode 100644 test/form/samples/nested-deoptimization/_expected.js create mode 100644 test/form/samples/nested-deoptimization/main.js create mode 100644 test/form/samples/property-setters-and-getters/shadowed-setters/_config.js create mode 100644 test/form/samples/property-setters-and-getters/shadowed-setters/_expected.js create mode 100644 test/form/samples/property-setters-and-getters/shadowed-setters/main.js create mode 100644 test/form/samples/side-effects-class-getters-setters/_config.js create mode 100644 test/form/samples/side-effects-class-getters-setters/_expected.js create mode 100644 test/form/samples/side-effects-class-getters-setters/main.js create mode 100644 test/form/samples/side-effects-static-methods/_config.js create mode 100644 test/form/samples/side-effects-static-methods/_expected.js create mode 100644 test/form/samples/side-effects-static-methods/main.js rename test/form/samples/skips-dead-branches/{_expected/es.js => _expected.js} (100%) delete mode 100644 test/form/samples/skips-dead-branches/_expected/amd.js delete mode 100644 test/form/samples/skips-dead-branches/_expected/cjs.js delete mode 100644 test/form/samples/skips-dead-branches/_expected/iife.js delete mode 100644 test/form/samples/skips-dead-branches/_expected/system.js delete mode 100644 test/form/samples/skips-dead-branches/_expected/umd.js create mode 100644 test/form/samples/static-class-property-calls/_config.js create mode 100644 test/form/samples/static-class-property-calls/_expected.js create mode 100644 test/form/samples/static-class-property-calls/main.js create mode 100644 test/form/samples/super-classes/super-class-prototype-access/_config.js create mode 100644 test/form/samples/super-classes/super-class-prototype-access/_expected.js create mode 100644 test/form/samples/super-classes/super-class-prototype-access/main.js create mode 100644 test/form/samples/super-classes/super-class-prototype-assignment/_config.js create mode 100644 test/form/samples/super-classes/super-class-prototype-assignment/_expected.js create mode 100644 test/form/samples/super-classes/super-class-prototype-assignment/main.js create mode 100644 test/form/samples/super-classes/super-class-prototype-calls/_config.js create mode 100644 test/form/samples/super-classes/super-class-prototype-calls/_expected.js create mode 100644 test/form/samples/super-classes/super-class-prototype-calls/main.js create mode 100644 test/form/samples/super-classes/super-class-prototype-values/_config.js create mode 100644 test/form/samples/super-classes/super-class-prototype-values/_expected.js create mode 100644 test/form/samples/super-classes/super-class-prototype-values/main.js create mode 100644 test/form/samples/this-in-class-body/_config.js create mode 100644 test/form/samples/this-in-class-body/_expected.js create mode 100644 test/form/samples/this-in-class-body/main.js create mode 100644 test/function/samples/access-instance-prop/_config.js create mode 100644 test/function/samples/access-instance-prop/main.js create mode 100644 test/function/samples/array-getter/_config.js create mode 100644 test/function/samples/array-getter/main.js create mode 100644 test/function/samples/array-mutation/_config.js create mode 100644 test/function/samples/array-mutation/main.js create mode 100644 test/function/samples/conditionals-deoptimization/_config.js create mode 100644 test/function/samples/conditionals-deoptimization/main.js create mode 100644 test/function/samples/deoptimize-assumes-getters/_config.js create mode 100644 test/function/samples/deoptimize-assumes-getters/main.js create mode 100644 test/function/samples/deoptimize-class/_config.js create mode 100644 test/function/samples/deoptimize-class/main.js create mode 100644 test/function/samples/deoptimize-computed-class-keys/_config.js create mode 100644 test/function/samples/deoptimize-computed-class-keys/main.js create mode 100644 test/function/samples/deoptimize-computed-keys/_config.js create mode 100644 test/function/samples/deoptimize-computed-keys/main.js create mode 100644 test/function/samples/deoptimize-object/_config.js create mode 100644 test/function/samples/deoptimize-object/main.js create mode 100644 test/function/samples/deoptimize-this-parameters/_config.js create mode 100644 test/function/samples/deoptimize-this-parameters/main.js create mode 100644 test/function/samples/modify-object-via-this-d/_config.js create mode 100644 test/function/samples/modify-object-via-this-d/main.js create mode 100644 test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/_config.js create mode 100644 test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/main.js create mode 100644 test/function/samples/modify-this-via-getter/getter-in-assignment/_config.js create mode 100644 test/function/samples/modify-this-via-getter/getter-in-assignment/main.js create mode 100644 test/function/samples/modify-this-via-getter/getters-on-this/_config.js create mode 100644 test/function/samples/modify-this-via-getter/getters-on-this/main.js create mode 100644 test/function/samples/modify-this-via-getter/known-getter/_config.js create mode 100644 test/function/samples/modify-this-via-getter/known-getter/main.js create mode 100644 test/function/samples/modify-this-via-getter/known-super-prop/_config.js create mode 100644 test/function/samples/modify-this-via-getter/known-super-prop/main.js create mode 100644 test/function/samples/modify-this-via-getter/unknown-prop-getter/_config.js create mode 100644 test/function/samples/modify-this-via-getter/unknown-prop-getter/main.js create mode 100644 test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/_config.js create mode 100644 test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/main.js create mode 100644 test/function/samples/modify-this-via-getter/unknown-property-access/_config.js create mode 100644 test/function/samples/modify-this-via-getter/unknown-property-access/main.js create mode 100644 test/function/samples/modify-this-via-getter/unknown-super-prop/_config.js create mode 100644 test/function/samples/modify-this-via-getter/unknown-super-prop/main.js create mode 100644 test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/_config.js create mode 100644 test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/main.js create mode 100644 test/function/samples/modify-this-via-setter/known-setter/_config.js create mode 100644 test/function/samples/modify-this-via-setter/known-setter/main.js create mode 100644 test/function/samples/modify-this-via-setter/known-super-prop/_config.js create mode 100644 test/function/samples/modify-this-via-setter/known-super-prop/main.js create mode 100644 test/function/samples/modify-this-via-setter/unknown-prop-setter/_config.js create mode 100644 test/function/samples/modify-this-via-setter/unknown-prop-setter/main.js create mode 100644 test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/_config.js create mode 100644 test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/main.js create mode 100644 test/function/samples/modify-this-via-setter/unknown-super-prop/_config.js create mode 100644 test/function/samples/modify-this-via-setter/unknown-super-prop/main.js diff --git a/cli/run/build.ts b/cli/run/build.ts index 38c55e2e05f..c5fab831470 100644 --- a/cli/run/build.ts +++ b/cli/run/build.ts @@ -24,9 +24,7 @@ export default async function build( } else if (inputOptions.input instanceof Array) { inputFiles = inputOptions.input.join(', '); } else if (typeof inputOptions.input === 'object' && inputOptions.input !== null) { - inputFiles = Object.keys(inputOptions.input) - .map(name => (inputOptions.input as Record)[name]) - .join(', '); + inputFiles = Object.values(inputOptions.input).join(', '); } stderr(cyan(`\n${bold(inputFiles!)} → ${bold(files.join(', '))}...`)); } diff --git a/cli/run/watch-cli.ts b/cli/run/watch-cli.ts index fb2ef95de75..f6d33fc4fd4 100644 --- a/cli/run/watch-cli.ts +++ b/cli/run/watch-cli.ts @@ -106,9 +106,7 @@ export async function watch(command: any) { if (typeof input !== 'string') { input = Array.isArray(input) ? input.join(', ') - : Object.keys(input as Record) - .map(key => (input as Record)[key]) - .join(', '); + : Object.values(input as Record).join(', '); } stderr( cyan(`bundles ${bold(input)} → ${bold(event.output.map(relativeId).join(', '))}...`) diff --git a/package-lock.json b/package-lock.json index 0830654b069..a3f12ec6d51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "rollup", - "version": "2.48.0", + "version": "2.49.0-1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index cb616b25548..cbb4e3f6b96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rollup", - "version": "2.48.0", + "version": "2.49.0-1", "description": "Next-generation ES module bundler", "main": "dist/rollup.js", "module": "dist/es/rollup.js", diff --git a/src/Bundle.ts b/src/Bundle.ts index 01e731fa2a7..84b506f037d 100644 --- a/src/Bundle.ts +++ b/src/Bundle.ts @@ -6,6 +6,7 @@ import { GetManualChunk, NormalizedInputOptions, NormalizedOutputOptions, + OutputAsset, OutputBundle, OutputBundleWithPlaceholders, OutputChunk, @@ -41,11 +42,7 @@ export default class Bundle { async generate(isWrite: boolean): Promise { timeStart('GENERATE', 1); const outputBundle: OutputBundleWithPlaceholders = Object.create(null); - this.pluginDriver.setOutputBundle( - outputBundle, - this.outputOptions, - this.facadeChunkByModule - ); + this.pluginDriver.setOutputBundle(outputBundle, this.outputOptions, this.facadeChunkByModule); try { await this.pluginDriver.hookParallel('renderStart', [this.outputOptions, this.inputOptions]); @@ -104,9 +101,9 @@ export default class Bundle { ): Promise> { const manualChunkAliasByEntry = new Map(); const chunkEntries = await Promise.all( - Object.keys(manualChunks).map(async alias => ({ + Object.entries(manualChunks).map(async ([alias, files]) => ({ alias, - entries: await this.graph.moduleLoader.addAdditionalModules(manualChunks[alias]) + entries: await this.graph.moduleLoader.addAdditionalModules(files) })) ); for (const { alias, entries } of chunkEntries) { @@ -169,24 +166,23 @@ export default class Bundle { } private finaliseAssets(outputBundle: OutputBundleWithPlaceholders): void { - for (const key of Object.keys(outputBundle)) { - const file = outputBundle[key] as any; + for (const file of Object.values(outputBundle)) { if (!file.type) { warnDeprecation( 'A plugin is directly adding properties to the bundle object in the "generateBundle" hook. This is deprecated and will be removed in a future Rollup version, please use "this.emitFile" instead.', true, this.inputOptions ); - file.type = 'asset'; + (file as OutputAsset).type = 'asset'; } - if (this.outputOptions.validate && typeof file.code == 'string') { + if (this.outputOptions.validate && typeof (file as OutputChunk).code == 'string') { try { - this.graph.contextParse(file.code, { + this.graph.contextParse((file as OutputChunk).code, { allowHashBang: true, ecmaVersion: 'latest' }); } catch (exception) { - this.inputOptions.onwarn(errChunkInvalid(file, exception)); + this.inputOptions.onwarn(errChunkInvalid(file as OutputChunk, exception)); } } } diff --git a/src/Chunk.ts b/src/Chunk.ts index f8258da7b0c..cc03a6e8086 100644 --- a/src/Chunk.ts +++ b/src/Chunk.ts @@ -1326,8 +1326,8 @@ export default class Chunk { if (!this.outputOptions.preserveModules) { if (this.includedNamespaces.has(module)) { const memberVariables = module.namespace.getMemberVariables(); - for (const name of Object.keys(memberVariables)) { - moduleImports.add(memberVariables[name]); + for (const variable of Object.values(memberVariables)) { + moduleImports.add(variable); } } } diff --git a/src/Graph.ts b/src/Graph.ts index 9afb0fdda66..e62703cb37a 100644 --- a/src/Graph.ts +++ b/src/Graph.ts @@ -34,9 +34,9 @@ function normalizeEntryModules( name: null })); } - return Object.keys(entryModules).map(name => ({ + return Object.entries(entryModules).map(([name, id]) => ({ fileName: null, - id: entryModules[name], + id, implicitlyLoadedAfter: [], importer: undefined, name @@ -74,13 +74,14 @@ export default class Graph { // increment access counter for (const name in this.pluginCache) { const cache = this.pluginCache[name]; - for (const key of Object.keys(cache)) cache[key][0]++; + for (const value of Object.values(cache)) value[0]++; } } if (watcher) { this.watchMode = true; - const handleChange: WatchChangeHook = (...args) => this.pluginDriver.hookSeqSync('watchChange', args); + const handleChange: WatchChangeHook = (...args) => + this.pluginDriver.hookSeqSync('watchChange', args); const handleClose = () => this.pluginDriver.hookSeqSync('closeWatcher', []); watcher.on('change', handleChange); watcher.on('close', handleClose); @@ -118,9 +119,9 @@ export default class Graph { if (onCommentOrig && typeof onCommentOrig == 'function') { options.onComment = (block, text, start, end, ...args) => { - comments.push({type: block ? "Block" : "Line", value: text, start, end}); + comments.push({ type: block ? 'Block' : 'Line', value: text, start, end }); return onCommentOrig.call(options, block, text, start, end, ...args); - } + }; } else { options.onComment = comments; } @@ -146,8 +147,8 @@ export default class Graph { for (const name in this.pluginCache) { const cache = this.pluginCache[name]; let allDeleted = true; - for (const key of Object.keys(cache)) { - if (cache[key][0] >= this.options.experimentalCacheExpiry) delete cache[key]; + for (const [key, value] of Object.entries(cache)) { + if (value[0] >= this.options.experimentalCacheExpiry) delete cache[key]; else allDeleted = false; } if (allDeleted) delete this.pluginCache[name]; @@ -238,8 +239,7 @@ export default class Graph { private warnForMissingExports() { for (const module of this.modules) { - for (const importName of Object.keys(module.importDescriptions)) { - const importDescription = module.importDescriptions[importName]; + for (const importDescription of Object.values(module.importDescriptions)) { if ( importDescription.name !== '*' && !(importDescription.module as Module).getVariableForExportName(importDescription.name) diff --git a/src/Module.ts b/src/Module.ts index 26d896a5901..db1f7eeb4d4 100644 --- a/src/Module.ts +++ b/src/Module.ts @@ -1002,8 +1002,7 @@ export default class Module { private addModulesToImportDescriptions(importDescription: { [name: string]: ImportDescription | ReexportDescription; }) { - for (const name of Object.keys(importDescription)) { - const specifier = importDescription[name]; + for (const specifier of Object.values(importDescription)) { const id = this.resolvedIds[specifier.source].id; specifier.module = this.graph.modulesById.get(id)!; } diff --git a/src/ast/CallOptions.ts b/src/ast/CallOptions.ts index ad368d69951..bff7691ea20 100644 --- a/src/ast/CallOptions.ts +++ b/src/ast/CallOptions.ts @@ -5,5 +5,6 @@ export const NO_ARGS = []; export interface CallOptions { args: (ExpressionEntity | SpreadElement)[]; + thisParam: ExpressionEntity | null; withNew: boolean; } diff --git a/src/ast/Entity.ts b/src/ast/Entity.ts index f67667c74c4..e08a3d9b33d 100644 --- a/src/ast/Entity.ts +++ b/src/ast/Entity.ts @@ -1,9 +1,7 @@ import { HasEffectsContext } from './ExecutionContext'; import { ObjectPath } from './utils/PathTracker'; -export interface Entity { - toString: () => string; -} +export interface Entity {} export interface WritableEntity extends Entity { /** @@ -13,5 +11,5 @@ export interface WritableEntity extends Entity { * expression of this node is reassigned as well. */ deoptimizePath(path: ObjectPath): void; - hasEffectsWhenAssignedAtPath(path: ObjectPath, execution: HasEffectsContext): boolean; + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean; } diff --git a/src/ast/NodeEvents.ts b/src/ast/NodeEvents.ts new file mode 100644 index 00000000000..e952677032a --- /dev/null +++ b/src/ast/NodeEvents.ts @@ -0,0 +1,4 @@ +export const EVENT_ACCESSED = 0; +export const EVENT_ASSIGNED = 1; +export const EVENT_CALLED = 2; +export type NodeEvent = typeof EVENT_ACCESSED | typeof EVENT_ASSIGNED | typeof EVENT_CALLED; diff --git a/src/ast/keys.ts b/src/ast/keys.ts index 9434d9aae03..abd258e6e37 100644 --- a/src/ast/keys.ts +++ b/src/ast/keys.ts @@ -9,7 +9,7 @@ export const keys: { export function getAndCreateKeys(esTreeNode: GenericEsTreeNode) { keys[esTreeNode.type] = Object.keys(esTreeNode).filter( - key => key !== '_rollupAnnotations' && typeof esTreeNode[key] === 'object' + key => typeof esTreeNode[key] === 'object' && key !== '_rollupAnnotations' ); return keys[esTreeNode.type]; } diff --git a/src/ast/nodes/ArrayExpression.ts b/src/ast/nodes/ArrayExpression.ts index f37b81dbbc5..34c24bef3f6 100644 --- a/src/ast/nodes/ArrayExpression.ts +++ b/src/ast/nodes/ArrayExpression.ts @@ -1,34 +1,67 @@ import { CallOptions } from '../CallOptions'; +import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; -import { ObjectPath, UNKNOWN_PATH } from '../utils/PathTracker'; -import { - arrayMembers, - getMemberReturnExpressionWhenCalled, - hasMemberEffectWhenCalled, - UNKNOWN_EXPRESSION -} from '../values'; +import { NodeEvent } from '../NodeEvents'; +import { ObjectPath, PathTracker, UnknownInteger } from '../utils/PathTracker'; +import { UNDEFINED_EXPRESSION, UNKNOWN_LITERAL_NUMBER } from '../values'; import * as NodeType from './NodeType'; +import { ARRAY_PROTOTYPE } from './shared/ArrayPrototype'; +import { ExpressionEntity, LiteralValueOrUnknown } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; +import { ObjectEntity, ObjectProperty } from './shared/ObjectEntity'; import SpreadElement from './SpreadElement'; export default class ArrayExpression extends NodeBase { elements!: (ExpressionNode | SpreadElement | null)[]; type!: NodeType.tArrayExpression; + private objectEntity: ObjectEntity | null = null; - bind() { - super.bind(); - for (const element of this.elements) { - if (element !== null) element.deoptimizePath(UNKNOWN_PATH); - } + deoptimizePath(path: ObjectPath) { + this.getObjectEntity().deoptimizePath(path); + } + + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.getObjectEntity().deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + } + + getLiteralValueAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + return this.getObjectEntity().getLiteralValueAtPath(path, recursionTracker, origin); } - getReturnExpressionWhenCalledAtPath(path: ObjectPath) { - if (path.length !== 1) return UNKNOWN_EXPRESSION; - return getMemberReturnExpressionWhenCalled(arrayMembers, path[0]); + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): ExpressionEntity { + return this.getObjectEntity().getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); + } + + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { + return this.getObjectEntity().hasEffectsWhenAccessedAtPath(path, context); } - hasEffectsWhenAccessedAtPath(path: ObjectPath) { - return path.length > 1; + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext) { + return this.getObjectEntity().hasEffectsWhenAssignedAtPath(path, context); } hasEffectsWhenCalledAtPath( @@ -36,9 +69,26 @@ export default class ArrayExpression extends NodeBase { callOptions: CallOptions, context: HasEffectsContext ): boolean { - if (path.length === 1) { - return hasMemberEffectWhenCalled(arrayMembers, path[0], this.included, callOptions, context); + return this.getObjectEntity().hasEffectsWhenCalledAtPath(path, callOptions, context); + } + + private getObjectEntity(): ObjectEntity { + if (this.objectEntity !== null) { + return this.objectEntity; + } + const properties: ObjectProperty[] = [ + { kind: 'init', key: 'length', property: UNKNOWN_LITERAL_NUMBER } + ]; + for (let index = 0; index < this.elements.length; index++) { + const element = this.elements[index]; + if (element instanceof SpreadElement) { + properties.unshift({ kind: 'init', key: UnknownInteger, property: element }); + } else if (!element) { + properties.push({ kind: 'init', key: String(index), property: UNDEFINED_EXPRESSION }); + } else { + properties.push({ kind: 'init', key: String(index), property: element }); + } } - return true; + return (this.objectEntity = new ObjectEntity(properties, ARRAY_PROTOTYPE)); } } diff --git a/src/ast/nodes/ArrayPattern.ts b/src/ast/nodes/ArrayPattern.ts index e73f591fd7b..e0d23d61be4 100644 --- a/src/ast/nodes/ArrayPattern.ts +++ b/src/ast/nodes/ArrayPattern.ts @@ -1,8 +1,8 @@ import { HasEffectsContext } from '../ExecutionContext'; import { EMPTY_PATH, ObjectPath } from '../utils/PathTracker'; -import { UNKNOWN_EXPRESSION } from '../values'; import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; +import { UNKNOWN_EXPRESSION } from './shared/Expression'; import { NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; diff --git a/src/ast/nodes/ArrowFunctionExpression.ts b/src/ast/nodes/ArrowFunctionExpression.ts index 52c08376857..b9d1ba672f4 100644 --- a/src/ast/nodes/ArrowFunctionExpression.ts +++ b/src/ast/nodes/ArrowFunctionExpression.ts @@ -3,11 +3,11 @@ import { BROKEN_FLOW_NONE, HasEffectsContext, InclusionContext } from '../Execut import ReturnValueScope from '../scopes/ReturnValueScope'; import Scope from '../scopes/Scope'; import { ObjectPath, UnknownKey, UNKNOWN_PATH } from '../utils/PathTracker'; -import { UNKNOWN_EXPRESSION } from '../values'; import BlockStatement from './BlockStatement'; import Identifier from './Identifier'; import * as NodeType from './NodeType'; import RestElement from './RestElement'; +import { UNKNOWN_EXPRESSION } from './shared/Expression'; import { ExpressionNode, GenericEsTreeNode, IncludeChildren, NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; import SpreadElement from './SpreadElement'; @@ -31,6 +31,9 @@ export default class ArrowFunctionExpression extends NodeBase { } } + // Arrow functions do not mutate their context + deoptimizeThisOnEventAtPath() {} + getReturnExpressionWhenCalledAtPath(path: ObjectPath) { return path.length === 0 ? this.scope.getReturnExpression() : UNKNOWN_EXPRESSION; } @@ -98,10 +101,6 @@ export default class ArrowFunctionExpression extends NodeBase { } } - mayModifyThisWhenCalledAtPath() { - return false; - } - parseNode(esTreeNode: GenericEsTreeNode) { if (esTreeNode.body.type === NodeType.BlockStatement) { this.body = new this.context.nodeConstructors.BlockStatement( diff --git a/src/ast/nodes/AssignmentExpression.ts b/src/ast/nodes/AssignmentExpression.ts index f982147be34..a515fc95009 100644 --- a/src/ast/nodes/AssignmentExpression.ts +++ b/src/ast/nodes/AssignmentExpression.ts @@ -33,7 +33,7 @@ export default class AssignmentExpression extends NodeBase { | '**='; right!: ExpressionNode; type!: NodeType.tAssignmentExpression; - private deoptimized = false; + protected deoptimized = false; hasEffects(context: HasEffectsContext): boolean { if (!this.deoptimized) this.applyDeoptimizations(); @@ -122,7 +122,7 @@ export default class AssignmentExpression extends NodeBase { } } - private applyDeoptimizations() { + protected applyDeoptimizations() { this.deoptimized = true; this.left.deoptimizePath(EMPTY_PATH); this.right.deoptimizePath(UNKNOWN_PATH); diff --git a/src/ast/nodes/AssignmentPattern.ts b/src/ast/nodes/AssignmentPattern.ts index 06d6ea3268b..0453a6a1008 100644 --- a/src/ast/nodes/AssignmentPattern.ts +++ b/src/ast/nodes/AssignmentPattern.ts @@ -13,6 +13,7 @@ export default class AssignmentPattern extends NodeBase implements PatternNode { left!: PatternNode; right!: ExpressionNode; type!: NodeType.tAssignmentPattern; + protected deoptimized = false; addExportedVariables( variables: Variable[], @@ -21,12 +22,6 @@ export default class AssignmentPattern extends NodeBase implements PatternNode { this.left.addExportedVariables(variables, exportNamesByVariable); } - bind() { - super.bind(); - this.left.deoptimizePath(EMPTY_PATH); - this.right.deoptimizePath(UNKNOWN_PATH); - } - declare(kind: string, init: ExpressionEntity) { return this.left.declare(kind, init); } @@ -47,4 +42,10 @@ export default class AssignmentPattern extends NodeBase implements PatternNode { this.left.render(code, options, { isShorthandProperty }); this.right.render(code, options); } + + protected applyDeoptimizations():void { + this.deoptimized = true; + this.left.deoptimizePath(EMPTY_PATH); + this.right.deoptimizePath(UNKNOWN_PATH); + } } diff --git a/src/ast/nodes/BinaryExpression.ts b/src/ast/nodes/BinaryExpression.ts index eb85497e630..26f632b307d 100644 --- a/src/ast/nodes/BinaryExpression.ts +++ b/src/ast/nodes/BinaryExpression.ts @@ -6,10 +6,10 @@ import { PathTracker, SHARED_RECURSION_TRACKER } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; import ExpressionStatement from './ExpressionStatement'; import { LiteralValue } from './Literal'; import * as NodeType from './NodeType'; +import { LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; const binaryOperators: { diff --git a/src/ast/nodes/BlockStatement.ts b/src/ast/nodes/BlockStatement.ts index f60c30ee830..614d5aa2de5 100644 --- a/src/ast/nodes/BlockStatement.ts +++ b/src/ast/nodes/BlockStatement.ts @@ -4,9 +4,9 @@ import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import BlockScope from '../scopes/BlockScope'; import ChildScope from '../scopes/ChildScope'; import Scope from '../scopes/Scope'; -import { UNKNOWN_EXPRESSION } from '../values'; import ExpressionStatement from './ExpressionStatement'; import * as NodeType from './NodeType'; +import { UNKNOWN_EXPRESSION } from './shared/Expression'; import { IncludeChildren, Node, StatementBase, StatementNode } from './shared/Node'; export default class BlockStatement extends StatementBase { diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index 4478a272612..a3dffadf3f5 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -9,6 +9,7 @@ import { import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { EVENT_CALLED, NodeEvent } from '../NodeEvents'; import { EMPTY_PATH, ObjectPath, @@ -16,11 +17,15 @@ import { SHARED_RECURSION_TRACKER, UNKNOWN_PATH } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../values'; import Identifier from './Identifier'; import MemberExpression from './MemberExpression'; import * as NodeType from './NodeType'; -import { ExpressionEntity } from './shared/Expression'; +import { + ExpressionEntity, + LiteralValueOrUnknown, + UnknownValue, + UNKNOWN_EXPRESSION +} from './shared/Expression'; import { Annotation, ExpressionNode, @@ -36,11 +41,11 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt callee!: ExpressionNode | Super; optional!: boolean; type!: NodeType.tCallExpression; - + protected deoptimized = false; private callOptions!: CallOptions; - private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; + private deoptimizableDependentExpressions: DeoptimizableEntity[] = []; + private expressionsToBeDeoptimized = new Set(); private returnExpression: ExpressionEntity | null = null; - private wasPathDeoptmizedWhileOptimized = false; bind() { super.bind(); @@ -68,54 +73,68 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt ); } } - // ensure the returnExpression is set for the tree-shaking passes - this.getReturnExpression(SHARED_RECURSION_TRACKER); - // This deoptimizes "this" for non-namespace calls until we have a better solution - if ( - this.callee instanceof MemberExpression && - !this.callee.variable && - this.callee.mayModifyThisWhenCalledAtPath([], SHARED_RECURSION_TRACKER) - ) { - this.callee.object.deoptimizePath(UNKNOWN_PATH); - } - for (const argument of this.arguments) { - // This will make sure all properties of parameters behave as "unknown" - argument.deoptimizePath(UNKNOWN_PATH); - } + this.callOptions = { + args: this.arguments, + thisParam: + this.callee instanceof MemberExpression && !this.callee.variable + ? this.callee.object + : null, + withNew: false + }; } deoptimizeCache() { if (this.returnExpression !== UNKNOWN_EXPRESSION) { - this.returnExpression = null; - const returnExpression = this.getReturnExpression(SHARED_RECURSION_TRACKER); - const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized; - if (returnExpression !== UNKNOWN_EXPRESSION) { - // We need to replace here because is possible new expressions are added - // while we are deoptimizing the old ones - this.expressionsToBeDeoptimized = []; - if (this.wasPathDeoptmizedWhileOptimized) { - returnExpression.deoptimizePath(UNKNOWN_PATH); - this.wasPathDeoptmizedWhileOptimized = false; - } - } - for (const expression of expressionsToBeDeoptimized) { + this.returnExpression = UNKNOWN_EXPRESSION; + for (const expression of this.deoptimizableDependentExpressions) { expression.deoptimizeCache(); } + for (const expression of this.expressionsToBeDeoptimized) { + expression.deoptimizePath(UNKNOWN_PATH); + } } } deoptimizePath(path: ObjectPath) { - if (path.length === 0) return; - const trackedEntities = this.context.deoptimizationTracker.getEntities(path); - if (trackedEntities.has(this)) return; - trackedEntities.add(this); - const returnExpression = this.getReturnExpression(SHARED_RECURSION_TRACKER); + if ( + path.length === 0 || + this.context.deoptimizationTracker.trackEntityAtPathAndGetIfTracked(path, this) + ) { + return; + } + const returnExpression = this.getReturnExpression(); if (returnExpression !== UNKNOWN_EXPRESSION) { - this.wasPathDeoptmizedWhileOptimized = true; returnExpression.deoptimizePath(path); } } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + const returnExpression = this.getReturnExpression(recursionTracker); + if (returnExpression === UNKNOWN_EXPRESSION) { + thisParameter.deoptimizePath(UNKNOWN_PATH); + } else { + recursionTracker.withTrackedEntityAtPath( + path, + returnExpression, + () => { + this.expressionsToBeDeoptimized.add(thisParameter); + returnExpression.deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + }, + undefined + ); + } + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -125,42 +144,45 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt if (returnExpression === UNKNOWN_EXPRESSION) { return UnknownValue; } - const trackedEntities = recursionTracker.getEntities(path); - if (trackedEntities.has(returnExpression)) { - return UnknownValue; - } - this.expressionsToBeDeoptimized.push(origin); - trackedEntities.add(returnExpression); - const value = returnExpression.getLiteralValueAtPath(path, recursionTracker, origin); - trackedEntities.delete(returnExpression); - return value; + return recursionTracker.withTrackedEntityAtPath( + path, + returnExpression, + () => { + this.deoptimizableDependentExpressions.push(origin); + return returnExpression.getLiteralValueAtPath(path, recursionTracker, origin); + }, + UnknownValue + ); } getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity - ) { + ): ExpressionEntity { const returnExpression = this.getReturnExpression(recursionTracker); if (this.returnExpression === UNKNOWN_EXPRESSION) { return UNKNOWN_EXPRESSION; } - const trackedEntities = recursionTracker.getEntities(path); - if (trackedEntities.has(returnExpression)) { - return UNKNOWN_EXPRESSION; - } - this.expressionsToBeDeoptimized.push(origin); - trackedEntities.add(returnExpression); - const value = returnExpression.getReturnExpressionWhenCalledAtPath( + return recursionTracker.withTrackedEntityAtPath( path, - recursionTracker, - origin + returnExpression, + () => { + this.deoptimizableDependentExpressions.push(origin); + return returnExpression.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); + }, + UNKNOWN_EXPRESSION ); - trackedEntities.delete(returnExpression); - return value; } hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); for (const argument of this.arguments) { if (argument.hasEffects(context)) return true; } @@ -176,19 +198,17 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return false; - const trackedExpressions = context.accessed.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return this.returnExpression!.hasEffectsWhenAccessedAtPath(path, context); + return ( + !context.accessed.trackEntityAtPathAndGetIfTracked(path, this) && + this.getReturnExpression().hasEffectsWhenAccessedAtPath(path, context) + ); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return true; - const trackedExpressions = context.assigned.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return this.returnExpression!.hasEffectsWhenAssignedAtPath(path, context); + return ( + !context.assigned.trackEntityAtPathAndGetIfTracked(path, this) && + this.getReturnExpression().hasEffectsWhenAssignedAtPath(path, context) + ); } hasEffectsWhenCalledAtPath( @@ -196,16 +216,17 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt callOptions: CallOptions, context: HasEffectsContext ): boolean { - const trackedExpressions = (callOptions.withNew - ? context.instantiated - : context.called - ).getEntities(path, callOptions); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return this.returnExpression!.hasEffectsWhenCalledAtPath(path, callOptions, context); + return ( + !(callOptions.withNew + ? context.instantiated + : context.called + ).trackEntityAtPathAndGetIfTracked(path, callOptions, this) && + this.getReturnExpression().hasEffectsWhenCalledAtPath(path, callOptions, context) + ); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); if (includeChildrenRecursively) { super.include(context, includeChildrenRecursively); if ( @@ -220,18 +241,12 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt this.callee.include(context, false); } this.callee.includeCallArguments(context, this.arguments); - if (!this.returnExpression!.included) { - this.returnExpression!.include(context, false); + const returnExpression = this.getReturnExpression(); + if (!returnExpression.included) { + returnExpression.include(context, false); } } - initialise() { - this.callOptions = { - args: this.arguments, - withNew: false - }; - } - render( code: MagicString, options: RenderOptions, @@ -275,11 +290,31 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } } - private getReturnExpression(recursionTracker: PathTracker): ExpressionEntity { + protected applyDeoptimizations() { + this.deoptimized = true; + const { thisParam } = this.callOptions; + if (thisParam) { + this.callee.deoptimizeThisOnEventAtPath( + EVENT_CALLED, + EMPTY_PATH, + thisParam, + SHARED_RECURSION_TRACKER + ); + } + for (const argument of this.arguments) { + // This will make sure all properties of parameters behave as "unknown" + argument.deoptimizePath(UNKNOWN_PATH); + } + } + + private getReturnExpression( + recursionTracker: PathTracker = SHARED_RECURSION_TRACKER + ): ExpressionEntity { if (this.returnExpression === null) { this.returnExpression = UNKNOWN_EXPRESSION; return (this.returnExpression = this.callee.getReturnExpressionWhenCalledAtPath( EMPTY_PATH, + this.callOptions, recursionTracker, this )); diff --git a/src/ast/nodes/CatchClause.ts b/src/ast/nodes/CatchClause.ts index c7885e8ab80..77e8eb2eca6 100644 --- a/src/ast/nodes/CatchClause.ts +++ b/src/ast/nodes/CatchClause.ts @@ -1,8 +1,8 @@ import CatchScope from '../scopes/CatchScope'; import Scope from '../scopes/Scope'; -import { UNKNOWN_EXPRESSION } from '../values'; import BlockStatement from './BlockStatement'; import * as NodeType from './NodeType'; +import { UNKNOWN_EXPRESSION } from './shared/Expression'; import { GenericEsTreeNode, NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; diff --git a/src/ast/nodes/ClassBody.ts b/src/ast/nodes/ClassBody.ts index 0dc6efa32a5..3f7c55ff760 100644 --- a/src/ast/nodes/ClassBody.ts +++ b/src/ast/nodes/ClassBody.ts @@ -1,42 +1,40 @@ -import { CallOptions } from '../CallOptions'; -import { HasEffectsContext } from '../ExecutionContext'; +import { InclusionContext } from '../ExecutionContext'; import ClassBodyScope from '../scopes/ClassBodyScope'; import Scope from '../scopes/Scope'; -import { EMPTY_PATH, ObjectPath } from '../utils/PathTracker'; import MethodDefinition from './MethodDefinition'; import * as NodeType from './NodeType'; import PropertyDefinition from './PropertyDefinition'; -import { NodeBase } from './shared/Node'; +import ClassNode from './shared/ClassNode'; +import { GenericEsTreeNode, IncludeChildren, NodeBase } from './shared/Node'; export default class ClassBody extends NodeBase { body!: (MethodDefinition | PropertyDefinition)[]; + scope!: ClassBodyScope; type!: NodeType.tClassBody; - private classConstructor!: MethodDefinition | null; - createScope(parentScope: Scope) { - this.scope = new ClassBodyScope(parentScope); + this.scope = new ClassBodyScope(parentScope, this.parent as ClassNode, this.context); } - hasEffectsWhenCalledAtPath( - path: ObjectPath, - callOptions: CallOptions, - context: HasEffectsContext - ) { - if (path.length > 0) return true; - return ( - this.classConstructor !== null && - this.classConstructor.hasEffectsWhenCalledAtPath(EMPTY_PATH, callOptions, context) - ); + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + this.included = true; + this.context.includeVariableInModule(this.scope.thisVariable); + for (const definition of this.body) { + definition.include(context, includeChildrenRecursively); + } } - initialise() { - for (const method of this.body) { - if (method instanceof MethodDefinition && method.kind === 'constructor') { - this.classConstructor = method; - return; - } + parseNode(esTreeNode: GenericEsTreeNode) { + const body: NodeBase[] = (this.body = []); + for (const definition of esTreeNode.body) { + body.push( + new this.context.nodeConstructors[definition.type]( + definition, + this, + definition.static ? this.scope : this.scope.instanceScope + ) + ); } - this.classConstructor = null; + super.parseNode(esTreeNode); } } diff --git a/src/ast/nodes/ConditionalExpression.ts b/src/ast/nodes/ConditionalExpression.ts index 2b1dfea88a3..d11b2869319 100644 --- a/src/ast/nodes/ConditionalExpression.ts +++ b/src/ast/nodes/ConditionalExpression.ts @@ -11,17 +11,11 @@ import { removeAnnotations } from '../../utils/treeshakeNode'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { - EMPTY_PATH, - ObjectPath, - PathTracker, - SHARED_RECURSION_TRACKER, - UNKNOWN_PATH -} from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; +import { NodeEvent } from '../NodeEvents'; +import { EMPTY_PATH, ObjectPath, PathTracker, SHARED_RECURSION_TRACKER, UNKNOWN_PATH } from '../utils/PathTracker'; import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; -import { ExpressionEntity } from './shared/Expression'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { MultiExpression } from './shared/MultiExpression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; import SpreadElement from './SpreadElement'; @@ -35,42 +29,38 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; private isBranchResolutionAnalysed = false; private usedBranch: ExpressionNode | null = null; - private wasPathDeoptimizedWhileOptimized = false; - - bind() { - super.bind(); - // ensure the usedBranch is set for the tree-shaking passes - this.getUsedBranch(); - } deoptimizeCache() { if (this.usedBranch !== null) { const unusedBranch = this.usedBranch === this.consequent ? this.alternate : this.consequent; this.usedBranch = null; - const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized; - this.expressionsToBeDeoptimized = []; - if (this.wasPathDeoptimizedWhileOptimized) { - unusedBranch.deoptimizePath(UNKNOWN_PATH); - } - for (const expression of expressionsToBeDeoptimized) { + unusedBranch.deoptimizePath(UNKNOWN_PATH); + for (const expression of this.expressionsToBeDeoptimized) { expression.deoptimizeCache(); } } } deoptimizePath(path: ObjectPath) { - if (path.length > 0) { - const usedBranch = this.getUsedBranch(); - if (usedBranch === null) { - this.consequent.deoptimizePath(path); - this.alternate.deoptimizePath(path); - } else { - this.wasPathDeoptimizedWhileOptimized = true; - usedBranch.deoptimizePath(path); - } + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { + this.consequent.deoptimizePath(path); + this.alternate.deoptimizePath(path); + } else { + usedBranch.deoptimizePath(path); } } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.consequent.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + this.alternate.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -84,47 +74,64 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { const usedBranch = this.getUsedBranch(); if (usedBranch === null) return new MultiExpression([ - this.consequent.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin), - this.alternate.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin) + this.consequent.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ), + this.alternate.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ) ]); this.expressionsToBeDeoptimized.push(origin); - return usedBranch.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); + return usedBranch.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); } hasEffects(context: HasEffectsContext): boolean { if (this.test.hasEffects(context)) return true; - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { return this.consequent.hasEffects(context) || this.alternate.hasEffects(context); } - return this.usedBranch.hasEffects(context); + return usedBranch.hasEffects(context); } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return false; - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { return ( this.consequent.hasEffectsWhenAccessedAtPath(path, context) || this.alternate.hasEffectsWhenAccessedAtPath(path, context) ); } - return this.usedBranch.hasEffectsWhenAccessedAtPath(path, context); + return usedBranch.hasEffectsWhenAccessedAtPath(path, context); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return true; - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { return ( this.consequent.hasEffectsWhenAssignedAtPath(path, context) || this.alternate.hasEffectsWhenAssignedAtPath(path, context) ); } - return this.usedBranch.hasEffectsWhenAssignedAtPath(path, context); + return usedBranch.hasEffectsWhenAssignedAtPath(path, context); } hasEffectsWhenCalledAtPath( @@ -132,36 +139,35 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz callOptions: CallOptions, context: HasEffectsContext ): boolean { - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { return ( this.consequent.hasEffectsWhenCalledAtPath(path, callOptions, context) || this.alternate.hasEffectsWhenCalledAtPath(path, callOptions, context) ); } - return this.usedBranch.hasEffectsWhenCalledAtPath(path, callOptions, context); + return usedBranch.hasEffectsWhenCalledAtPath(path, callOptions, context); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { this.included = true; - if ( - includeChildrenRecursively || - this.test.shouldBeIncluded(context) || - this.usedBranch === null - ) { + const usedBranch = this.getUsedBranch(); + if (includeChildrenRecursively || this.test.shouldBeIncluded(context) || usedBranch === null) { this.test.include(context, includeChildrenRecursively); this.consequent.include(context, includeChildrenRecursively); this.alternate.include(context, includeChildrenRecursively); } else { - this.usedBranch.include(context, includeChildrenRecursively); + usedBranch.include(context, includeChildrenRecursively); } } includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { this.consequent.includeCallArguments(context, args); this.alternate.includeCallArguments(context, args); } else { - this.usedBranch.includeCallArguments(context, args); + usedBranch.includeCallArguments(context, args); } } @@ -170,6 +176,7 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz options: RenderOptions, { renderedParentType, isCalleeOfRenderedParent, preventASI }: NodeRenderOptions = BLANK ) { + const usedBranch = this.getUsedBranch(); if (!this.test.included) { const colonPos = findFirstOccurrenceOutsideComment(code.original, ':', this.consequent.end); const inclusionStart = findNonWhiteSpace( @@ -179,14 +186,14 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz : colonPos) + 1 ); if (preventASI) { - removeLineBreaks(code, inclusionStart, this.usedBranch!.start); + removeLineBreaks(code, inclusionStart, usedBranch!.start); } code.remove(this.start, inclusionStart); if (this.consequent.included) { code.remove(colonPos, this.end); } removeAnnotations(this, code); - this.usedBranch!.render(code, options, { + usedBranch!.render(code, options, { isCalleeOfRenderedParent: renderedParentType ? isCalleeOfRenderedParent : (this.parent as CallExpression).callee === this, diff --git a/src/ast/nodes/ForInStatement.ts b/src/ast/nodes/ForInStatement.ts index 609f0d022c2..4c92457fb19 100644 --- a/src/ast/nodes/ForInStatement.ts +++ b/src/ast/nodes/ForInStatement.ts @@ -14,19 +14,14 @@ export default class ForInStatement extends StatementBase { left!: VariableDeclaration | PatternNode; right!: ExpressionNode; type!: NodeType.tForInStatement; - - bind() { - this.left.bind(); - this.left.deoptimizePath(EMPTY_PATH); - this.right.bind(); - this.body.bind(); - } + protected deoptimized = false; createScope(parentScope: Scope) { this.scope = new BlockScope(parentScope); } hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); if ( (this.left && (this.left.hasEffects(context) || @@ -48,9 +43,9 @@ export default class ForInStatement extends StatementBase { } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); this.included = true; this.left.include(context, includeChildrenRecursively || true); - this.left.deoptimizePath(EMPTY_PATH); this.right.include(context, includeChildrenRecursively); const { brokenFlow } = context; this.body.includeAsSingleStatement(context, includeChildrenRecursively); @@ -66,4 +61,9 @@ export default class ForInStatement extends StatementBase { } this.body.render(code, options); } + + protected applyDeoptimizations():void { + this.deoptimized = true; + this.left.deoptimizePath(EMPTY_PATH); + } } diff --git a/src/ast/nodes/ForOfStatement.ts b/src/ast/nodes/ForOfStatement.ts index 65527662fc0..030d36ece9e 100644 --- a/src/ast/nodes/ForOfStatement.ts +++ b/src/ast/nodes/ForOfStatement.ts @@ -15,27 +15,22 @@ export default class ForOfStatement extends StatementBase { left!: VariableDeclaration | PatternNode; right!: ExpressionNode; type!: NodeType.tForOfStatement; - - bind() { - this.left.bind(); - this.left.deoptimizePath(EMPTY_PATH); - this.right.bind(); - this.body.bind(); - } + protected deoptimized = false; createScope(parentScope: Scope) { this.scope = new BlockScope(parentScope); } hasEffects(): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); // Placeholder until proper Symbol.Iterator support return true; } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); this.included = true; this.left.include(context, includeChildrenRecursively || true); - this.left.deoptimizePath(EMPTY_PATH); this.right.include(context, includeChildrenRecursively); const { brokenFlow } = context; this.body.includeAsSingleStatement(context, includeChildrenRecursively); @@ -51,4 +46,9 @@ export default class ForOfStatement extends StatementBase { } this.body.render(code, options); } + + protected applyDeoptimizations(): void { + this.deoptimized = true; + this.left.deoptimizePath(EMPTY_PATH); + } } diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index 0c1394198ef..6fabee91ebf 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -6,14 +6,14 @@ import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { NodeEvent } from '../NodeEvents'; import FunctionScope from '../scopes/FunctionScope'; import { EMPTY_PATH, ObjectPath, PathTracker } from '../utils/PathTracker'; -import { LiteralValueOrUnknown } from '../values'; import GlobalVariable from '../variables/GlobalVariable'; import LocalVariable from '../variables/LocalVariable'; import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; -import { ExpressionEntity } from './shared/Expression'; +import { ExpressionEntity, LiteralValueOrUnknown } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; import SpreadElement from './SpreadElement'; @@ -25,7 +25,7 @@ export default class Identifier extends NodeBase implements PatternNode { type!: NodeType.tIdentifier; variable: Variable | null = null; - private bound = false; + protected deoptimized = false; addExportedVariables( variables: Variable[], @@ -37,19 +37,10 @@ export default class Identifier extends NodeBase implements PatternNode { } bind() { - if (this.bound) return; - this.bound = true; if (this.variable === null && isReference(this, this.parent as any)) { this.variable = this.scope.findVariable(this.name); this.variable.addReference(this); } - if ( - this.variable !== null && - this.variable instanceof LocalVariable && - this.variable.additionalInitializers !== null - ) { - this.variable.consolidateInitializers(); - } } declare(kind: string, init: ExpressionEntity) { @@ -79,32 +70,45 @@ export default class Identifier extends NodeBase implements PatternNode { } deoptimizePath(path: ObjectPath) { - if (!this.bound) this.bind(); if (path.length === 0 && !this.scope.contains(this.name)) { this.disallowImportReassignment(); } this.variable!.deoptimizePath(path); } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.variable!.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, origin: DeoptimizableEntity ): LiteralValueOrUnknown { - if (!this.bound) this.bind(); return this.variable!.getLiteralValueAtPath(path, recursionTracker, origin); } getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity - ) { - if (!this.bound) this.bind(); - return this.variable!.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); + ): ExpressionEntity { + return this.variable!.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); } hasEffects(): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); return ( (this.context.options.treeshake as NormalizedTreeshakingOptions).unknownGlobalSideEffects && this.variable instanceof GlobalVariable && @@ -129,6 +133,7 @@ export default class Identifier extends NodeBase implements PatternNode { } include() { + if (!this.deoptimized) this.applyDeoptimizations(); if (!this.included) { this.included = true; if (this.variable !== null) { @@ -141,12 +146,6 @@ export default class Identifier extends NodeBase implements PatternNode { this.variable!.includeCallArguments(context, args); } - mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker) { - return this.variable - ? this.variable.mayModifyThisWhenCalledAtPath(path, recursionTracker) - : true; - } - render( code: MagicString, _options: RenderOptions, @@ -175,6 +174,16 @@ export default class Identifier extends NodeBase implements PatternNode { } } + protected applyDeoptimizations() { + this.deoptimized = true; + if ( + this.variable !== null && + this.variable instanceof LocalVariable + ) { + this.variable.consolidateInitializers(); + } + } + private disallowImportReassignment() { return this.context.error( { diff --git a/src/ast/nodes/IfStatement.ts b/src/ast/nodes/IfStatement.ts index 23667804428..4231e1382d3 100644 --- a/src/ast/nodes/IfStatement.ts +++ b/src/ast/nodes/IfStatement.ts @@ -5,10 +5,10 @@ import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { BROKEN_FLOW_NONE, HasEffectsContext, InclusionContext } from '../ExecutionContext'; import TrackingScope from '../scopes/TrackingScope'; import { EMPTY_PATH, SHARED_RECURSION_TRACKER } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; import BlockStatement from './BlockStatement'; import Identifier from './Identifier'; import * as NodeType from './NodeType'; +import { LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { ExpressionNode, GenericEsTreeNode, diff --git a/src/ast/nodes/ImportDeclaration.ts b/src/ast/nodes/ImportDeclaration.ts index 450381bad86..0f7d177f633 100644 --- a/src/ast/nodes/ImportDeclaration.ts +++ b/src/ast/nodes/ImportDeclaration.ts @@ -13,6 +13,7 @@ export default class ImportDeclaration extends NodeBase { specifiers!: (ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier)[]; type!: NodeType.tImportDeclaration; + // Do not bind specifiers bind() {} hasEffects() { diff --git a/src/ast/nodes/Literal.ts b/src/ast/nodes/Literal.ts index fc55458ea11..d890950c2ea 100644 --- a/src/ast/nodes/Literal.ts +++ b/src/ast/nodes/Literal.ts @@ -6,12 +6,10 @@ import { getLiteralMembersForValue, getMemberReturnExpressionWhenCalled, hasMemberEffectWhenCalled, - LiteralValueOrUnknown, - MemberDescription, - UnknownValue, - UNKNOWN_EXPRESSION + MemberDescription } from '../values'; import * as NodeType from './NodeType'; +import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from './shared/Expression'; import { GenericEsTreeNode, NodeBase } from './shared/Node'; export type LiteralValue = string | boolean | null | number | RegExp | undefined; @@ -26,6 +24,8 @@ export default class Literal extends Node private members!: { [key: string]: MemberDescription }; + deoptimizeThisOnEventAtPath() {} + getLiteralValueAtPath(path: ObjectPath): LiteralValueOrUnknown { if ( path.length > 0 || @@ -62,7 +62,7 @@ export default class Literal extends Node context: HasEffectsContext ): boolean { if (path.length === 1) { - return hasMemberEffectWhenCalled(this.members, path[0], this.included, callOptions, context); + return hasMemberEffectWhenCalled(this.members, path[0], callOptions, context); } return true; } diff --git a/src/ast/nodes/LogicalExpression.ts b/src/ast/nodes/LogicalExpression.ts index e19ba265c9e..9faf815cdcd 100644 --- a/src/ast/nodes/LogicalExpression.ts +++ b/src/ast/nodes/LogicalExpression.ts @@ -11,6 +11,7 @@ import { removeAnnotations } from '../../utils/treeshakeNode'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { NodeEvent } from '../NodeEvents'; import { EMPTY_PATH, ObjectPath, @@ -18,10 +19,9 @@ import { SHARED_RECURSION_TRACKER, UNKNOWN_PATH } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; -import { ExpressionEntity } from './shared/Expression'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { MultiExpression } from './shared/MultiExpression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; @@ -36,25 +36,14 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable // We collect deoptimization information if usedBranch !== null private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; private isBranchResolutionAnalysed = false; - private unusedBranch: ExpressionNode | null = null; private usedBranch: ExpressionNode | null = null; - private wasPathDeoptimizedWhileOptimized = false; - - bind() { - super.bind(); - // ensure the usedBranch is set for the tree-shaking passes - this.getUsedBranch(); - } deoptimizeCache() { if (this.usedBranch !== null) { + const unusedBranch = this.usedBranch === this.left ? this.right : this.left; this.usedBranch = null; - const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized; - this.expressionsToBeDeoptimized = []; - if (this.wasPathDeoptimizedWhileOptimized) { - this.unusedBranch!.deoptimizePath(UNKNOWN_PATH); - } - for (const expression of expressionsToBeDeoptimized) { + unusedBranch.deoptimizePath(UNKNOWN_PATH); + for (const expression of this.expressionsToBeDeoptimized) { expression.deoptimizeCache(); } } @@ -66,11 +55,20 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable this.left.deoptimizePath(path); this.right.deoptimizePath(path); } else { - this.wasPathDeoptimizedWhileOptimized = true; usedBranch.deoptimizePath(path); } } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.left.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + this.right.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -84,49 +82,55 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { const usedBranch = this.getUsedBranch(); if (usedBranch === null) return new MultiExpression([ - this.left.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin), - this.right.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin) + this.left.getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin), + this.right.getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin) ]); this.expressionsToBeDeoptimized.push(origin); - return usedBranch.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); + return usedBranch.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); } hasEffects(context: HasEffectsContext): boolean { if (this.left.hasEffects(context)) { return true; } - if (this.usedBranch !== this.left) { + if (this.getUsedBranch() !== this.left) { return this.right.hasEffects(context); } return false; } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return false; - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { return ( this.left.hasEffectsWhenAccessedAtPath(path, context) || this.right.hasEffectsWhenAccessedAtPath(path, context) ); } - return this.usedBranch.hasEffectsWhenAccessedAtPath(path, context); + return usedBranch.hasEffectsWhenAccessedAtPath(path, context); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return true; - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { return ( this.left.hasEffectsWhenAssignedAtPath(path, context) || this.right.hasEffectsWhenAssignedAtPath(path, context) ); } - return this.usedBranch.hasEffectsWhenAssignedAtPath(path, context); + return usedBranch.hasEffectsWhenAssignedAtPath(path, context); } hasEffectsWhenCalledAtPath( @@ -134,26 +138,28 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable callOptions: CallOptions, context: HasEffectsContext ): boolean { - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { return ( this.left.hasEffectsWhenCalledAtPath(path, callOptions, context) || this.right.hasEffectsWhenCalledAtPath(path, callOptions, context) ); } - return this.usedBranch.hasEffectsWhenCalledAtPath(path, callOptions, context); + return usedBranch.hasEffectsWhenCalledAtPath(path, callOptions, context); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { this.included = true; + const usedBranch = this.getUsedBranch(); if ( includeChildrenRecursively || - (this.usedBranch === this.right && this.left.shouldBeIncluded(context)) || - this.usedBranch === null + (usedBranch === this.right && this.left.shouldBeIncluded(context)) || + usedBranch === null ) { this.left.include(context, includeChildrenRecursively); this.right.include(context, includeChildrenRecursively); } else { - this.usedBranch.include(context, includeChildrenRecursively); + usedBranch.include(context, includeChildrenRecursively); } } @@ -178,7 +184,7 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable code.remove(operatorPos, this.end); } removeAnnotations(this, code); - this.usedBranch!.render(code, options, { + this.getUsedBranch()!.render(code, options, { isCalleeOfRenderedParent: renderedParentType ? isCalleeOfRenderedParent : (this.parent as CallExpression).callee === this, @@ -198,17 +204,12 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable if (leftValue === UnknownValue) { return null; } else { - if ( + this.usedBranch = (this.operator === '||' && leftValue) || (this.operator === '&&' && !leftValue) || (this.operator === '??' && leftValue != null) - ) { - this.usedBranch = this.left; - this.unusedBranch = this.right; - } else { - this.usedBranch = this.right; - this.unusedBranch = this.left; - } + ? this.left + : this.right; } } return this.usedBranch; diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index 240c487ad7b..c5401379781 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -6,6 +6,7 @@ import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { EVENT_ACCESSED, EVENT_ASSIGNED, NodeEvent } from '../NodeEvents'; import { EMPTY_PATH, ObjectPath, @@ -15,14 +16,15 @@ import { UnknownKey, UNKNOWN_PATH } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; import ExternalVariable from '../variables/ExternalVariable'; import NamespaceVariable from '../variables/NamespaceVariable'; import Variable from '../variables/Variable'; +import AssignmentExpression from './AssignmentExpression'; import Identifier from './Identifier'; import Literal from './Literal'; import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from './shared/Expression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; import SpreadElement from './SpreadElement'; import Super from './Super'; @@ -78,14 +80,12 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE propertyKey!: ObjectPathKey | null; type!: NodeType.tMemberExpression; variable: Variable | null = null; - + protected deoptimized = false; private bound = false; private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; private replacement: string | null = null; - private wasPathDeoptimizedWhileOptimized = false; bind() { - if (this.bound) return; this.bound = true; const path = getPathIfNotComputed(this); const baseVariable = path && this.scope.findVariable(path[0].key); @@ -101,8 +101,6 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE } } else { super.bind(); - // ensure the propertyKey is set for the tree-shaking passes - this.getPropertyKey(); } } @@ -110,34 +108,50 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized; this.expressionsToBeDeoptimized = []; this.propertyKey = UnknownKey; - if (this.wasPathDeoptimizedWhileOptimized) { - this.object.deoptimizePath(UNKNOWN_PATH); - } + this.object.deoptimizePath(UNKNOWN_PATH); for (const expression of expressionsToBeDeoptimized) { expression.deoptimizeCache(); } } deoptimizePath(path: ObjectPath) { - if (!this.bound) this.bind(); if (path.length === 0) this.disallowNamespaceReassignment(); if (this.variable) { this.variable.deoptimizePath(path); } else if (!this.replacement) { - this.wasPathDeoptimizedWhileOptimized = true; this.object.deoptimizePath([this.getPropertyKey(), ...path]); } } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ): void { + if (this.variable) { + this.variable.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } else if (!this.replacement) { + this.object.deoptimizeThisOnEventAtPath( + event, + [this.getPropertyKey(), ...path], + thisParameter, + recursionTracker + ); + } + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, origin: DeoptimizableEntity ): LiteralValueOrUnknown { - if (!this.bound) this.bind(); if (this.variable !== null) { return this.variable.getLiteralValueAtPath(path, recursionTracker, origin); } + if (this.replacement) { + return UnknownValue; + } this.expressionsToBeDeoptimized.push(origin); return this.object.getLiteralValueAtPath( [this.getPropertyKey(), ...path], @@ -148,46 +162,67 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity - ) { - if (!this.bound) this.bind(); + ): ExpressionEntity { if (this.variable !== null) { - return this.variable.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); + return this.variable.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); + } + if (this.replacement) { + return UNKNOWN_EXPRESSION; } this.expressionsToBeDeoptimized.push(origin); return this.object.getReturnExpressionWhenCalledAtPath( [this.getPropertyKey(), ...path], + callOptions, recursionTracker, origin ); } hasEffects(context: HasEffectsContext): boolean { - const propertyReadSideEffects = (this.context.options.treeshake as NormalizedTreeshakingOptions) - .propertyReadSideEffects; + if (!this.deoptimized) this.applyDeoptimizations(); + const { propertyReadSideEffects } = this.context.options + .treeshake as NormalizedTreeshakingOptions; return ( - propertyReadSideEffects === 'always' || this.property.hasEffects(context) || this.object.hasEffects(context) || - (propertyReadSideEffects && - this.object.hasEffectsWhenAccessedAtPath([this.propertyKey!], context)) + // Assignments do not access the property before assigning + (!( + this.variable || + this.replacement || + (this.parent instanceof AssignmentExpression && this.parent.operator === '=') + ) && + propertyReadSideEffects && + (propertyReadSideEffects === 'always' || + this.object.hasEffectsWhenAccessedAtPath([this.getPropertyKey()], context))) ); } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return false; if (this.variable !== null) { return this.variable.hasEffectsWhenAccessedAtPath(path, context); } - return this.object.hasEffectsWhenAccessedAtPath([this.propertyKey!, ...path], context); + if (this.replacement) { + return true; + } + return this.object.hasEffectsWhenAccessedAtPath([this.getPropertyKey(), ...path], context); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { if (this.variable !== null) { return this.variable.hasEffectsWhenAssignedAtPath(path, context); } - return this.object.hasEffectsWhenAssignedAtPath([this.propertyKey!, ...path], context); + if (this.replacement) { + return true; + } + return this.object.hasEffectsWhenAssignedAtPath([this.getPropertyKey(), ...path], context); } hasEffectsWhenCalledAtPath( @@ -198,14 +233,18 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE if (this.variable !== null) { return this.variable.hasEffectsWhenCalledAtPath(path, callOptions, context); } + if (this.replacement) { + return true; + } return this.object.hasEffectsWhenCalledAtPath( - [this.propertyKey!, ...path], + [this.getPropertyKey(), ...path], callOptions, context ); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); if (!this.included) { this.included = true; if (this.variable !== null) { @@ -228,16 +267,6 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE this.propertyKey = getResolvablePropertyKey(this); } - mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker) { - if (this.variable) { - return this.variable.mayModifyThisWhenCalledAtPath(path, recursionTracker); - } - return this.object.mayModifyThisWhenCalledAtPath( - [this.propertyKey as ObjectPathKey].concat(path), - recursionTracker - ); - } - render( code: MagicString, options: RenderOptions, @@ -270,6 +299,36 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE } } + protected applyDeoptimizations() { + this.deoptimized = true; + const { propertyReadSideEffects } = this.context.options + .treeshake as NormalizedTreeshakingOptions; + if ( + // Namespaces are not bound and should not be deoptimized + this.bound && + propertyReadSideEffects && + !(this.variable || this.replacement) + ) { + // Regular Assignments do not access the property before assigning + if (!(this.parent instanceof AssignmentExpression && this.parent.operator === '=')) { + this.object.deoptimizeThisOnEventAtPath( + EVENT_ACCESSED, + [this.propertyKey!], + this.object, + SHARED_RECURSION_TRACKER + ); + } + if (this.parent instanceof AssignmentExpression) { + this.object.deoptimizeThisOnEventAtPath( + EVENT_ASSIGNED, + [this.propertyKey!], + this.object, + SHARED_RECURSION_TRACKER + ); + } + } + } + private disallowNamespaceReassignment() { if (this.object instanceof Identifier) { const variable = this.scope.findVariable(this.object.name); diff --git a/src/ast/nodes/MethodDefinition.ts b/src/ast/nodes/MethodDefinition.ts index 7ce5195cc2a..07b0e88366a 100644 --- a/src/ast/nodes/MethodDefinition.ts +++ b/src/ast/nodes/MethodDefinition.ts @@ -1,30 +1,13 @@ -import { CallOptions } from '../CallOptions'; -import { HasEffectsContext } from '../ExecutionContext'; -import { EMPTY_PATH, ObjectPath } from '../utils/PathTracker'; import FunctionExpression from './FunctionExpression'; import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; -import { ExpressionNode, NodeBase } from './shared/Node'; +import MethodBase from './shared/MethodBase'; +import { ExpressionNode } from './shared/Node'; -export default class MethodDefinition extends NodeBase { - computed!: boolean; +export default class MethodDefinition extends MethodBase { key!: ExpressionNode | PrivateIdentifier; kind!: 'constructor' | 'method' | 'get' | 'set'; static!: boolean; type!: NodeType.tMethodDefinition; value!: FunctionExpression; - - hasEffects(context: HasEffectsContext) { - return this.key.hasEffects(context); - } - - hasEffectsWhenCalledAtPath( - path: ObjectPath, - callOptions: CallOptions, - context: HasEffectsContext - ) { - return ( - path.length > 0 || this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, callOptions, context) - ); - } } diff --git a/src/ast/nodes/NewExpression.ts b/src/ast/nodes/NewExpression.ts index 79633447566..acff4ce3630 100644 --- a/src/ast/nodes/NewExpression.ts +++ b/src/ast/nodes/NewExpression.ts @@ -9,18 +9,11 @@ export default class NewExpression extends NodeBase { arguments!: ExpressionNode[]; callee!: ExpressionNode; type!: NodeType.tNewExpression; - + protected deoptimized = false; private callOptions!: CallOptions; - bind() { - super.bind(); - for (const argument of this.arguments) { - // This will make sure all properties of parameters behave as "unknown" - argument.deoptimizePath(UNKNOWN_PATH); - } - } - hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); for (const argument of this.arguments) { if (argument.hasEffects(context)) return true; } @@ -36,13 +29,22 @@ export default class NewExpression extends NodeBase { } hasEffectsWhenAccessedAtPath(path: ObjectPath) { - return path.length > 1; + return path.length > 0; } initialise() { this.callOptions = { args: this.arguments, + thisParam: null, withNew: true }; } + + protected applyDeoptimizations(): void { + this.deoptimized = true; + for (const argument of this.arguments) { + // This will make sure all properties of parameters behave as "unknown" + argument.deoptimizePath(UNKNOWN_PATH); + } + } } diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index d4190a3d4d0..561ef146cf8 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -1,96 +1,57 @@ import MagicString from 'magic-string'; import { BLANK } from '../../utils/blank'; -import { getOrCreate } from '../../utils/getOrCreate'; import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; +import { NodeEvent } from '../NodeEvents'; import { EMPTY_PATH, ObjectPath, PathTracker, SHARED_RECURSION_TRACKER, - UNKNOWN_PATH + UnknownKey } from '../utils/PathTracker'; -import { - getMemberReturnExpressionWhenCalled, - hasMemberEffectWhenCalled, - LiteralValueOrUnknown, - objectMembers, - UnknownValue, - UNKNOWN_EXPRESSION -} from '../values'; import Identifier from './Identifier'; import Literal from './Literal'; import * as NodeType from './NodeType'; import Property from './Property'; -import { ExpressionEntity } from './shared/Expression'; +import { + ExpressionEntity, + LiteralValueOrUnknown, + UnknownValue, + UNKNOWN_EXPRESSION +} from './shared/Expression'; import { NodeBase } from './shared/Node'; +import { ObjectEntity, ObjectProperty } from './shared/ObjectEntity'; +import { OBJECT_PROTOTYPE } from './shared/ObjectPrototype'; import SpreadElement from './SpreadElement'; -interface PropertyMap { - [key: string]: { - exactMatchRead: Property | null; - exactMatchWrite: Property | null; - propertiesRead: (Property | SpreadElement)[]; - propertiesWrite: Property[]; - }; -} - export default class ObjectExpression extends NodeBase implements DeoptimizableEntity { properties!: (Property | SpreadElement)[]; type!: NodeType.tObjectExpression; + private objectEntity: ObjectEntity | null = null; - private deoptimizedPaths = new Set(); - - // We collect deoptimization information if we can resolve a computed property access - private expressionsToBeDeoptimized = new Map(); - private hasUnknownDeoptimizedProperty = false; - private propertyMap: PropertyMap | null = null; - private unmatchablePropertiesRead: (Property | SpreadElement)[] = []; - private unmatchablePropertiesWrite: Property[] = []; - - bind() { - super.bind(); - // ensure the propertyMap is set for the tree-shaking passes - this.getPropertyMap(); - } - - // We could also track this per-property but this would quickly become much more complex deoptimizeCache() { - if (!this.hasUnknownDeoptimizedProperty) this.deoptimizeAllProperties(); + this.getObjectEntity().deoptimizeAllProperties(); } deoptimizePath(path: ObjectPath) { - if (this.hasUnknownDeoptimizedProperty) return; - const propertyMap = this.getPropertyMap(); - const key = path[0]; - if (path.length === 1) { - if (typeof key !== 'string') { - this.deoptimizeAllProperties(); - return; - } - if (!this.deoptimizedPaths.has(key)) { - this.deoptimizedPaths.add(key); + this.getObjectEntity().deoptimizePath(path); + } - // we only deoptimizeCache exact matches as in all other cases, - // we do not return a literal value or return expression - const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized.get(key); - if (expressionsToBeDeoptimized) { - for (const expression of expressionsToBeDeoptimized) { - expression.deoptimizeCache(); - } - } - } - } - const subPath = path.length === 1 ? UNKNOWN_PATH : path.slice(1); - for (const property of typeof key === 'string' - ? propertyMap[key] - ? propertyMap[key].propertiesRead - : [] - : this.properties) { - property.deoptimizePath(subPath); - } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.getObjectEntity().deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); } getLiteralValueAtPath( @@ -98,135 +59,29 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE recursionTracker: PathTracker, origin: DeoptimizableEntity ): LiteralValueOrUnknown { - const propertyMap = this.getPropertyMap(); - const key = path[0]; - - if ( - path.length === 0 || - this.hasUnknownDeoptimizedProperty || - typeof key !== 'string' || - this.deoptimizedPaths.has(key) - ) { - return UnknownValue; - } - - if ( - path.length === 1 && - !propertyMap[key] && - !objectMembers[key] && - this.unmatchablePropertiesRead.length === 0 - ) { - getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); - return undefined; - } - - if ( - !propertyMap[key] || - propertyMap[key].exactMatchRead === null || - propertyMap[key].propertiesRead.length > 1 - ) { - return UnknownValue; - } - - getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); - return propertyMap[key].exactMatchRead!.getLiteralValueAtPath( - path.slice(1), - recursionTracker, - origin - ); + return this.getObjectEntity().getLiteralValueAtPath(path, recursionTracker, origin); } getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { - const propertyMap = this.getPropertyMap(); - const key = path[0]; - - if ( - path.length === 0 || - this.hasUnknownDeoptimizedProperty || - typeof key !== 'string' || - this.deoptimizedPaths.has(key) - ) { - return UNKNOWN_EXPRESSION; - } - - if ( - path.length === 1 && - objectMembers[key] && - this.unmatchablePropertiesRead.length === 0 && - (!propertyMap[key] || propertyMap[key].exactMatchRead === null) - ) { - return getMemberReturnExpressionWhenCalled(objectMembers, key); - } - - if ( - !propertyMap[key] || - propertyMap[key].exactMatchRead === null || - propertyMap[key].propertiesRead.length > 1 - ) { - return UNKNOWN_EXPRESSION; - } - - getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); - return propertyMap[key].exactMatchRead!.getReturnExpressionWhenCalledAtPath( - path.slice(1), + return this.getObjectEntity().getReturnExpressionWhenCalledAtPath( + path, + callOptions, recursionTracker, origin ); } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { - if (path.length === 0) return false; - const key = path[0]; - const propertyMap = this.propertyMap!; - if ( - path.length > 1 && - (this.hasUnknownDeoptimizedProperty || - typeof key !== 'string' || - this.deoptimizedPaths.has(key) || - !propertyMap[key] || - propertyMap[key].exactMatchRead === null) - ) - return true; - - const subPath = path.slice(1); - for (const property of typeof key !== 'string' - ? this.properties - : propertyMap[key] - ? propertyMap[key].propertiesRead - : []) { - if (property.hasEffectsWhenAccessedAtPath(subPath, context)) return true; - } - return false; + return this.getObjectEntity().hasEffectsWhenAccessedAtPath(path, context); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext) { - const key = path[0]; - const propertyMap = this.propertyMap!; - if ( - path.length > 1 && - (this.hasUnknownDeoptimizedProperty || - this.deoptimizedPaths.has(key as string) || - !propertyMap[key as string] || - propertyMap[key as string].exactMatchRead === null) - ) { - return true; - } - - const subPath = path.slice(1); - for (const property of typeof key !== 'string' - ? this.properties - : path.length > 1 - ? propertyMap[key].propertiesRead - : propertyMap[key] - ? propertyMap[key].propertiesWrite - : []) { - if (property.hasEffectsWhenAssignedAtPath(subPath, context)) return true; - } - return false; + return this.getObjectEntity().hasEffectsWhenAssignedAtPath(path, context); } hasEffectsWhenCalledAtPath( @@ -234,36 +89,7 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE callOptions: CallOptions, context: HasEffectsContext ): boolean { - const key = path[0]; - if ( - typeof key !== 'string' || - this.hasUnknownDeoptimizedProperty || - this.deoptimizedPaths.has(key) || - (this.propertyMap![key] - ? !this.propertyMap![key].exactMatchRead - : path.length > 1 || !objectMembers[key]) - ) { - return true; - } - const subPath = path.slice(1); - if (this.propertyMap![key]) { - for (const property of this.propertyMap![key].propertiesRead) { - if (property.hasEffectsWhenCalledAtPath(subPath, callOptions, context)) return true; - } - } - if (path.length === 1 && objectMembers[key]) - return hasMemberEffectWhenCalled(objectMembers, key, this.included, callOptions, context); - return false; - } - - mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker) { - if (!path.length || typeof path[0] !== 'string') { - return true; - } - const property = this.getPropertyMap()[path[0]]?.exactMatchRead; - return property - ? property.value.mayModifyThisWhenCalledAtPath(path.slice(1), recursionTracker) - : true; + return this.getObjectEntity().hasEffectsWhenCalledAtPath(path, callOptions, context); } render( @@ -282,73 +108,41 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE } } - private deoptimizeAllProperties() { - this.hasUnknownDeoptimizedProperty = true; - for (const property of this.properties) { - property.deoptimizePath(UNKNOWN_PATH); - } - for (const expressionsToBeDeoptimized of this.expressionsToBeDeoptimized.values()) { - for (const expression of expressionsToBeDeoptimized) { - expression.deoptimizeCache(); - } - } - } - - private getPropertyMap(): PropertyMap { - if (this.propertyMap !== null) { - return this.propertyMap; + private getObjectEntity(): ObjectEntity { + if (this.objectEntity !== null) { + return this.objectEntity; } - const propertyMap = (this.propertyMap = Object.create(null)); - for (let index = this.properties.length - 1; index >= 0; index--) { - const property = this.properties[index]; + const properties: ObjectProperty[] = []; + for (const property of this.properties) { if (property instanceof SpreadElement) { - this.unmatchablePropertiesRead.push(property); + properties.push({ kind: 'init', key: UnknownKey, property }); continue; } - const isWrite = property.kind !== 'get'; - const isRead = property.kind !== 'set'; let key: string; - let unmatchable = false; if (property.computed) { const keyValue = property.key.getLiteralValueAtPath( EMPTY_PATH, SHARED_RECURSION_TRACKER, this ); - if (keyValue === UnknownValue) unmatchable = true; - key = String(keyValue); - } else if (property.key instanceof Identifier) { - key = property.key.name; - } else { - key = String((property.key as Literal).value); - } - if (unmatchable || (key === '__proto__' && !property.computed)) { - if (isRead) { - this.unmatchablePropertiesRead.push(property); + if (keyValue === UnknownValue) { + properties.push({ kind: property.kind, key: UnknownKey, property }); + continue; } else { - this.unmatchablePropertiesWrite.push(property); + key = String(keyValue); + } + } else { + key = + property.key instanceof Identifier + ? property.key.name + : String((property.key as Literal).value); + if (key === '__proto__' && property.kind === 'init') { + properties.unshift({ kind: 'init', key: UnknownKey, property: UNKNOWN_EXPRESSION }); + continue; } - continue; - } - const propertyMapProperty = propertyMap[key]; - if (!propertyMapProperty) { - propertyMap[key] = { - exactMatchRead: isRead ? property : null, - exactMatchWrite: isWrite ? property : null, - propertiesRead: isRead ? [property, ...this.unmatchablePropertiesRead] : [], - propertiesWrite: isWrite && !isRead ? [property, ...this.unmatchablePropertiesWrite] : [] - }; - continue; - } - if (isRead && propertyMapProperty.exactMatchRead === null) { - propertyMapProperty.exactMatchRead = property; - propertyMapProperty.propertiesRead.push(property, ...this.unmatchablePropertiesRead); - } - if (isWrite && !isRead && propertyMapProperty.exactMatchWrite === null) { - propertyMapProperty.exactMatchWrite = property; - propertyMapProperty.propertiesWrite.push(property, ...this.unmatchablePropertiesWrite); } + properties.push({ kind: property.kind, key, property }); } - return propertyMap; + return (this.objectEntity = new ObjectEntity(properties, OBJECT_PROTOTYPE)); } } diff --git a/src/ast/nodes/Property.ts b/src/ast/nodes/Property.ts index 1cfb12233e1..c8de9bd935d 100644 --- a/src/ast/nodes/Property.ts +++ b/src/ast/nodes/Property.ts @@ -1,147 +1,37 @@ import MagicString from 'magic-string'; import { NormalizedTreeshakingOptions } from '../../rollup/types'; import { RenderOptions } from '../../utils/renderHelpers'; -import { CallOptions, NO_ARGS } from '../CallOptions'; -import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; -import { - EMPTY_PATH, - ObjectPath, - PathTracker, - SHARED_RECURSION_TRACKER, - UnknownKey -} from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION } from '../values'; +import { UnknownKey } from '../utils/PathTracker'; import * as NodeType from './NodeType'; -import { ExpressionEntity } from './shared/Expression'; -import { ExpressionNode, NodeBase } from './shared/Node'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './shared/Expression'; +import MethodBase from './shared/MethodBase'; +import { ExpressionNode } from './shared/Node'; import { PatternNode } from './shared/Pattern'; -export default class Property extends NodeBase implements DeoptimizableEntity, PatternNode { - computed!: boolean; +export default class Property extends MethodBase implements PatternNode { key!: ExpressionNode; kind!: 'init' | 'get' | 'set'; method!: boolean; shorthand!: boolean; type!: NodeType.tProperty; - value!: ExpressionNode | (ExpressionNode & PatternNode); - - private accessorCallOptions!: CallOptions; + protected deoptimized = false; private declarationInit: ExpressionEntity | null = null; - private returnExpression: ExpressionEntity | null = null; - - bind() { - super.bind(); - if (this.kind === 'get') { - // ensure the returnExpression is set for the tree-shaking passes - this.getReturnExpression(); - } - if (this.declarationInit !== null) { - this.declarationInit.deoptimizePath([UnknownKey, UnknownKey]); - } - } declare(kind: string, init: ExpressionEntity) { this.declarationInit = init; return (this.value as PatternNode).declare(kind, UNKNOWN_EXPRESSION); } - // As getter properties directly receive their values from function expressions that always - // have a fixed return value, there is no known situation where a getter is deoptimized. - deoptimizeCache(): void {} - - deoptimizePath(path: ObjectPath) { - if (this.kind === 'get') { - this.getReturnExpression().deoptimizePath(path); - } else { - this.value.deoptimizePath(path); - } - } - - getLiteralValueAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ): LiteralValueOrUnknown { - if (this.kind === 'get') { - return this.getReturnExpression().getLiteralValueAtPath(path, recursionTracker, origin); - } - return this.value.getLiteralValueAtPath(path, recursionTracker, origin); - } - - getReturnExpressionWhenCalledAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ): ExpressionEntity { - if (this.kind === 'get') { - return this.getReturnExpression().getReturnExpressionWhenCalledAtPath( - path, - recursionTracker, - origin - ); - } - return this.value.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); - } - hasEffects(context: HasEffectsContext): boolean { - const propertyReadSideEffects = (this.context.options.treeshake as NormalizedTreeshakingOptions).propertyReadSideEffects; - return this.parent.type === 'ObjectPattern' && propertyReadSideEffects === 'always' || + if (!this.deoptimized) this.applyDeoptimizations(); + const propertyReadSideEffects = (this.context.options.treeshake as NormalizedTreeshakingOptions) + .propertyReadSideEffects; + return ( + (this.parent.type === 'ObjectPattern' && propertyReadSideEffects === 'always') || this.key.hasEffects(context) || - this.value.hasEffects(context); - } - - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (this.kind === 'get') { - const trackedExpressions = context.accessed.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return ( - this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context) || - (path.length > 0 && this.returnExpression!.hasEffectsWhenAccessedAtPath(path, context)) - ); - } - return this.value.hasEffectsWhenAccessedAtPath(path, context); - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (this.kind === 'get') { - const trackedExpressions = context.assigned.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return this.returnExpression!.hasEffectsWhenAssignedAtPath(path, context); - } - if (this.kind === 'set') { - const trackedExpressions = context.assigned.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context); - } - return this.value.hasEffectsWhenAssignedAtPath(path, context); - } - - hasEffectsWhenCalledAtPath( - path: ObjectPath, - callOptions: CallOptions, - context: HasEffectsContext - ) { - if (this.kind === 'get') { - const trackedExpressions = (callOptions.withNew - ? context.instantiated - : context.called - ).getEntities(path, callOptions); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return this.returnExpression!.hasEffectsWhenCalledAtPath(path, callOptions, context); - } - return this.value.hasEffectsWhenCalledAtPath(path, callOptions, context); - } - - initialise() { - this.accessorCallOptions = { - args: NO_ARGS, - withNew: false - }; + this.value.hasEffects(context) + ); } render(code: MagicString, options: RenderOptions) { @@ -151,15 +41,10 @@ export default class Property extends NodeBase implements DeoptimizableEntity, P this.value.render(code, options, { isShorthandProperty: this.shorthand }); } - private getReturnExpression(): ExpressionEntity { - if (this.returnExpression === null) { - this.returnExpression = UNKNOWN_EXPRESSION; - return (this.returnExpression = this.value.getReturnExpressionWhenCalledAtPath( - EMPTY_PATH, - SHARED_RECURSION_TRACKER, - this - )); + protected applyDeoptimizations():void { + this.deoptimized = true; + if (this.declarationInit !== null) { + this.declarationInit.deoptimizePath([UnknownKey, UnknownKey]); } - return this.returnExpression; } } diff --git a/src/ast/nodes/PropertyDefinition.ts b/src/ast/nodes/PropertyDefinition.ts index 0b0b02e3643..92dcd0eec02 100644 --- a/src/ast/nodes/PropertyDefinition.ts +++ b/src/ast/nodes/PropertyDefinition.ts @@ -1,6 +1,16 @@ +import { CallOptions } from '../CallOptions'; +import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; +import { NodeEvent } from '../NodeEvents'; +import { ObjectPath, PathTracker } from '../utils/PathTracker'; import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; +import { + ExpressionEntity, + LiteralValueOrUnknown, + UnknownValue, + UNKNOWN_EXPRESSION +} from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; export default class PropertyDefinition extends NodeBase { @@ -10,10 +20,60 @@ export default class PropertyDefinition extends NodeBase { type!: NodeType.tPropertyDefinition; value!: ExpressionNode | null; + deoptimizePath(path: ObjectPath) { + this.value?.deoptimizePath(path); + } + + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.value?.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } + + getLiteralValueAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + return this.value + ? this.value.getLiteralValueAtPath(path, recursionTracker, origin) + : UnknownValue; + } + + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): ExpressionEntity { + return this.value + ? this.value.getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin) + : UNKNOWN_EXPRESSION; + } + hasEffects(context: HasEffectsContext): boolean { return ( this.key.hasEffects(context) || (this.static && this.value !== null && this.value.hasEffects(context)) ); } + + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + return !this.value || this.value.hasEffectsWhenAccessedAtPath(path, context); + } + + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + return !this.value || this.value.hasEffectsWhenAssignedAtPath(path, context); + } + + hasEffectsWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + context: HasEffectsContext + ) { + return !this.value || this.value.hasEffectsWhenCalledAtPath(path, callOptions, context); + } } diff --git a/src/ast/nodes/RestElement.ts b/src/ast/nodes/RestElement.ts index 1ea98754e47..6ce201cdfa2 100644 --- a/src/ast/nodes/RestElement.ts +++ b/src/ast/nodes/RestElement.ts @@ -1,16 +1,16 @@ import { HasEffectsContext } from '../ExecutionContext'; import { EMPTY_PATH, ObjectPath, UnknownKey } from '../utils/PathTracker'; -import { UNKNOWN_EXPRESSION } from '../values'; +import LocalVariable from '../variables/LocalVariable'; import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; -import { ExpressionEntity } from './shared/Expression'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './shared/Expression'; import { NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; export default class RestElement extends NodeBase implements PatternNode { argument!: PatternNode; type!: NodeType.tRestElement; - + protected deoptimized = false; private declarationInit: ExpressionEntity | null = null; addExportedVariables( @@ -20,23 +20,23 @@ export default class RestElement extends NodeBase implements PatternNode { this.argument.addExportedVariables(variables, exportNamesByVariable); } - bind() { - super.bind(); - if (this.declarationInit !== null) { - this.declarationInit.deoptimizePath([UnknownKey, UnknownKey]); - } - } - - declare(kind: string, init: ExpressionEntity) { + declare(kind: string, init: ExpressionEntity): LocalVariable[] { this.declarationInit = init; return this.argument.declare(kind, UNKNOWN_EXPRESSION); } - deoptimizePath(path: ObjectPath) { + deoptimizePath(path: ObjectPath): void { path.length === 0 && this.argument.deoptimizePath(EMPTY_PATH); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { return path.length > 0 || this.argument.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context); } + + protected applyDeoptimizations(): void { + this.deoptimized = true; + if (this.declarationInit !== null) { + this.declarationInit.deoptimizePath([UnknownKey, UnknownKey]); + } + } } diff --git a/src/ast/nodes/ReturnStatement.ts b/src/ast/nodes/ReturnStatement.ts index 87bec1a78c2..37b42812bd8 100644 --- a/src/ast/nodes/ReturnStatement.ts +++ b/src/ast/nodes/ReturnStatement.ts @@ -5,8 +5,8 @@ import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { UNKNOWN_EXPRESSION } from '../values'; import * as NodeType from './NodeType'; +import { UNKNOWN_EXPRESSION } from './shared/Expression'; import { ExpressionNode, IncludeChildren, StatementBase } from './shared/Node'; export default class ReturnStatement extends StatementBase { diff --git a/src/ast/nodes/SequenceExpression.ts b/src/ast/nodes/SequenceExpression.ts index 13930c336f7..cc3b234ac76 100644 --- a/src/ast/nodes/SequenceExpression.ts +++ b/src/ast/nodes/SequenceExpression.ts @@ -10,10 +10,11 @@ import { treeshakeNode } from '../../utils/treeshakeNode'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { EVENT_CALLED, NodeEvent } from '../NodeEvents'; import { ObjectPath, PathTracker } from '../utils/PathTracker'; -import { LiteralValueOrUnknown } from '../values'; import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; +import { ExpressionEntity, LiteralValueOrUnknown } from './shared/Expression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; export default class SequenceExpression extends NodeBase { @@ -24,6 +25,17 @@ export default class SequenceExpression extends NodeBase { if (path.length > 0) this.expressions[this.expressions.length - 1].deoptimizePath(path); } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + if (event === EVENT_CALLED || path.length > 0) { + this.expressions[this.expressions.length - 1].deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, diff --git a/src/ast/nodes/SpreadElement.ts b/src/ast/nodes/SpreadElement.ts index cc0807079c4..a8246dddbdc 100644 --- a/src/ast/nodes/SpreadElement.ts +++ b/src/ast/nodes/SpreadElement.ts @@ -1,13 +1,22 @@ -import { UnknownKey } from '../utils/PathTracker'; +import { NodeEvent } from '../NodeEvents'; +import { ObjectPath, PathTracker, UnknownKey } from '../utils/PathTracker'; import * as NodeType from './NodeType'; +import { ExpressionEntity } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; export default class SpreadElement extends NodeBase { argument!: ExpressionNode; type!: NodeType.tSpreadElement; + protected deoptimized = false; - bind() { - super.bind(); + deoptimizeThisOnEventAtPath(event: NodeEvent, path: ObjectPath, thisParameter: ExpressionEntity, recursionTracker: PathTracker) { + if (path.length > 0) { + this.argument.deoptimizeThisOnEventAtPath(event, [UnknownKey, ...path], thisParameter, recursionTracker) + } + } + + protected applyDeoptimizations(): void { + this.deoptimized = true; // Only properties of properties of the argument could become subject to reassignment // This will also reassign the return values of iterators this.argument.deoptimizePath([UnknownKey, UnknownKey]); diff --git a/src/ast/nodes/Super.ts b/src/ast/nodes/Super.ts index d09554da7bc..8a685e6b500 100644 --- a/src/ast/nodes/Super.ts +++ b/src/ast/nodes/Super.ts @@ -1,6 +1,25 @@ +import { ObjectPath } from '../utils/PathTracker'; +import ThisVariable from '../variables/ThisVariable'; import * as NodeType from './NodeType'; import { NodeBase } from './shared/Node'; export default class Super extends NodeBase { type!: NodeType.tSuper; + + variable!: ThisVariable; + + bind() { + this.variable = this.scope.findVariable('this') as ThisVariable; + } + + deoptimizePath(path: ObjectPath) { + this.variable.deoptimizePath(path); + } + + include() { + if (!this.included) { + this.included = true; + this.context.includeVariableInModule(this.variable); + } + } } diff --git a/src/ast/nodes/TaggedTemplateExpression.ts b/src/ast/nodes/TaggedTemplateExpression.ts index 84e9dffc3fe..90a18c81ee2 100644 --- a/src/ast/nodes/TaggedTemplateExpression.ts +++ b/src/ast/nodes/TaggedTemplateExpression.ts @@ -52,6 +52,7 @@ export default class TaggedTemplateExpression extends NodeBase { initialise() { this.callOptions = { args: NO_ARGS, + thisParam: null, withNew: false, }; } diff --git a/src/ast/nodes/TemplateElement.ts b/src/ast/nodes/TemplateElement.ts index e5cc466fc3a..2dbe9317fe3 100644 --- a/src/ast/nodes/TemplateElement.ts +++ b/src/ast/nodes/TemplateElement.ts @@ -9,6 +9,7 @@ export default class TemplateElement extends NodeBase { raw: string; }; + // Do not try to bind value bind() {} hasEffects() { diff --git a/src/ast/nodes/TemplateLiteral.ts b/src/ast/nodes/TemplateLiteral.ts index fbc02c708ee..06314a3bc00 100644 --- a/src/ast/nodes/TemplateLiteral.ts +++ b/src/ast/nodes/TemplateLiteral.ts @@ -1,8 +1,8 @@ import MagicString from 'magic-string'; import { RenderOptions } from '../../utils/renderHelpers'; import { ObjectPath } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; import * as NodeType from './NodeType'; +import { LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; import TemplateElement from './TemplateElement'; diff --git a/src/ast/nodes/ThisExpression.ts b/src/ast/nodes/ThisExpression.ts index f68631adae6..b1f2ada38aa 100644 --- a/src/ast/nodes/ThisExpression.ts +++ b/src/ast/nodes/ThisExpression.ts @@ -1,21 +1,40 @@ import MagicString from 'magic-string'; import { HasEffectsContext } from '../ExecutionContext'; +import { NodeEvent } from '../NodeEvents'; import ModuleScope from '../scopes/ModuleScope'; -import { ObjectPath } from '../utils/PathTracker'; -import ThisVariable from '../variables/ThisVariable'; +import { ObjectPath, PathTracker } from '../utils/PathTracker'; +import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; -import FunctionNode from './shared/FunctionNode'; +import { ExpressionEntity } from './shared/Expression'; import { NodeBase } from './shared/Node'; export default class ThisExpression extends NodeBase { type!: NodeType.tThisExpression; - variable!: ThisVariable; + variable!: Variable; private alias!: string | null; bind() { - super.bind(); - this.variable = this.scope.findVariable('this') as ThisVariable; + this.variable = this.scope.findVariable('this'); + } + + deoptimizePath(path: ObjectPath) { + this.variable.deoptimizePath(path); + } + + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.variable.deoptimizeThisOnEventAtPath( + event, + path, + // We rewrite the parameter so that a ThisVariable can detect self-mutations + thisParameter === this ? this.variable : thisParameter, + recursionTracker + ); } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { @@ -26,6 +45,13 @@ export default class ThisExpression extends NodeBase { return this.variable.hasEffectsWhenAssignedAtPath(path, context); } + include() { + if (!this.included) { + this.included = true; + this.context.includeVariableInModule(this.variable); + } + } + initialise() { this.alias = this.scope.findLexicalBoundary() instanceof ModuleScope ? this.context.moduleContext : null; @@ -39,12 +65,6 @@ export default class ThisExpression extends NodeBase { this.start ); } - for (let parent = this.parent; parent instanceof NodeBase; parent = parent.parent) { - if (parent instanceof FunctionNode) { - parent.referencesThis = true; - break; - } - } } render(code: MagicString) { diff --git a/src/ast/nodes/UnaryExpression.ts b/src/ast/nodes/UnaryExpression.ts index b80b70474ea..11087ca408a 100644 --- a/src/ast/nodes/UnaryExpression.ts +++ b/src/ast/nodes/UnaryExpression.ts @@ -1,10 +1,10 @@ import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; import { EMPTY_PATH, ObjectPath, PathTracker } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; import Identifier from './Identifier'; import { LiteralValue } from './Literal'; import * as NodeType from './NodeType'; +import { LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; const unaryOperators: { @@ -24,13 +24,7 @@ export default class UnaryExpression extends NodeBase { operator!: '!' | '+' | '-' | 'delete' | 'typeof' | 'void' | '~'; prefix!: boolean; type!: NodeType.tUnaryExpression; - - bind() { - super.bind(); - if (this.operator === 'delete') { - this.argument.deoptimizePath(EMPTY_PATH); - } - } + protected deoptimized = false; getLiteralValueAtPath( path: ObjectPath, @@ -45,6 +39,7 @@ export default class UnaryExpression extends NodeBase { } hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); if (this.operator === 'typeof' && this.argument instanceof Identifier) return false; return ( this.argument.hasEffects(context) || @@ -59,4 +54,11 @@ export default class UnaryExpression extends NodeBase { } return path.length > 1; } + + protected applyDeoptimizations(): void { + this.deoptimized = true; + if (this.operator === 'delete') { + this.argument.deoptimizePath(EMPTY_PATH); + } + } } diff --git a/src/ast/nodes/UpdateExpression.ts b/src/ast/nodes/UpdateExpression.ts index 2d2a60f621d..b83864238c9 100644 --- a/src/ast/nodes/UpdateExpression.ts +++ b/src/ast/nodes/UpdateExpression.ts @@ -1,6 +1,9 @@ import MagicString from 'magic-string'; import { RenderOptions } from '../../utils/renderHelpers'; -import { getSystemExportFunctionLeft, getSystemExportStatement } from '../../utils/systemJsRendering'; +import { + getSystemExportFunctionLeft, + getSystemExportStatement +} from '../../utils/systemJsRendering'; import { HasEffectsContext } from '../ExecutionContext'; import { EMPTY_PATH, ObjectPath } from '../utils/PathTracker'; import Identifier from './Identifier'; @@ -12,17 +15,10 @@ export default class UpdateExpression extends NodeBase { operator!: '++' | '--'; prefix!: boolean; type!: NodeType.tUpdateExpression; - - bind() { - super.bind(); - this.argument.deoptimizePath(EMPTY_PATH); - if (this.argument instanceof Identifier) { - const variable = this.scope.findVariable(this.argument.name); - variable.isReassigned = true; - } - } + protected deoptimized = false; hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); return ( this.argument.hasEffects(context) || this.argument.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context) @@ -83,4 +79,13 @@ export default class UpdateExpression extends NodeBase { } } } + + protected applyDeoptimizations() { + this.deoptimized = true; + this.argument.deoptimizePath(EMPTY_PATH); + if (this.argument instanceof Identifier) { + const variable = this.scope.findVariable(this.argument.name); + variable.isReassigned = true; + } + } } diff --git a/src/ast/nodes/YieldExpression.ts b/src/ast/nodes/YieldExpression.ts index d322951c8fd..e7142d777f3 100644 --- a/src/ast/nodes/YieldExpression.ts +++ b/src/ast/nodes/YieldExpression.ts @@ -9,15 +9,10 @@ export default class YieldExpression extends NodeBase { argument!: ExpressionNode | null; delegate!: boolean; type!: NodeType.tYieldExpression; - - bind() { - super.bind(); - if (this.argument !== null) { - this.argument.deoptimizePath(UNKNOWN_PATH); - } - } + protected deoptimized = false; hasEffects(context: HasEffectsContext) { + if (!this.deoptimized) this.applyDeoptimizations(); return ( !context.ignore.returnAwaitYield || (this.argument !== null && this.argument.hasEffects(context)) @@ -32,4 +27,9 @@ export default class YieldExpression extends NodeBase { } } } + + protected applyDeoptimizations() { + this.deoptimized = true; + this.argument?.deoptimizePath(UNKNOWN_PATH); + } } diff --git a/src/ast/nodes/shared/ArrayPrototype.ts b/src/ast/nodes/shared/ArrayPrototype.ts new file mode 100644 index 00000000000..d01ca084612 --- /dev/null +++ b/src/ast/nodes/shared/ArrayPrototype.ts @@ -0,0 +1,144 @@ +import { UnknownInteger } from '../../utils/PathTracker'; +import { UNKNOWN_LITERAL_BOOLEAN, UNKNOWN_LITERAL_NUMBER } from '../../values'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './Expression'; +import { + Method, + METHOD_RETURNS_BOOLEAN, + METHOD_RETURNS_NUMBER, + METHOD_RETURNS_STRING, + METHOD_RETURNS_UNKNOWN +} from './MethodTypes'; +import { ObjectEntity, ObjectProperty } from './ObjectEntity'; +import { OBJECT_PROTOTYPE } from './ObjectPrototype'; + +const NEW_ARRAY_PROPERTIES: ObjectProperty[] = [ + { kind: 'init', key: UnknownInteger, property: UNKNOWN_EXPRESSION }, + { kind: 'init', key: 'length', property: UNKNOWN_LITERAL_NUMBER } +]; + +const METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_BOOLEAN: [ExpressionEntity] = [ + new Method({ + callsArgs: [0], + mutatesSelfAsArray: true, + returns: null, + returnsPrimitive: UNKNOWN_LITERAL_BOOLEAN + }) +]; + +const METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_NUMBER: [ExpressionEntity] = [ + new Method({ + callsArgs: [0], + mutatesSelfAsArray: true, + returns: null, + returnsPrimitive: UNKNOWN_LITERAL_NUMBER + }) +]; + +export const METHOD_RETURNS_NEW_ARRAY: [ExpressionEntity] = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: false, + returns: () => new ObjectEntity(NEW_ARRAY_PROPERTIES, ARRAY_PROTOTYPE), + returnsPrimitive: null + }) +]; + +const METHOD_MUTATES_SELF_RETURNS_NEW_ARRAY: [ExpressionEntity] = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: true, + returns: () => new ObjectEntity(NEW_ARRAY_PROPERTIES, ARRAY_PROTOTYPE), + returnsPrimitive: null + }) +]; + +const METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_NEW_ARRAY: [ExpressionEntity] = [ + new Method({ + callsArgs: [0], + mutatesSelfAsArray: true, + returns: () => new ObjectEntity(NEW_ARRAY_PROPERTIES, ARRAY_PROTOTYPE), + returnsPrimitive: null + }) +]; + +const METHOD_MUTATES_SELF_RETURNS_NUMBER: [ExpressionEntity] = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: true, + returns: null, + returnsPrimitive: UNKNOWN_LITERAL_NUMBER + }) +]; + +const METHOD_MUTATES_SELF_RETURNS_UNKNOWN: [ExpressionEntity] = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: true, + returns: null, + returnsPrimitive: UNKNOWN_EXPRESSION + }) +]; + +const METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_UNKNOWN: [ExpressionEntity] = [ + new Method({ + callsArgs: [0], + mutatesSelfAsArray: true, + returns: null, + returnsPrimitive: UNKNOWN_EXPRESSION + }) +]; + +const METHOD_MUTATES_SELF_RETURNS_SELF: [ExpressionEntity] = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: true, + returns: 'self', + returnsPrimitive: null + }) +]; + +const METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_SELF: [ExpressionEntity] = [ + new Method({ + callsArgs: [0], + mutatesSelfAsArray: true, + returns: 'self', + returnsPrimitive: null + }) +]; + +export const ARRAY_PROTOTYPE = new ObjectEntity( + { + // @ts-ignore + __proto__: null, + // We assume that accessors have effects as we do not track the accessed value afterwards + at: METHOD_MUTATES_SELF_RETURNS_UNKNOWN, + concat: METHOD_RETURNS_NEW_ARRAY, + copyWithin: METHOD_MUTATES_SELF_RETURNS_SELF, + entries: METHOD_MUTATES_SELF_RETURNS_NEW_ARRAY, + every: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_BOOLEAN, + fill: METHOD_MUTATES_SELF_RETURNS_SELF, + filter: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_NEW_ARRAY, + find: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_UNKNOWN, + findIndex: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_NUMBER, + forEach: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_UNKNOWN, + includes: METHOD_RETURNS_BOOLEAN, + indexOf: METHOD_RETURNS_NUMBER, + join: METHOD_RETURNS_STRING, + keys: METHOD_RETURNS_UNKNOWN, + lastIndexOf: METHOD_RETURNS_NUMBER, + map: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_NEW_ARRAY, + pop: METHOD_MUTATES_SELF_RETURNS_UNKNOWN, + push: METHOD_MUTATES_SELF_RETURNS_NUMBER, + reduce: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_UNKNOWN, + reduceRight: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_UNKNOWN, + reverse: METHOD_MUTATES_SELF_RETURNS_SELF, + shift: METHOD_MUTATES_SELF_RETURNS_UNKNOWN, + slice: METHOD_MUTATES_SELF_RETURNS_NEW_ARRAY, + some: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_BOOLEAN, + sort: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_SELF, + splice: METHOD_MUTATES_SELF_RETURNS_NEW_ARRAY, + unshift: METHOD_MUTATES_SELF_RETURNS_NUMBER, + values: METHOD_MUTATES_SELF_RETURNS_UNKNOWN + }, + OBJECT_PROTOTYPE +); diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index 4323d8c789f..7a930b94582 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -1,29 +1,82 @@ import { CallOptions } from '../../CallOptions'; +import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; +import { NodeEvent } from '../../NodeEvents'; import ChildScope from '../../scopes/ChildScope'; import Scope from '../../scopes/Scope'; -import { ObjectPath } from '../../utils/PathTracker'; +import { + EMPTY_PATH, + ObjectPath, + PathTracker, + SHARED_RECURSION_TRACKER, + UnknownKey +} from '../../utils/PathTracker'; import ClassBody from '../ClassBody'; import Identifier from '../Identifier'; +import Literal from '../Literal'; +import MethodDefinition from '../MethodDefinition'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue } from './Expression'; import { ExpressionNode, NodeBase } from './Node'; +import { ObjectEntity, ObjectProperty } from './ObjectEntity'; +import { ObjectMember } from './ObjectMember'; +import { OBJECT_PROTOTYPE } from './ObjectPrototype'; -export default class ClassNode extends NodeBase { +export default class ClassNode extends NodeBase implements DeoptimizableEntity { body!: ClassBody; id!: Identifier | null; superClass!: ExpressionNode | null; + private classConstructor!: MethodDefinition | null; + private objectEntity: ObjectEntity | null = null; createScope(parentScope: Scope) { this.scope = new ChildScope(parentScope); } - hasEffectsWhenAccessedAtPath(path: ObjectPath) { - if (path.length <= 1) return false; - return path.length > 2 || path[0] !== 'prototype'; + deoptimizeCache() { + this.getObjectEntity().deoptimizeAllProperties(); } - hasEffectsWhenAssignedAtPath(path: ObjectPath) { - if (path.length <= 1) return false; - return path.length > 2 || path[0] !== 'prototype'; + deoptimizePath(path: ObjectPath) { + this.getObjectEntity().deoptimizePath(path); + } + + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.getObjectEntity().deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + } + + getLiteralValueAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + return this.getObjectEntity().getLiteralValueAtPath(path, recursionTracker, origin); + } + + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): ExpressionEntity { + return this.getObjectEntity().getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin); + } + + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { + return this.getObjectEntity().hasEffectsWhenAccessedAtPath(path, context); + } + + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext) { + return this.getObjectEntity().hasEffectsWhenAssignedAtPath(path, context); } hasEffectsWhenCalledAtPath( @@ -31,17 +84,76 @@ export default class ClassNode extends NodeBase { callOptions: CallOptions, context: HasEffectsContext ) { - if (!callOptions.withNew) return true; - return ( - this.body.hasEffectsWhenCalledAtPath(path, callOptions, context) || - (this.superClass !== null && - this.superClass.hasEffectsWhenCalledAtPath(path, callOptions, context)) - ); + if (path.length === 0) { + return ( + !callOptions.withNew || + (this.classConstructor !== null + ? this.classConstructor.hasEffectsWhenCalledAtPath(EMPTY_PATH, callOptions, context) + : this.superClass !== null && + this.superClass.hasEffectsWhenCalledAtPath(path, callOptions, context)) + ); + } else { + return this.getObjectEntity().hasEffectsWhenCalledAtPath(path, callOptions, context); + } } initialise() { if (this.id !== null) { this.id.declare('class', this); } + for (const method of this.body.body) { + if (method instanceof MethodDefinition && method.kind === 'constructor') { + this.classConstructor = method; + return; + } + } + this.classConstructor = null; + } + + private getObjectEntity(): ObjectEntity { + if (this.objectEntity !== null) { + return this.objectEntity; + } + const staticProperties: ObjectProperty[] = []; + const dynamicMethods: ObjectProperty[] = []; + for (const definition of this.body.body) { + const properties = definition.static ? staticProperties : dynamicMethods; + const definitionKind = (definition as MethodDefinition | { kind: undefined }).kind; + // Note that class fields do not end up on the prototype + if (properties === dynamicMethods && !definitionKind) continue; + const kind = definitionKind === 'set' || definitionKind === 'get' ? definitionKind : 'init'; + let key: string; + if (definition.computed) { + const keyValue = definition.key.getLiteralValueAtPath( + EMPTY_PATH, + SHARED_RECURSION_TRACKER, + this + ); + if (keyValue === UnknownValue) { + properties.push({ kind, key: UnknownKey, property: definition }); + continue; + } else { + key = String(keyValue); + } + } else { + key = + definition.key instanceof Identifier + ? definition.key.name + : String((definition.key as Literal).value); + } + properties.push({ kind, key, property: definition }); + } + staticProperties.unshift({ + key: 'prototype', + kind: 'init', + property: new ObjectEntity( + dynamicMethods, + this.superClass ? new ObjectMember(this.superClass, 'prototype') : OBJECT_PROTOTYPE + ) + }); + return (this.objectEntity = new ObjectEntity( + staticProperties, + this.superClass || OBJECT_PROTOTYPE + )); } } diff --git a/src/ast/nodes/shared/Expression.ts b/src/ast/nodes/shared/Expression.ts index eba5367420b..4a851c2f523 100644 --- a/src/ast/nodes/shared/Expression.ts +++ b/src/ast/nodes/shared/Expression.ts @@ -2,13 +2,29 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { WritableEntity } from '../../Entity'; import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; -import { ObjectPath, PathTracker } from '../../utils/PathTracker'; -import { LiteralValueOrUnknown } from '../../values'; +import { NodeEvent } from '../../NodeEvents'; +import { ObjectPath, PathTracker, UNKNOWN_PATH } from '../../utils/PathTracker'; +import { LiteralValue } from '../Literal'; import SpreadElement from '../SpreadElement'; import { ExpressionNode, IncludeChildren } from './Node'; -export interface ExpressionEntity extends WritableEntity { - included: boolean; +export const UnknownValue = Symbol('Unknown Value'); + +export type LiteralValueOrUnknown = LiteralValue | typeof UnknownValue; + +export class ExpressionEntity implements WritableEntity { + included = false; + + deoptimizePath(_path: ObjectPath): void {} + + deoptimizeThisOnEventAtPath( + _event: NodeEvent, + _path: ObjectPath, + thisParameter: ExpressionEntity, + _recursionTracker: PathTracker + ): void { + thisParameter.deoptimizePath(UNKNOWN_PATH); + } /** * If possible it returns a stringifyable literal value for this node that can be used @@ -16,22 +32,47 @@ export interface ExpressionEntity extends WritableEntity { * Otherwise it should return UnknownValue. */ getLiteralValueAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ): LiteralValueOrUnknown; + _path: ObjectPath, + _recursionTracker: PathTracker, + _origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + return UnknownValue; + } + getReturnExpressionWhenCalledAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ): ExpressionEntity; - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean; + _path: ObjectPath, + _callOptions: CallOptions, + _recursionTracker: PathTracker, + _origin: DeoptimizableEntity + ): ExpressionEntity { + return UNKNOWN_EXPRESSION; + } + + hasEffectsWhenAccessedAtPath(_path: ObjectPath, _context: HasEffectsContext): boolean { + return true; + } + + hasEffectsWhenAssignedAtPath(_path: ObjectPath, _context: HasEffectsContext): boolean { + return true; + } + hasEffectsWhenCalledAtPath( - path: ObjectPath, - callOptions: CallOptions, - context: HasEffectsContext - ): boolean; - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void; - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void; - mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker): boolean; + _path: ObjectPath, + _callOptions: CallOptions, + _context: HasEffectsContext + ): boolean { + return true; + } + + include(_context: InclusionContext, _includeChildrenRecursively: IncludeChildren): void { + this.included = true; + } + + includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { + for (const arg of args) { + arg.include(context, false); + } + } } + +export const UNKNOWN_EXPRESSION: ExpressionEntity = new (class UnknownExpression extends ExpressionEntity {})(); diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index 65e81bed072..3f7cf1f66a5 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -1,13 +1,16 @@ import { CallOptions } from '../../CallOptions'; import { BROKEN_FLOW_NONE, HasEffectsContext, InclusionContext } from '../../ExecutionContext'; +import { EVENT_CALLED, NodeEvent } from '../../NodeEvents'; import FunctionScope from '../../scopes/FunctionScope'; import { ObjectPath, UnknownKey, UNKNOWN_PATH } from '../../utils/PathTracker'; -import { UnknownObjectExpression, UNKNOWN_EXPRESSION } from '../../values'; import BlockStatement from '../BlockStatement'; import Identifier, { IdentifierWithVariable } from '../Identifier'; import RestElement from '../RestElement'; import SpreadElement from '../SpreadElement'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './Expression'; import { ExpressionNode, GenericEsTreeNode, IncludeChildren, NodeBase } from './Node'; +import { ObjectEntity } from './ObjectEntity'; +import { OBJECT_PROTOTYPE } from './ObjectPrototype'; import { PatternNode } from './Pattern'; export default class FunctionNode extends NodeBase { @@ -16,9 +19,7 @@ export default class FunctionNode extends NodeBase { id!: IdentifierWithVariable | null; params!: PatternNode[]; preventChildBlockScope!: true; - referencesThis!: boolean; scope!: FunctionScope; - private isPrototypeDeoptimized = false; createScope(parentScope: FunctionScope) { @@ -39,6 +40,17 @@ export default class FunctionNode extends NodeBase { } } + // TODO for completeness, we should also track other events here + deoptimizeThisOnEventAtPath(event: NodeEvent, path: ObjectPath, thisParameter: ExpressionEntity) { + if (event === EVENT_CALLED) { + if (path.length > 0 ) { + thisParameter.deoptimizePath(UNKNOWN_PATH); + } else { + this.scope.thisVariable.addEntityToBeDeoptimized(thisParameter); + } + } + } + getReturnExpressionWhenCalledAtPath(path: ObjectPath) { return path.length === 0 ? this.scope.getReturnExpression() : UNKNOWN_EXPRESSION; } @@ -71,7 +83,7 @@ export default class FunctionNode extends NodeBase { const thisInit = context.replacedVariableInits.get(this.scope.thisVariable); context.replacedVariableInits.set( this.scope.thisVariable, - callOptions.withNew ? new UnknownObjectExpression() : UNKNOWN_EXPRESSION + callOptions.withNew ? new ObjectEntity({}, OBJECT_PROTOTYPE) : UNKNOWN_EXPRESSION ); const { brokenFlow, ignore } = context; context.ignore = { @@ -121,14 +133,7 @@ export default class FunctionNode extends NodeBase { this.body.addImplicitReturnExpressionToScope(); } - mayModifyThisWhenCalledAtPath( - path: ObjectPath - ) { - return path.length ? true : this.referencesThis - } - parseNode(esTreeNode: GenericEsTreeNode) { - this.referencesThis = false; this.body = new this.context.nodeConstructors.BlockStatement( esTreeNode.body, this, diff --git a/src/ast/nodes/shared/MethodBase.ts b/src/ast/nodes/shared/MethodBase.ts new file mode 100644 index 00000000000..1a425f7cce5 --- /dev/null +++ b/src/ast/nodes/shared/MethodBase.ts @@ -0,0 +1,131 @@ +import { CallOptions, NO_ARGS } from '../../CallOptions'; +import { DeoptimizableEntity } from '../../DeoptimizableEntity'; +import { HasEffectsContext } from '../../ExecutionContext'; +import { EVENT_ACCESSED, EVENT_ASSIGNED, EVENT_CALLED, NodeEvent } from '../../NodeEvents'; +import { + EMPTY_PATH, + ObjectPath, + PathTracker, + SHARED_RECURSION_TRACKER +} from '../../utils/PathTracker'; +import PrivateIdentifier from '../PrivateIdentifier'; +import { ExpressionEntity, LiteralValueOrUnknown, UNKNOWN_EXPRESSION } from './Expression'; +import { ExpressionNode, NodeBase } from './Node'; +import { PatternNode } from './Pattern'; + +export default class MethodBase extends NodeBase implements DeoptimizableEntity { + computed!: boolean; + key!: ExpressionNode | PrivateIdentifier; + kind!: 'constructor' | 'method' | 'init' | 'get' | 'set'; + value!: ExpressionNode | (ExpressionNode & PatternNode); + + private accessedValue: ExpressionEntity | null = null; + private accessorCallOptions: CallOptions = { + args: NO_ARGS, + thisParam: null, + withNew: false + }; + + // As getter properties directly receive their values from fixed function + // expressions, there is no known situation where a getter is deoptimized. + deoptimizeCache(): void {} + + deoptimizePath(path: ObjectPath) { + this.getAccessedValue().deoptimizePath(path); + } + + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + if (event === EVENT_ACCESSED && this.kind === 'get' && path.length === 0) { + return this.value.deoptimizeThisOnEventAtPath( + EVENT_CALLED, + EMPTY_PATH, + thisParameter, + recursionTracker + ); + } + if (event === EVENT_ASSIGNED && this.kind === 'set' && path.length === 0) { + return this.value.deoptimizeThisOnEventAtPath( + EVENT_CALLED, + EMPTY_PATH, + thisParameter, + recursionTracker + ); + } + this.getAccessedValue().deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + } + + getLiteralValueAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + return this.getAccessedValue().getLiteralValueAtPath(path, recursionTracker, origin); + } + + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): ExpressionEntity { + return this.getAccessedValue().getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); + } + + hasEffects(context: HasEffectsContext) { + return this.key.hasEffects(context); + } + + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + if (this.kind === 'get' && path.length === 0) { + return this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context); + } + return this.getAccessedValue().hasEffectsWhenAccessedAtPath(path, context); + } + + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + if (this.kind === 'set') { + return this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context); + } + return this.getAccessedValue().hasEffectsWhenAssignedAtPath(path, context); + } + + hasEffectsWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + context: HasEffectsContext + ) { + return this.getAccessedValue().hasEffectsWhenCalledAtPath(path, callOptions, context); + } + + protected getAccessedValue(): ExpressionEntity { + if (this.accessedValue === null) { + if (this.kind === 'get') { + this.accessedValue = UNKNOWN_EXPRESSION; + return (this.accessedValue = this.value.getReturnExpressionWhenCalledAtPath( + EMPTY_PATH, + this.accessorCallOptions, + SHARED_RECURSION_TRACKER, + this + )); + } else { + return (this.accessedValue = this.value); + } + } + return this.accessedValue; + } +} diff --git a/src/ast/nodes/shared/MethodTypes.ts b/src/ast/nodes/shared/MethodTypes.ts new file mode 100644 index 00000000000..d43f4324ceb --- /dev/null +++ b/src/ast/nodes/shared/MethodTypes.ts @@ -0,0 +1,132 @@ +import { CallOptions, NO_ARGS } from '../../CallOptions'; +import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; +import { EVENT_CALLED, NodeEvent } from '../../NodeEvents'; +import { EMPTY_PATH, ObjectPath, UNKNOWN_INTEGER_PATH } from '../../utils/PathTracker'; +import { UNKNOWN_LITERAL_BOOLEAN, UNKNOWN_LITERAL_NUMBER, UNKNOWN_LITERAL_STRING } from '../../values'; +import SpreadElement from '../SpreadElement'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './Expression'; +import { ExpressionNode } from './Node'; + +type MethodDescription = { + callsArgs: number[] | null; + mutatesSelfAsArray: boolean; +} & ( + | { + returns: 'self' | (() => ExpressionEntity); + returnsPrimitive: null; + } + | { + returns: null; + returnsPrimitive: ExpressionEntity; + } +); + +export class Method extends ExpressionEntity { + constructor(private readonly description: MethodDescription) { + super(); + } + + deoptimizeThisOnEventAtPath(event: NodeEvent, path: ObjectPath, thisParameter: ExpressionEntity) { + if (event === EVENT_CALLED && path.length === 0 && this.description.mutatesSelfAsArray) { + thisParameter.deoptimizePath(UNKNOWN_INTEGER_PATH); + } + } + + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions + ): ExpressionEntity { + if (path.length > 0) { + return UNKNOWN_EXPRESSION; + } + return ( + this.description.returnsPrimitive || + (this.description.returns === 'self' + ? callOptions.thisParam || UNKNOWN_EXPRESSION + : this.description.returns()) + ); + } + + hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean { + return path.length > 1; + } + + hasEffectsWhenAssignedAtPath(path: ObjectPath): boolean { + return path.length > 0; + } + + hasEffectsWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + context: HasEffectsContext + ): boolean { + if ( + path.length > 0 || + (this.description.mutatesSelfAsArray && + callOptions.thisParam?.hasEffectsWhenAssignedAtPath(UNKNOWN_INTEGER_PATH, context)) + ) { + return true; + } + if (!this.description.callsArgs) { + return false; + } + for (const argIndex of this.description.callsArgs) { + if ( + callOptions.args[argIndex]?.hasEffectsWhenCalledAtPath( + EMPTY_PATH, + { + args: NO_ARGS, + thisParam: null, + withNew: false + }, + context + ) + ) { + return true; + } + } + return false; + } + + includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { + for (const arg of args) { + arg.include(context, false); + } + } +} + +export const METHOD_RETURNS_BOOLEAN = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: false, + returns: null, + returnsPrimitive: UNKNOWN_LITERAL_BOOLEAN + }) +]; + +export const METHOD_RETURNS_STRING = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: false, + returns: null, + returnsPrimitive: UNKNOWN_LITERAL_STRING + }) +]; + +export const METHOD_RETURNS_NUMBER = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: false, + returns: null, + returnsPrimitive: UNKNOWN_LITERAL_NUMBER + }) +]; + +export const METHOD_RETURNS_UNKNOWN = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: false, + returns: null, + returnsPrimitive: UNKNOWN_EXPRESSION + }) +]; diff --git a/src/ast/nodes/shared/MultiExpression.ts b/src/ast/nodes/shared/MultiExpression.ts index f657c7e3454..b7d7426b5db 100644 --- a/src/ast/nodes/shared/MultiExpression.ts +++ b/src/ast/nodes/shared/MultiExpression.ts @@ -2,17 +2,14 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; import { ObjectPath, PathTracker } from '../../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../../values'; import { ExpressionEntity } from './Expression'; import { IncludeChildren } from './Node'; -export class MultiExpression implements ExpressionEntity { +export class MultiExpression extends ExpressionEntity { included = false; - private expressions: ExpressionEntity[]; - - constructor(expressions: ExpressionEntity[]) { - this.expressions = expressions; + constructor(private expressions: ExpressionEntity[]) { + super(); } deoptimizePath(path: ObjectPath): void { @@ -21,18 +18,15 @@ export class MultiExpression implements ExpressionEntity { } } - getLiteralValueAtPath(): LiteralValueOrUnknown { - return UnknownValue; - } - getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { return new MultiExpression( this.expressions.map(expression => - expression.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin) + expression.getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin) ) ); } @@ -71,10 +65,4 @@ export class MultiExpression implements ExpressionEntity { } } } - - includeCallArguments(): void {} - - mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker) { - return this.expressions.some(e => e.mayModifyThisWhenCalledAtPath(path, recursionTracker)); - } } diff --git a/src/ast/nodes/shared/Node.ts b/src/ast/nodes/shared/Node.ts index 26fa04daf55..ccb0e0aa873 100644 --- a/src/ast/nodes/shared/Node.ts +++ b/src/ast/nodes/shared/Node.ts @@ -3,8 +3,6 @@ import { locate } from 'locate-character'; import MagicString from 'magic-string'; import { AstContext } from '../../../Module'; import { NodeRenderOptions, RenderOptions } from '../../../utils/renderHelpers'; -import { CallOptions } from '../../CallOptions'; -import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { Entity } from '../../Entity'; import { createHasEffectsContext, @@ -13,11 +11,8 @@ import { } from '../../ExecutionContext'; import { getAndCreateKeys, keys } from '../../keys'; import ChildScope from '../../scopes/ChildScope'; -import { ObjectPath, PathTracker } from '../../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../../values'; import Variable from '../../variables/Variable'; import * as NodeType from '../NodeType'; -import SpreadElement from '../SpreadElement'; import { ExpressionEntity } from './Expression'; export interface GenericEsTreeNode extends acorn.Node { @@ -91,23 +86,28 @@ export interface StatementNode extends Node {} export interface ExpressionNode extends ExpressionEntity, Node {} -export class NodeBase implements ExpressionNode { +export class NodeBase extends ExpressionEntity implements ExpressionNode { annotations?: Annotation[]; context: AstContext; end!: number; esTreeNode: acorn.Node; - included = false; keys: string[]; parent: Node | { context: AstContext; type: string }; scope!: ChildScope; start!: number; type!: keyof typeof NodeType; + // Nodes can apply custom deoptimizations once they become part of the + // executed code. To do this, they must initialize this as false, implement + // applyDeoptimizations and call this from include and hasEffects if they + // have custom handlers + protected deoptimized?: boolean; constructor( esTreeNode: GenericEsTreeNode, parent: Node | { context: AstContext; type: string }, parentScope: ChildScope ) { + super(); this.esTreeNode = esTreeNode; this.keys = keys[esTreeNode.type] || getAndCreateKeys(esTreeNode); this.parent = parent; @@ -149,25 +149,8 @@ export class NodeBase implements ExpressionNode { this.scope = parentScope; } - deoptimizePath(_path: ObjectPath) {} - - getLiteralValueAtPath( - _path: ObjectPath, - _recursionTracker: PathTracker, - _origin: DeoptimizableEntity - ): LiteralValueOrUnknown { - return UnknownValue; - } - - getReturnExpressionWhenCalledAtPath( - _path: ObjectPath, - _recursionTracker: PathTracker, - _origin: DeoptimizableEntity - ): ExpressionEntity { - return UNKNOWN_EXPRESSION; - } - hasEffects(context: HasEffectsContext): boolean { + if (this.deoptimized === false) this.applyDeoptimizations(); for (const key of this.keys) { const value = (this as GenericEsTreeNode)[key]; if (value === null) continue; @@ -180,23 +163,8 @@ export class NodeBase implements ExpressionNode { return false; } - hasEffectsWhenAccessedAtPath(path: ObjectPath, _context: HasEffectsContext) { - return path.length > 0; - } - - hasEffectsWhenAssignedAtPath(_path: ObjectPath, _context: HasEffectsContext) { - return true; - } - - hasEffectsWhenCalledAtPath( - _path: ObjectPath, - _callOptions: CallOptions, - _context: HasEffectsContext - ) { - return true; - } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (this.deoptimized === false) this.applyDeoptimizations(); this.included = true; for (const key of this.keys) { const value = (this as GenericEsTreeNode)[key]; @@ -215,12 +183,6 @@ export class NodeBase implements ExpressionNode { this.include(context, includeChildrenRecursively); } - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - for (const arg of args) { - arg.include(context, false); - } - } - /** * Override to perform special initialisation steps after the scope is initialised */ @@ -232,15 +194,10 @@ export class NodeBase implements ExpressionNode { } } - mayModifyThisWhenCalledAtPath(_path: ObjectPath, _recursionTracker: PathTracker) { - return true; - } - parseNode(esTreeNode: GenericEsTreeNode) { - for (const key of Object.keys(esTreeNode)) { + for (const [key, value] of Object.entries(esTreeNode)) { // That way, we can override this function to add custom initialisation and then call super.parseNode if (this.hasOwnProperty(key)) continue; - const value = esTreeNode[key]; if (key === '_rollupAnnotations') { this.annotations = value; } else if (typeof value !== 'object' || value === null) { @@ -279,6 +236,8 @@ export class NodeBase implements ExpressionNode { shouldBeIncluded(context: InclusionContext): boolean { return this.included || (!context.brokenFlow && this.hasEffects(createHasEffectsContext())); } + + protected applyDeoptimizations(): void {} } export { NodeBase as StatementBase }; diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts new file mode 100644 index 00000000000..b754c36413f --- /dev/null +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -0,0 +1,477 @@ +import { CallOptions } from '../../CallOptions'; +import { DeoptimizableEntity } from '../../DeoptimizableEntity'; +import { HasEffectsContext } from '../../ExecutionContext'; +import { EVENT_ACCESSED, EVENT_CALLED, NodeEvent } from '../../NodeEvents'; +import { + ObjectPath, + ObjectPathKey, + PathTracker, + UnknownInteger, + UnknownKey, + UNKNOWN_INTEGER_PATH, + UNKNOWN_PATH +} from '../../utils/PathTracker'; +import { + ExpressionEntity, + LiteralValueOrUnknown, + UnknownValue, + UNKNOWN_EXPRESSION +} from './Expression'; + +export interface ObjectProperty { + key: ObjectPathKey; + kind: 'init' | 'set' | 'get'; + property: ExpressionEntity; +} + +type PropertyMap = Record; +const INTEGER_REG_EXP = /^\d+$/; + +export class ObjectEntity extends ExpressionEntity { + private readonly allProperties: ExpressionEntity[] = []; + private readonly deoptimizedPaths: Record = Object.create(null); + private readonly expressionsToBeDeoptimizedByKey: Record< + string, + DeoptimizableEntity[] + > = Object.create(null); + private readonly gettersByKey: PropertyMap = Object.create(null); + private hasUnknownDeoptimizedInteger = false; + private hasUnknownDeoptimizedProperty = false; + private readonly propertiesAndGettersByKey: PropertyMap = Object.create(null); + private readonly propertiesAndSettersByKey: PropertyMap = Object.create(null); + private readonly settersByKey: PropertyMap = Object.create(null); + private readonly thisParametersToBeDeoptimized = new Set(); + private readonly unknownIntegerProps: ExpressionEntity[] = []; + private readonly unmatchableGetters: ExpressionEntity[] = []; + private readonly unmatchablePropertiesAndGetters: ExpressionEntity[] = []; + private readonly unmatchableSetters: ExpressionEntity[] = []; + + // If a PropertyMap is used, this will be taken as propertiesAndGettersByKey + // and we assume there are no setters or getters + constructor( + properties: ObjectProperty[] | PropertyMap, + private prototypeExpression: ExpressionEntity | null + ) { + super(); + if (Array.isArray(properties)) { + this.buildPropertyMaps(properties); + } else { + this.propertiesAndGettersByKey = this.propertiesAndSettersByKey = properties; + for (const propertiesForKey of Object.values(properties)) { + this.allProperties.push(...propertiesForKey); + } + } + } + + deoptimizeAllProperties(): void { + if (this.hasUnknownDeoptimizedProperty) { + return; + } + this.hasUnknownDeoptimizedProperty = true; + for (const properties of Object.values(this.propertiesAndGettersByKey).concat( + Object.values(this.settersByKey) + )) { + for (const property of properties) { + property.deoptimizePath(UNKNOWN_PATH); + } + } + // While the prototype itself cannot be mutated, each property can + this.prototypeExpression?.deoptimizePath([UnknownKey, UnknownKey]); + this.deoptimizeCachedEntities(); + } + + deoptimizeIntegerProperties(): void { + if (this.hasUnknownDeoptimizedProperty || this.hasUnknownDeoptimizedInteger) { + return; + } + this.hasUnknownDeoptimizedInteger = true; + for (const [key, propertiesAndGetters] of Object.entries(this.propertiesAndGettersByKey)) { + if (INTEGER_REG_EXP.test(key)) { + for (const property of propertiesAndGetters) { + property.deoptimizePath(UNKNOWN_PATH); + } + } + } + this.deoptimizeCachedIntegerEntities(); + } + + deoptimizePath(path: ObjectPath) { + if (this.hasUnknownDeoptimizedProperty) return; + const key = path[0]; + if (path.length === 1) { + if (typeof key !== 'string') { + if (key === UnknownInteger) { + return this.deoptimizeIntegerProperties(); + } + return this.deoptimizeAllProperties(); + } + if (!this.deoptimizedPaths[key]) { + this.deoptimizedPaths[key] = true; + + // we only deoptimizeCache exact matches as in all other cases, + // we do not return a literal value or return expression + const expressionsToBeDeoptimized = this.expressionsToBeDeoptimizedByKey[key]; + if (expressionsToBeDeoptimized) { + for (const expression of expressionsToBeDeoptimized) { + expression.deoptimizeCache(); + } + } + } + } + + const subPath = path.length === 1 ? UNKNOWN_PATH : path.slice(1); + for (const property of typeof key === 'string' + ? (this.propertiesAndGettersByKey[key] || this.unmatchablePropertiesAndGetters).concat( + this.settersByKey[key] || this.unmatchableSetters + ) + : this.allProperties) { + property.deoptimizePath(subPath); + } + this.prototypeExpression?.deoptimizePath(path.length === 1 ? [UnknownKey, UnknownKey] : path); + } + + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + if (path.length === 0) { + return; + } + const [key, ...subPath] = path; + + if ( + this.hasUnknownDeoptimizedProperty || + // single paths that are deoptimized will not become getters or setters + ((event === EVENT_CALLED || path.length > 1) && + typeof key === 'string' && + this.deoptimizedPaths[key]) + ) { + thisParameter.deoptimizePath(UNKNOWN_PATH); + return; + } + + const [propertiesForExactMatchByKey, relevantPropertiesByKey, relevantUnmatchableProperties] = + event === EVENT_CALLED || path.length > 1 + ? [ + this.propertiesAndGettersByKey, + this.propertiesAndGettersByKey, + this.unmatchablePropertiesAndGetters + ] + : event === EVENT_ACCESSED + ? [this.propertiesAndGettersByKey, this.gettersByKey, this.unmatchableGetters] + : [this.propertiesAndSettersByKey, this.settersByKey, this.unmatchableSetters]; + + if (typeof key === 'string') { + if (propertiesForExactMatchByKey[key]) { + const properties = relevantPropertiesByKey[key]; + if (properties) { + for (const property of properties) { + property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); + } + } + this.thisParametersToBeDeoptimized.add(thisParameter); + return; + } + for (const property of relevantUnmatchableProperties) { + property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); + } + if (INTEGER_REG_EXP.test(key)) { + for (const property of this.unknownIntegerProps) { + property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); + } + } + } else { + for (const properties of Object.values(relevantPropertiesByKey).concat([ + relevantUnmatchableProperties + ])) { + for (const property of properties) { + property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); + } + } + for (const property of this.unknownIntegerProps) { + property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); + } + } + this.thisParametersToBeDeoptimized.add(thisParameter); + this.prototypeExpression?.deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + } + + getLiteralValueAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + if (path.length === 0) { + return UnknownValue; + } + const key = path[0]; + const expressionAtPath = this.getMemberExpressionAndTrackDeopt(key, origin); + if (expressionAtPath) { + return expressionAtPath.getLiteralValueAtPath(path.slice(1), recursionTracker, origin); + } + if (this.prototypeExpression) { + return this.prototypeExpression.getLiteralValueAtPath(path, recursionTracker, origin); + } + if (path.length === 1) { + return undefined; + } + return UnknownValue; + } + + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): ExpressionEntity { + if (path.length === 0) { + return UNKNOWN_EXPRESSION; + } + const key = path[0]; + const expressionAtPath = this.getMemberExpressionAndTrackDeopt(key, origin); + if (expressionAtPath) { + return expressionAtPath.getReturnExpressionWhenCalledAtPath( + path.slice(1), + callOptions, + recursionTracker, + origin + ); + } + if (this.prototypeExpression) { + return this.prototypeExpression.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); + } + return UNKNOWN_EXPRESSION; + } + + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + const [key, ...subPath] = path; + if (path.length > 1) { + if (typeof key !== 'string') { + return true; + } + const expressionAtPath = this.getMemberExpression(key); + if (expressionAtPath) { + return expressionAtPath.hasEffectsWhenAccessedAtPath(subPath, context); + } + if (this.prototypeExpression) { + return this.prototypeExpression.hasEffectsWhenAccessedAtPath(path, context); + } + return true; + } + + if (this.hasUnknownDeoptimizedProperty) return true; + if (typeof key === 'string') { + if (this.propertiesAndGettersByKey[key]) { + const getters = this.gettersByKey[key]; + if (getters) { + for (const getter of getters) { + if (getter.hasEffectsWhenAccessedAtPath(subPath, context)) return true; + } + } + return false; + } + for (const getter of this.unmatchableGetters) { + if (getter.hasEffectsWhenAccessedAtPath(subPath, context)) return true; + } + } else { + for (const getters of Object.values(this.gettersByKey).concat([this.unmatchableGetters])) { + for (const getter of getters) { + if (getter.hasEffectsWhenAccessedAtPath(subPath, context)) return true; + } + } + } + if (this.prototypeExpression) { + return this.prototypeExpression.hasEffectsWhenAccessedAtPath(path, context); + } + return false; + } + + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + const [key, ...subPath] = path; + if (path.length > 1) { + if (typeof key !== 'string') { + return true; + } + const expressionAtPath = this.getMemberExpression(key); + if (expressionAtPath) { + return expressionAtPath.hasEffectsWhenAssignedAtPath(subPath, context); + } + if (this.prototypeExpression) { + return this.prototypeExpression.hasEffectsWhenAssignedAtPath(path, context); + } + return true; + } + + if (this.hasUnknownDeoptimizedProperty) return true; + if (typeof key === 'string') { + if (this.propertiesAndSettersByKey[key]) { + const setters = this.settersByKey[key]; + if (setters) { + for (const setter of setters) { + if (setter.hasEffectsWhenAssignedAtPath(subPath, context)) return true; + } + } + return false; + } + for (const property of this.unmatchableSetters) { + if (property.hasEffectsWhenAssignedAtPath(subPath, context)) return true; + } + } else { + for (const setters of Object.values(this.settersByKey).concat([this.unmatchableSetters])) { + for (const setter of setters) { + if (setter.hasEffectsWhenAssignedAtPath(subPath, context)) return true; + } + } + } + if (this.prototypeExpression) { + return this.prototypeExpression.hasEffectsWhenAssignedAtPath(path, context); + } + return false; + } + + hasEffectsWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + context: HasEffectsContext + ): boolean { + const key = path[0]; + const expressionAtPath = this.getMemberExpression(key); + if (expressionAtPath) { + return expressionAtPath.hasEffectsWhenCalledAtPath(path.slice(1), callOptions, context); + } + if (this.prototypeExpression) { + return this.prototypeExpression.hasEffectsWhenCalledAtPath(path, callOptions, context); + } + return true; + } + + private buildPropertyMaps(properties: ObjectProperty[]): void { + const { + allProperties, + propertiesAndGettersByKey, + propertiesAndSettersByKey, + settersByKey, + gettersByKey, + unknownIntegerProps, + unmatchablePropertiesAndGetters, + unmatchableGetters, + unmatchableSetters + } = this; + const unmatchablePropertiesAndSetters: ExpressionEntity[] = []; + for (let index = properties.length - 1; index >= 0; index--) { + const { key, kind, property } = properties[index]; + allProperties.push(property); + if (typeof key !== 'string') { + if (key === UnknownInteger) { + unknownIntegerProps.push(property); + continue; + } + if (kind === 'set') unmatchableSetters.push(property); + if (kind === 'get') unmatchableGetters.push(property); + if (kind !== 'get') unmatchablePropertiesAndSetters.push(property); + if (kind !== 'set') unmatchablePropertiesAndGetters.push(property); + } else { + if (kind === 'set') { + if (!propertiesAndSettersByKey[key]) { + propertiesAndSettersByKey[key] = [property, ...unmatchablePropertiesAndSetters]; + settersByKey[key] = [property, ...unmatchableSetters]; + } + } else if (kind === 'get') { + if (!propertiesAndGettersByKey[key]) { + propertiesAndGettersByKey[key] = [property, ...unmatchablePropertiesAndGetters]; + gettersByKey[key] = [property, ...unmatchableGetters]; + } + } else { + if (!propertiesAndSettersByKey[key]) { + propertiesAndSettersByKey[key] = [property, ...unmatchablePropertiesAndSetters]; + } + if (!propertiesAndGettersByKey[key]) { + propertiesAndGettersByKey[key] = [property, ...unmatchablePropertiesAndGetters]; + if (INTEGER_REG_EXP.test(key)) { + for (const integerProperty of unknownIntegerProps) { + propertiesAndGettersByKey[key].push(integerProperty); + } + } + } + } + } + } + } + + private deoptimizeCachedEntities() { + for (const expressionsToBeDeoptimized of Object.values(this.expressionsToBeDeoptimizedByKey)) { + for (const expression of expressionsToBeDeoptimized) { + expression.deoptimizeCache(); + } + } + for (const expression of this.thisParametersToBeDeoptimized) { + expression.deoptimizePath(UNKNOWN_PATH); + } + } + + private deoptimizeCachedIntegerEntities() { + for (const [key, expressionsToBeDeoptimized] of Object.entries( + this.expressionsToBeDeoptimizedByKey + )) { + if (INTEGER_REG_EXP.test(key)) { + for (const expression of expressionsToBeDeoptimized) { + expression.deoptimizeCache(); + } + } + } + for (const expression of this.thisParametersToBeDeoptimized) { + expression.deoptimizePath(UNKNOWN_INTEGER_PATH); + } + } + + private getMemberExpression(key: ObjectPathKey): ExpressionEntity | null { + if ( + this.hasUnknownDeoptimizedProperty || + typeof key !== 'string' || + (this.hasUnknownDeoptimizedInteger && INTEGER_REG_EXP.test(key)) || + this.deoptimizedPaths[key] + ) { + return UNKNOWN_EXPRESSION; + } + const properties = this.propertiesAndGettersByKey[key]; + if (properties?.length === 1) { + return properties[0]; + } + if ( + properties || + this.unmatchablePropertiesAndGetters.length > 0 || + (this.unknownIntegerProps.length && INTEGER_REG_EXP.test(key)) + ) { + return UNKNOWN_EXPRESSION; + } + return null; + } + + private getMemberExpressionAndTrackDeopt( + key: ObjectPathKey, + origin: DeoptimizableEntity + ): ExpressionEntity | null { + if (typeof key !== 'string') { + return UNKNOWN_EXPRESSION; + } + const expression = this.getMemberExpression(key); + if (expression !== UNKNOWN_EXPRESSION) { + const expressionsToBeDeoptimized = (this.expressionsToBeDeoptimizedByKey[key] = + this.expressionsToBeDeoptimizedByKey[key] || []); + expressionsToBeDeoptimized.push(origin); + } + return expression; + } +} diff --git a/src/ast/nodes/shared/ObjectMember.ts b/src/ast/nodes/shared/ObjectMember.ts new file mode 100644 index 00000000000..d72db17c612 --- /dev/null +++ b/src/ast/nodes/shared/ObjectMember.ts @@ -0,0 +1,64 @@ +import { CallOptions } from '../../CallOptions'; +import { DeoptimizableEntity } from '../../DeoptimizableEntity'; +import { HasEffectsContext } from '../../ExecutionContext'; +import { NodeEvent } from '../../NodeEvents'; +import { ObjectPath, PathTracker } from '../../utils/PathTracker'; +import { ExpressionEntity, LiteralValueOrUnknown } from './Expression'; + +export class ObjectMember extends ExpressionEntity { + constructor(private readonly object: ExpressionEntity, private readonly key: string) { + super(); + } + + deoptimizePath(path: ObjectPath): void { + this.object.deoptimizePath([this.key, ...path]); + } + + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.object.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } + + getLiteralValueAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + return this.object.getLiteralValueAtPath([this.key, ...path], recursionTracker, origin); + } + + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): ExpressionEntity { + return this.object.getReturnExpressionWhenCalledAtPath( + [this.key, ...path], + callOptions, + recursionTracker, + origin + ); + } + + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + if (path.length === 0) return false; + return this.object.hasEffectsWhenAccessedAtPath([this.key, ...path], context); + } + + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + return this.object.hasEffectsWhenAssignedAtPath([this.key, ...path], context); + } + + hasEffectsWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + context: HasEffectsContext + ): boolean { + return this.object.hasEffectsWhenCalledAtPath([this.key, ...path], callOptions, context); + } +} diff --git a/src/ast/nodes/shared/ObjectPrototype.ts b/src/ast/nodes/shared/ObjectPrototype.ts new file mode 100644 index 00000000000..88e20e2d7a0 --- /dev/null +++ b/src/ast/nodes/shared/ObjectPrototype.ts @@ -0,0 +1,26 @@ +import { + METHOD_RETURNS_BOOLEAN, + METHOD_RETURNS_STRING, + METHOD_RETURNS_UNKNOWN +} from './MethodTypes'; +import { ObjectEntity } from './ObjectEntity'; + +export const OBJECT_PROTOTYPE = new ObjectEntity( + { + // @ts-ignore + __proto__: null, + // @ts-ignore + hasOwnProperty: METHOD_RETURNS_BOOLEAN, + // @ts-ignore + isPrototypeOf: METHOD_RETURNS_BOOLEAN, + // @ts-ignore + propertyIsEnumerable: METHOD_RETURNS_BOOLEAN, + // @ts-ignore + toLocaleString: METHOD_RETURNS_STRING, + // @ts-ignore + toString: METHOD_RETURNS_STRING, + // @ts-ignore + valueOf: METHOD_RETURNS_UNKNOWN + }, + null +); diff --git a/src/ast/scopes/BlockScope.ts b/src/ast/scopes/BlockScope.ts index b72bcc3a19e..d6393239f6e 100644 --- a/src/ast/scopes/BlockScope.ts +++ b/src/ast/scopes/BlockScope.ts @@ -1,7 +1,6 @@ import { AstContext } from '../../Module'; import Identifier from '../nodes/Identifier'; -import { ExpressionEntity } from '../nodes/shared/Expression'; -import { UNKNOWN_EXPRESSION } from '../values'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; import LocalVariable from '../variables/LocalVariable'; import ChildScope from './ChildScope'; diff --git a/src/ast/scopes/ClassBodyScope.ts b/src/ast/scopes/ClassBodyScope.ts index ebda392188a..83952d39dc6 100644 --- a/src/ast/scopes/ClassBodyScope.ts +++ b/src/ast/scopes/ClassBodyScope.ts @@ -1,6 +1,24 @@ +import { AstContext } from '../../Module'; +import { ExpressionEntity } from '../nodes/shared/Expression'; +import LocalVariable from '../variables/LocalVariable'; +import ThisVariable from '../variables/ThisVariable'; import ChildScope from './ChildScope'; +import Scope from './Scope'; export default class ClassBodyScope extends ChildScope { + instanceScope: ChildScope; + thisVariable: LocalVariable; + + constructor(parent: Scope, classNode: ExpressionEntity, context: AstContext) { + super(parent); + this.variables.set( + 'this', + (this.thisVariable = new LocalVariable('this', null, classNode, context)) + ); + this.instanceScope = new ChildScope(this); + this.instanceScope.variables.set('this', new ThisVariable(context)); + } + findLexicalBoundary() { return this; } diff --git a/src/ast/scopes/ParameterScope.ts b/src/ast/scopes/ParameterScope.ts index 8aafd8d4e09..77d30c09d7f 100644 --- a/src/ast/scopes/ParameterScope.ts +++ b/src/ast/scopes/ParameterScope.ts @@ -1,9 +1,9 @@ import { AstContext } from '../../Module'; import { InclusionContext } from '../ExecutionContext'; import Identifier from '../nodes/Identifier'; +import { UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; import { ExpressionNode } from '../nodes/shared/Node'; import SpreadElement from '../nodes/SpreadElement'; -import { UNKNOWN_EXPRESSION } from '../values'; import LocalVariable from '../variables/LocalVariable'; import ChildScope from './ChildScope'; import Scope from './Scope'; diff --git a/src/ast/scopes/ReturnValueScope.ts b/src/ast/scopes/ReturnValueScope.ts index 8647cd299ba..3045740825a 100644 --- a/src/ast/scopes/ReturnValueScope.ts +++ b/src/ast/scopes/ReturnValueScope.ts @@ -1,6 +1,5 @@ -import { ExpressionEntity } from '../nodes/shared/Expression'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; import { UNKNOWN_PATH } from '../utils/PathTracker'; -import { UNKNOWN_EXPRESSION } from '../values'; import ParameterScope from './ParameterScope'; export default class ReturnValueScope extends ParameterScope { diff --git a/src/ast/utils/PathTracker.ts b/src/ast/utils/PathTracker.ts index 74e10553744..0c2d8306e27 100644 --- a/src/ast/utils/PathTracker.ts +++ b/src/ast/utils/PathTracker.ts @@ -2,23 +2,49 @@ import { getOrCreate } from '../../utils/getOrCreate'; import { Entity } from '../Entity'; export const UnknownKey = Symbol('Unknown Key'); -export type ObjectPathKey = string | typeof UnknownKey; +export const UnknownInteger = Symbol('Unknown Integer'); +export type ObjectPathKey = string | typeof UnknownKey | typeof UnknownInteger; export type ObjectPath = ObjectPathKey[]; export const EMPTY_PATH: ObjectPath = []; export const UNKNOWN_PATH: ObjectPath = [UnknownKey]; +export const UNKNOWN_INTEGER_PATH: ObjectPath = [UnknownInteger]; const EntitiesKey = Symbol('Entities'); interface EntityPaths { [EntitiesKey]: Set; [UnknownKey]?: EntityPaths; + [UnknownInteger]?: EntityPaths; [pathSegment: string]: EntityPaths; } export class PathTracker { - entityPaths: EntityPaths = Object.create(null, { [EntitiesKey]: { value: new Set() } }); + private entityPaths: EntityPaths = Object.create(null, { + [EntitiesKey]: { value: new Set() } + }); + + trackEntityAtPathAndGetIfTracked(path: ObjectPath, entity: Entity): boolean { + const trackedEntities = this.getEntities(path); + if (trackedEntities.has(entity)) return true; + trackedEntities.add(entity); + return false; + } + + withTrackedEntityAtPath( + path: ObjectPath, + entity: Entity, + onUntracked: () => T, + returnIfTracked: T + ): T { + const trackedEntities = this.getEntities(path); + if (trackedEntities.has(entity)) return returnIfTracked; + trackedEntities.add(entity); + const result = onUntracked(); + trackedEntities.delete(entity); + return result; + } - getEntities(path: ObjectPath): Set { + private getEntities(path: ObjectPath): Set { let currentPaths = this.entityPaths; for (const pathSegment of path) { currentPaths = currentPaths[pathSegment] = @@ -34,21 +60,29 @@ export const SHARED_RECURSION_TRACKER = new PathTracker(); interface DiscriminatedEntityPaths { [EntitiesKey]: Map>; [UnknownKey]?: DiscriminatedEntityPaths; + [UnknownInteger]?: DiscriminatedEntityPaths; [pathSegment: string]: DiscriminatedEntityPaths; } export class DiscriminatedPathTracker { - entityPaths: DiscriminatedEntityPaths = Object.create(null, { + private entityPaths: DiscriminatedEntityPaths = Object.create(null, { [EntitiesKey]: { value: new Map>() } }); - getEntities(path: ObjectPath, discriminator: object): Set { + trackEntityAtPathAndGetIfTracked( + path: ObjectPath, + discriminator: object, + entity: Entity + ): boolean { let currentPaths = this.entityPaths; for (const pathSegment of path) { currentPaths = currentPaths[pathSegment] = currentPaths[pathSegment] || Object.create(null, { [EntitiesKey]: { value: new Map>() } }); } - return getOrCreate(currentPaths[EntitiesKey], discriminator, () => new Set()); + const trackedEntities = getOrCreate(currentPaths[EntitiesKey], discriminator, () => new Set()); + if (trackedEntities.has(entity)) return true; + trackedEntities.add(entity); + return false; } } diff --git a/src/ast/values.ts b/src/ast/values.ts index 0d7ea2ca50f..1edfae57d84 100644 --- a/src/ast/values.ts +++ b/src/ast/values.ts @@ -1,16 +1,12 @@ import { CallOptions, NO_ARGS } from './CallOptions'; -import { HasEffectsContext, InclusionContext } from './ExecutionContext'; +import { HasEffectsContext } from './ExecutionContext'; import { LiteralValue } from './nodes/Literal'; -import { ExpressionEntity } from './nodes/shared/Expression'; -import { ExpressionNode } from './nodes/shared/Node'; -import SpreadElement from './nodes/SpreadElement'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './nodes/shared/Expression'; import { EMPTY_PATH, ObjectPath, ObjectPathKey } from './utils/PathTracker'; export interface MemberDescription { callsArgs: number[] | null; - mutatesSelf: boolean; - returns: { new (): ExpressionEntity } | null; - returnsPrimitive: ExpressionEntity | null; + returns: ExpressionEntity; } export interface MemberDescriptions { @@ -28,87 +24,23 @@ function assembleMemberDescriptions( return Object.create(inheritedDescriptions, memberDescriptions); } -export const UnknownValue = Symbol('Unknown Value'); -export type LiteralValueOrUnknown = LiteralValue | typeof UnknownValue; - -abstract class ValueBase implements ExpressionEntity { - included = true; - - deoptimizePath() {} - - getLiteralValueAtPath(): LiteralValueOrUnknown { - return UnknownValue; - } - - getReturnExpressionWhenCalledAtPath(_path: ObjectPath) { - return UNKNOWN_EXPRESSION; - } - - hasEffectsWhenAccessedAtPath( - path: ObjectPath, - _context: HasEffectsContext - ) { - return path.length > 0 - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath) { - return path.length > 0 - } - - hasEffectsWhenCalledAtPath( - _path: ObjectPath, - _callOptions: CallOptions, - _context: HasEffectsContext - ) { - return true; - } - - include() {} - - includeCallArguments(_context: InclusionContext, _args: (ExpressionNode | SpreadElement)[]) {} - - mayModifyThisWhenCalledAtPath() { return true; } -} - -function includeAll(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]) { - for (const arg of args) { - arg.include(context, false); - } -} - -export const UNKNOWN_EXPRESSION: ExpressionEntity = new class extends ValueBase { - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - includeAll(context, args); - } -}; - -export const UNDEFINED_EXPRESSION: ExpressionEntity = new class extends ValueBase { +export const UNDEFINED_EXPRESSION: ExpressionEntity = new (class UndefinedExpression extends ExpressionEntity { getLiteralValueAtPath() { return undefined; } -}; +})(); const returnsUnknown: RawMemberDescription = { value: { callsArgs: null, - mutatesSelf: false, - returns: null, - returnsPrimitive: UNKNOWN_EXPRESSION + returns: UNKNOWN_EXPRESSION } }; -const mutatesSelfReturnsUnknown: RawMemberDescription = { - value: { returns: null, returnsPrimitive: UNKNOWN_EXPRESSION, callsArgs: null, mutatesSelf: true } -}; -const callsArgReturnsUnknown: RawMemberDescription = { - value: { returns: null, returnsPrimitive: UNKNOWN_EXPRESSION, callsArgs: [0], mutatesSelf: false } -}; - -export class UnknownArrayExpression extends ValueBase { - included = false; +export const UNKNOWN_LITERAL_BOOLEAN: ExpressionEntity = new (class UnknownBoolean extends ExpressionEntity { getReturnExpressionWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { - return getMemberReturnExpressionWhenCalled(arrayMembers, path[0]); + return getMemberReturnExpressionWhenCalled(literalBooleanMembers, path[0]); } return UNKNOWN_EXPRESSION; } @@ -117,185 +49,60 @@ export class UnknownArrayExpression extends ValueBase { return path.length > 1; } - hasEffectsWhenAssignedAtPath(path: ObjectPath) { - return path.length > 1; - } - hasEffectsWhenCalledAtPath( path: ObjectPath, callOptions: CallOptions, context: HasEffectsContext ) { if (path.length === 1) { - return hasMemberEffectWhenCalled(arrayMembers, path[0], this.included, callOptions, context); + return hasMemberEffectWhenCalled(literalBooleanMembers, path[0], callOptions, context); } return true; } - - include() { - this.included = true; - } - - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - includeAll(context, args); - } -} - -const returnsArray: RawMemberDescription = { - value: { - callsArgs: null, - mutatesSelf: false, - returns: UnknownArrayExpression, - returnsPrimitive: null - } -}; -const mutatesSelfReturnsArray: RawMemberDescription = { - value: { - callsArgs: null, - mutatesSelf: true, - returns: UnknownArrayExpression, - returnsPrimitive: null - } -}; -const callsArgReturnsArray: RawMemberDescription = { - value: { - callsArgs: [0], - mutatesSelf: false, - returns: UnknownArrayExpression, - returnsPrimitive: null - } -}; -const callsArgMutatesSelfReturnsArray: RawMemberDescription = { - value: { - callsArgs: [0], - mutatesSelf: true, - returns: UnknownArrayExpression, - returnsPrimitive: null - } -}; - -const UNKNOWN_LITERAL_BOOLEAN: ExpressionEntity = new class extends ValueBase { - getReturnExpressionWhenCalledAtPath(path: ObjectPath) { - if (path.length === 1) { - return getMemberReturnExpressionWhenCalled(literalBooleanMembers, path[0]); - } - return UNKNOWN_EXPRESSION; - } - hasEffectsWhenAccessedAtPath(path: ObjectPath) { - return path.length > 1 - } - hasEffectsWhenCalledAtPath(path: ObjectPath) { - if (path.length === 1) { - const subPath = path[0]; - return typeof subPath !== 'string' || !literalBooleanMembers[subPath]; - } - return true; - } - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - includeAll(context, args); - } -}; +})(); const returnsBoolean: RawMemberDescription = { value: { callsArgs: null, - mutatesSelf: false, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_BOOLEAN - } -}; -const callsArgReturnsBoolean: RawMemberDescription = { - value: { - callsArgs: [0], - mutatesSelf: false, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_BOOLEAN + returns: UNKNOWN_LITERAL_BOOLEAN } }; -const UNKNOWN_LITERAL_NUMBER: ExpressionEntity = new class extends ValueBase { +export const UNKNOWN_LITERAL_NUMBER: ExpressionEntity = new (class UnknownNumber extends ExpressionEntity { getReturnExpressionWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { return getMemberReturnExpressionWhenCalled(literalNumberMembers, path[0]); } return UNKNOWN_EXPRESSION; } - hasEffectsWhenAccessedAtPath(path: ObjectPath) { return path.length > 1; } - hasEffectsWhenCalledAtPath(path: ObjectPath) { - if (path.length === 1) { - const subPath = path[0]; - return typeof subPath !== 'string' || !literalNumberMembers[subPath]; - } - return true; - } - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - includeAll(context, args); - } -}; -const returnsNumber: RawMemberDescription = { - value: { - callsArgs: null, - mutatesSelf: false, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_NUMBER - } -}; -const mutatesSelfReturnsNumber: RawMemberDescription = { - value: { - callsArgs: null, - mutatesSelf: true, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_NUMBER - } -}; -const callsArgReturnsNumber: RawMemberDescription = { - value: { - callsArgs: [0], - mutatesSelf: false, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_NUMBER + hasEffectsWhenAccessedAtPath(path: ObjectPath) { + return path.length > 1; } -}; -const UNKNOWN_LITERAL_STRING: ExpressionEntity = new class extends ValueBase { - getReturnExpressionWhenCalledAtPath(path: ObjectPath) { - if (path.length === 1) { - return getMemberReturnExpressionWhenCalled(literalStringMembers, path[0]); - } - return UNKNOWN_EXPRESSION; - } - hasEffectsWhenAccessedAtPath(path: ObjectPath) { return path.length > 1 } hasEffectsWhenCalledAtPath( path: ObjectPath, callOptions: CallOptions, context: HasEffectsContext ) { if (path.length === 1) { - return hasMemberEffectWhenCalled(literalStringMembers, path[0], true, callOptions, context); + return hasMemberEffectWhenCalled(literalNumberMembers, path[0], callOptions, context); } return true; } - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - includeAll(context, args); - } -}; +})(); -const returnsString: RawMemberDescription = { +const returnsNumber: RawMemberDescription = { value: { callsArgs: null, - mutatesSelf: false, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_STRING + returns: UNKNOWN_LITERAL_NUMBER } }; -export class UnknownObjectExpression extends ValueBase { - included = false; - +export const UNKNOWN_LITERAL_STRING: ExpressionEntity = new (class UnknownString extends ExpressionEntity { getReturnExpressionWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { - return getMemberReturnExpressionWhenCalled(objectMembers, path[0]); + return getMemberReturnExpressionWhenCalled(literalStringMembers, path[0]); } return UNKNOWN_EXPRESSION; } @@ -304,31 +111,26 @@ export class UnknownObjectExpression extends ValueBase { return path.length > 1; } - hasEffectsWhenAssignedAtPath(path: ObjectPath) { - return path.length > 1; - } - hasEffectsWhenCalledAtPath( path: ObjectPath, callOptions: CallOptions, context: HasEffectsContext ) { if (path.length === 1) { - return hasMemberEffectWhenCalled(objectMembers, path[0], this.included, callOptions, context); + return hasMemberEffectWhenCalled(literalStringMembers, path[0], callOptions, context); } return true; } +})(); - include() { - this.included = true; - } - - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - includeAll(context, args); +const returnsString: RawMemberDescription = { + value: { + callsArgs: null, + returns: UNKNOWN_LITERAL_STRING } -} +}; -export const objectMembers: MemberDescriptions = assembleMemberDescriptions({ +const objectMembers: MemberDescriptions = assembleMemberDescriptions({ hasOwnProperty: returnsBoolean, isPrototypeOf: returnsBoolean, propertyIsEnumerable: returnsBoolean, @@ -337,36 +139,6 @@ export const objectMembers: MemberDescriptions = assembleMemberDescriptions({ valueOf: returnsUnknown }); -export const arrayMembers: MemberDescriptions = assembleMemberDescriptions( - { - concat: returnsArray, - copyWithin: mutatesSelfReturnsArray, - every: callsArgReturnsBoolean, - fill: mutatesSelfReturnsArray, - filter: callsArgReturnsArray, - find: callsArgReturnsUnknown, - findIndex: callsArgReturnsNumber, - forEach: callsArgReturnsUnknown, - includes: returnsBoolean, - indexOf: returnsNumber, - join: returnsString, - lastIndexOf: returnsNumber, - map: callsArgReturnsArray, - pop: mutatesSelfReturnsUnknown, - push: mutatesSelfReturnsNumber, - reduce: callsArgReturnsUnknown, - reduceRight: callsArgReturnsUnknown, - reverse: mutatesSelfReturnsArray, - shift: mutatesSelfReturnsUnknown, - slice: returnsArray, - some: callsArgReturnsBoolean, - sort: callsArgMutatesSelfReturnsArray, - splice: mutatesSelfReturnsArray, - unshift: mutatesSelfReturnsNumber - }, - objectMembers -); - const literalBooleanMembers: MemberDescriptions = assembleMemberDescriptions( { valueOf: returnsBoolean @@ -404,14 +176,12 @@ const literalStringMembers: MemberDescriptions = assembleMemberDescriptions( replace: { value: { callsArgs: [1], - mutatesSelf: false, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_STRING + returns: UNKNOWN_LITERAL_STRING } }, search: returnsNumber, slice: returnsString, - split: returnsArray, + split: returnsUnknown, startsWith: returnsBoolean, substr: returnsString, substring: returnsString, @@ -441,16 +211,12 @@ export function getLiteralMembersForValue export function hasMemberEffectWhenCalled( members: MemberDescriptions, memberName: ObjectPathKey, - parentIncluded: boolean, callOptions: CallOptions, context: HasEffectsContext ) { - if ( - typeof memberName !== 'string' || - !members[memberName] || - (members[memberName].mutatesSelf && parentIncluded) - ) + if (typeof memberName !== 'string' || !members[memberName]) { return true; + } if (!members[memberName].callsArgs) return false; for (const argIndex of members[memberName].callsArgs!) { if ( @@ -459,6 +225,7 @@ export function hasMemberEffectWhenCalled( EMPTY_PATH, { args: NO_ARGS, + thisParam: null, withNew: false }, context @@ -474,7 +241,5 @@ export function getMemberReturnExpressionWhenCalled( memberName: ObjectPathKey ): ExpressionEntity { if (typeof memberName !== 'string' || !members[memberName]) return UNKNOWN_EXPRESSION; - return members[memberName].returnsPrimitive !== null - ? members[memberName].returnsPrimitive! - : new members[memberName].returns!(); + return members[memberName].returns; } diff --git a/src/ast/variables/ArgumentsVariable.ts b/src/ast/variables/ArgumentsVariable.ts index b67ff2f0de4..d511000e0cd 100644 --- a/src/ast/variables/ArgumentsVariable.ts +++ b/src/ast/variables/ArgumentsVariable.ts @@ -1,6 +1,6 @@ import { AstContext } from '../../Module'; +import { UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; import { ObjectPath } from '../utils/PathTracker'; -import { UNKNOWN_EXPRESSION } from '../values'; import LocalVariable from './LocalVariable'; export default class ArgumentsVariable extends LocalVariable { diff --git a/src/ast/variables/LocalVariable.ts b/src/ast/variables/LocalVariable.ts index 6c3ec83133d..346d1687487 100644 --- a/src/ast/variables/LocalVariable.ts +++ b/src/ast/variables/LocalVariable.ts @@ -2,14 +2,19 @@ import Module, { AstContext } from '../../Module'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { createInclusionContext, HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { NodeEvent } from '../NodeEvents'; import ExportDefaultDeclaration from '../nodes/ExportDefaultDeclaration'; import Identifier from '../nodes/Identifier'; import * as NodeType from '../nodes/NodeType'; -import { ExpressionEntity } from '../nodes/shared/Expression'; +import { + ExpressionEntity, + LiteralValueOrUnknown, + UnknownValue, + UNKNOWN_EXPRESSION +} from '../nodes/shared/Expression'; import { ExpressionNode, Node } from '../nodes/shared/Node'; import SpreadElement from '../nodes/SpreadElement'; import { ObjectPath, PathTracker, UNKNOWN_PATH } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../values'; import Variable from './Variable'; // To avoid infinite recursions @@ -24,7 +29,7 @@ export default class LocalVariable extends Variable { // Caching and deoptimization: // We track deoptimization when we do not return something unknown - private deoptimizationTracker: PathTracker; + protected deoptimizationTracker: PathTracker; private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; constructor( @@ -62,10 +67,13 @@ export default class LocalVariable extends Variable { } deoptimizePath(path: ObjectPath) { - if (path.length > MAX_PATH_DEPTH || this.isReassigned) return; - const trackedEntities = this.deoptimizationTracker.getEntities(path); - if (trackedEntities.has(this)) return; - trackedEntities.add(this); + if ( + path.length > MAX_PATH_DEPTH || + this.isReassigned || + this.deoptimizationTracker.trackEntityAtPathAndGetIfTracked(path, this) + ) { + return; + } if (path.length === 0) { if (!this.isReassigned) { this.isReassigned = true; @@ -74,13 +82,28 @@ export default class LocalVariable extends Variable { for (const expression of expressionsToBeDeoptimized) { expression.deoptimizeCache(); } - if (this.init) { - this.init.deoptimizePath(UNKNOWN_PATH); - } + this.init?.deoptimizePath(UNKNOWN_PATH); } - } else if (this.init) { - this.init.deoptimizePath(path); + } else { + this.init?.deoptimizePath(path); + } + } + + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ): void { + if (this.isReassigned || !this.init || path.length > MAX_PATH_DEPTH) { + return thisParameter.deoptimizePath(UNKNOWN_PATH); } + recursionTracker.withTrackedEntityAtPath( + path, + this.init, + () => this.init!.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker), + undefined + ); } getLiteralValueAtPath( @@ -91,53 +114,56 @@ export default class LocalVariable extends Variable { if (this.isReassigned || !this.init || path.length > MAX_PATH_DEPTH) { return UnknownValue; } - const trackedEntities = recursionTracker.getEntities(path); - if (trackedEntities.has(this.init)) { - return UnknownValue; - } - this.expressionsToBeDeoptimized.push(origin); - trackedEntities.add(this.init); - const value = this.init.getLiteralValueAtPath(path, recursionTracker, origin); - trackedEntities.delete(this.init); - return value; + return recursionTracker.withTrackedEntityAtPath( + path, + this.init, + () => { + this.expressionsToBeDeoptimized.push(origin); + return this.init!.getLiteralValueAtPath(path, recursionTracker, origin); + }, + UnknownValue + ); } getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { if (this.isReassigned || !this.init || path.length > MAX_PATH_DEPTH) { return UNKNOWN_EXPRESSION; } - const trackedEntities = recursionTracker.getEntities(path); - if (trackedEntities.has(this.init)) { - return UNKNOWN_EXPRESSION; - } - this.expressionsToBeDeoptimized.push(origin); - trackedEntities.add(this.init); - const value = this.init.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); - trackedEntities.delete(this.init); - return value; + return recursionTracker.withTrackedEntityAtPath( + path, + this.init, + () => { + this.expressionsToBeDeoptimized.push(origin); + return this.init!.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); + }, + UNKNOWN_EXPRESSION + ); } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { - if (path.length === 0) return false; if (this.isReassigned || path.length > MAX_PATH_DEPTH) return true; - const trackedExpressions = context.accessed.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return (this.init && this.init.hasEffectsWhenAccessedAtPath(path, context))!; + return (this.init && + !context.accessed.trackEntityAtPathAndGetIfTracked(path, this) && + this.init.hasEffectsWhenAccessedAtPath(path, context))!; } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext) { if (this.included || path.length > MAX_PATH_DEPTH) return true; if (path.length === 0) return false; if (this.isReassigned) return true; - const trackedExpressions = context.assigned.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return (this.init && this.init.hasEffectsWhenAssignedAtPath(path, context))!; + return (this.init && + !context.accessed.trackEntityAtPathAndGetIfTracked(path, this) && + this.init.hasEffectsWhenAssignedAtPath(path, context))!; } hasEffectsWhenCalledAtPath( @@ -146,13 +172,12 @@ export default class LocalVariable extends Variable { context: HasEffectsContext ) { if (path.length > MAX_PATH_DEPTH || this.isReassigned) return true; - const trackedExpressions = (callOptions.withNew - ? context.instantiated - : context.called - ).getEntities(path, callOptions); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return (this.init && this.init.hasEffectsWhenCalledAtPath(path, callOptions, context))!; + return (this.init && + !(callOptions.withNew + ? context.instantiated + : context.called + ).trackEntityAtPathAndGetIfTracked(path, callOptions, this) && + this.init.hasEffectsWhenCalledAtPath(path, callOptions, context))!; } include() { @@ -188,18 +213,4 @@ export default class LocalVariable extends Variable { markCalledFromTryStatement() { this.calledFromTryStatement = true; } - - mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker) { - if (this.isReassigned || !this.init || path.length > MAX_PATH_DEPTH) { - return true; - } - const trackedEntities = recursionTracker.getEntities(path); - if (trackedEntities.has(this.init)) { - return true; - } - trackedEntities.add(this.init); - const result = this.init.mayModifyThisWhenCalledAtPath(path, recursionTracker); - trackedEntities.delete(this.init); - return result; - } } diff --git a/src/ast/variables/NamespaceVariable.ts b/src/ast/variables/NamespaceVariable.ts index 0ed3fbbcfd1..62bf7ac2b0e 100644 --- a/src/ast/variables/NamespaceVariable.ts +++ b/src/ast/variables/NamespaceVariable.ts @@ -3,7 +3,6 @@ import { RenderOptions } from '../../utils/renderHelpers'; import { RESERVED_NAMES } from '../../utils/reservedNames'; import { getSystemExportStatement } from '../../utils/systemJsRendering'; import Identifier from '../nodes/Identifier'; -import { ObjectPath, UNKNOWN_PATH } from '../utils/PathTracker'; import Variable from './Variable'; export default class NamespaceVariable extends Variable { @@ -29,19 +28,6 @@ export default class NamespaceVariable extends Variable { this.name = identifier.name; } - deoptimizePath(path: ObjectPath) { - const memberVariables = this.getMemberVariables(); - const memberPath = path.length <= 1 ? UNKNOWN_PATH : path.slice(1); - const key = path[0]; - if (typeof key === 'string') { - memberVariables[key]?.deoptimizePath(memberPath); - } else { - for (const key of Object.keys(memberVariables)) { - memberVariables[key].deoptimizePath(memberPath); - } - } - } - getMemberVariables(): { [name: string]: Variable } { if (this.memberVariables) { return this.memberVariables; @@ -80,9 +66,7 @@ export default class NamespaceVariable extends Variable { const t = options.indent; const memberVariables = this.getMemberVariables(); - const members = Object.keys(memberVariables).map(name => { - const original = memberVariables[name]; - + const members = Object.entries(memberVariables).map(([name, original]) => { if (this.referencedEarly || original.isReassigned) { return `${t}get ${name}${_}()${_}{${_}return ${original.getName()}${ options.compact ? '' : ';' diff --git a/src/ast/variables/ThisVariable.ts b/src/ast/variables/ThisVariable.ts index b0fbe27e39a..76ba2127b5a 100644 --- a/src/ast/variables/ThisVariable.ts +++ b/src/ast/variables/ThisVariable.ts @@ -1,18 +1,58 @@ import { AstContext } from '../../Module'; -import { CallOptions } from '../CallOptions'; import { HasEffectsContext } from '../ExecutionContext'; -import { ExpressionEntity } from '../nodes/shared/Expression'; -import { ObjectPath } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../values'; +import { NodeEvent } from '../NodeEvents'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; +import { ObjectPath, SHARED_RECURSION_TRACKER } from '../utils/PathTracker'; import LocalVariable from './LocalVariable'; +interface ThisDeoptimizationEvent { + event: NodeEvent; + path: ObjectPath; + thisParameter: ExpressionEntity; +} + export default class ThisVariable extends LocalVariable { + private deoptimizedPaths: ObjectPath[] = []; + private entitiesToBeDeoptimized = new Set(); + private thisDeoptimizations: ThisDeoptimizationEvent[] = []; + constructor(context: AstContext) { super('this', null, null, context); } - getLiteralValueAtPath(): LiteralValueOrUnknown { - return UnknownValue; + addEntityToBeDeoptimized(entity: ExpressionEntity) { + for (const path of this.deoptimizedPaths) { + entity.deoptimizePath(path); + } + for (const thisDeoptimization of this.thisDeoptimizations) { + this.applyThisDeoptimizationEvent(entity, thisDeoptimization); + } + this.entitiesToBeDeoptimized.add(entity); + } + + deoptimizePath(path: ObjectPath) { + if ( + path.length === 0 || + this.deoptimizationTracker.trackEntityAtPathAndGetIfTracked(path, this) + ) { + return; + } + this.deoptimizedPaths.push(path); + for (const entity of this.entitiesToBeDeoptimized) { + entity.deoptimizePath(path); + } + } + + deoptimizeThisOnEventAtPath(event: NodeEvent, path: ObjectPath, thisParameter: ExpressionEntity) { + const thisDeoptimization: ThisDeoptimizationEvent = { + event, + path, + thisParameter + }; + for (const entity of this.entitiesToBeDeoptimized) { + this.applyThisDeoptimizationEvent(entity, thisDeoptimization); + } + this.thisDeoptimizations.push(thisDeoptimization); } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { @@ -29,14 +69,15 @@ export default class ThisVariable extends LocalVariable { ); } - hasEffectsWhenCalledAtPath( - path: ObjectPath, - callOptions: CallOptions, - context: HasEffectsContext + private applyThisDeoptimizationEvent( + entity: ExpressionEntity, + { event, path, thisParameter }: ThisDeoptimizationEvent ) { - return ( - this.getInit(context).hasEffectsWhenCalledAtPath(path, callOptions, context) || - super.hasEffectsWhenCalledAtPath(path, callOptions, context) + entity.deoptimizeThisOnEventAtPath( + event, + path, + thisParameter === this ? entity : thisParameter, + SHARED_RECURSION_TRACKER ); } diff --git a/src/ast/variables/UndefinedVariable.ts b/src/ast/variables/UndefinedVariable.ts index 32e45285abd..83a8d6220df 100644 --- a/src/ast/variables/UndefinedVariable.ts +++ b/src/ast/variables/UndefinedVariable.ts @@ -1,4 +1,4 @@ -import { LiteralValueOrUnknown } from '../values'; +import { LiteralValueOrUnknown } from '../nodes/shared/Expression'; import Variable from './Variable'; export default class UndefinedVariable extends Variable { diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index 9a3564ff4d7..fef15ed6f25 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -1,30 +1,23 @@ import ExternalModule from '../../ExternalModule'; import Module from '../../Module'; import { RESERVED_NAMES } from '../../utils/reservedNames'; -import { CallOptions } from '../CallOptions'; -import { DeoptimizableEntity } from '../DeoptimizableEntity'; -import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { HasEffectsContext } from '../ExecutionContext'; import Identifier from '../nodes/Identifier'; import { ExpressionEntity } from '../nodes/shared/Expression'; -import { ExpressionNode } from '../nodes/shared/Node'; -import SpreadElement from '../nodes/SpreadElement'; -import { ObjectPath, PathTracker } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../values'; +import { ObjectPath } from '../utils/PathTracker'; -export default class Variable implements ExpressionEntity { +export default class Variable extends ExpressionEntity { alwaysRendered = false; - included = false; isId = false; // both NamespaceVariable and ExternalVariable can be namespaces isNamespace?: boolean; isReassigned = false; module?: Module | ExternalModule; - name: string; renderBaseName: string | null = null; renderName: string | null = null; - constructor(name: string) { - this.name = name; + constructor(public name: string) { + super(); } /** @@ -33,20 +26,10 @@ export default class Variable implements ExpressionEntity { */ addReference(_identifier: Identifier) {} - deoptimizePath(_path: ObjectPath) {} - getBaseVariableName(): string { return this.renderBaseName || this.renderName || this.name; } - getLiteralValueAtPath( - _path: ObjectPath, - _recursionTracker: PathTracker, - _origin: DeoptimizableEntity - ): LiteralValueOrUnknown { - return UnknownValue; - } - getName(): string { const name = this.renderName || this.name; return this.renderBaseName @@ -54,30 +37,10 @@ export default class Variable implements ExpressionEntity { : name; } - getReturnExpressionWhenCalledAtPath( - _path: ObjectPath, - _recursionTracker: PathTracker, - _origin: DeoptimizableEntity - ): ExpressionEntity { - return UNKNOWN_EXPRESSION; - } - hasEffectsWhenAccessedAtPath(path: ObjectPath, _context: HasEffectsContext) { return path.length > 0; } - hasEffectsWhenAssignedAtPath(_path: ObjectPath, _context: HasEffectsContext) { - return true; - } - - hasEffectsWhenCalledAtPath( - _path: ObjectPath, - _callOptions: CallOptions, - _context: HasEffectsContext - ) { - return true; - } - /** * Marks this variable as being part of the bundle, which is usually the case when one of * its identifiers becomes part of the bundle. Returns true if it has not been included @@ -88,18 +51,8 @@ export default class Variable implements ExpressionEntity { this.included = true; } - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - for (const arg of args) { - arg.include(context, false); - } - } - markCalledFromTryStatement() {} - mayModifyThisWhenCalledAtPath(_path: ObjectPath, _recursionTracker: PathTracker) { - return true; - } - setRenderNames(baseName: string | null, name: string | null) { this.renderBaseName = baseName; this.renderName = name; diff --git a/src/rollup/rollup.ts b/src/rollup/rollup.ts index 86519eb5439..b013af1c84f 100644 --- a/src/rollup/rollup.ts +++ b/src/rollup/rollup.ts @@ -170,9 +170,7 @@ async function handleGenerateWrite( message: 'You must specify "output.file" or "output.dir" for the build.' }); } - await Promise.all( - Object.keys(generated).map(chunkId => writeOutputFile(generated[chunkId], outputOptions)) - ); + await Promise.all(Object.values(generated).map(chunk => writeOutputFile(chunk, outputOptions))); await outputPluginDriver.hookParallel('writeBundle', [outputOptions, generated]); } return createOutput(generated); @@ -228,12 +226,9 @@ function getOutputOptions( function createOutput(outputBundle: Record): RollupOutput { return { - output: (Object.keys(outputBundle) - .map(fileName => outputBundle[fileName]) - .filter(outputFile => Object.keys(outputFile).length > 0) as ( - | OutputChunk - | OutputAsset - )[]).sort((outputFileA, outputFileB) => { + output: (Object.values(outputBundle).filter( + outputFile => Object.keys(outputFile).length > 0 + ) as (OutputChunk | OutputAsset)[]).sort((outputFileA, outputFileB) => { const fileTypeA = getSortingFileType(outputFileA); const fileTypeB = getSortingFileType(outputFileB); if (fileTypeA === fileTypeB) return 0; diff --git a/src/utils/FileEmitter.ts b/src/utils/FileEmitter.ts index 65520b91fc3..8f0a3e1abd3 100644 --- a/src/utils/FileEmitter.ts +++ b/src/utils/FileEmitter.ts @@ -106,9 +106,12 @@ function hasValidType( ); } -function hasValidName(emittedFile: { type: 'asset' | 'chunk'; [key: string]: unknown; }): emittedFile is EmittedFile { +function hasValidName(emittedFile: { + type: 'asset' | 'chunk'; + [key: string]: unknown; +}): emittedFile is EmittedFile { const validatedName = emittedFile.fileName || emittedFile.name; - return !validatedName || typeof validatedName === 'string' && !isPathFragment(validatedName); + return !validatedName || (typeof validatedName === 'string' && !isPathFragment(validatedName)); } function getValidSource( @@ -351,8 +354,7 @@ function findExistingAssetFileNameWithSource( bundle: OutputBundleWithPlaceholders, source: string | Uint8Array ): string | null { - for (const fileName of Object.keys(bundle)) { - const outputFile = bundle[fileName]; + for (const [fileName, outputFile] of Object.entries(bundle)) { if (outputFile.type === 'asset' && areSourcesEqual(source, outputFile.source)) return fileName; } return null; diff --git a/src/utils/chunkAssignment.ts b/src/utils/chunkAssignment.ts index b4fb355377c..3490ddaf9ad 100644 --- a/src/utils/chunkAssignment.ts +++ b/src/utils/chunkAssignment.ts @@ -185,8 +185,8 @@ function createChunks( chunkModules[chunkSignature] = [module]; } } - return Object.keys(chunkModules).map(chunkSignature => ({ + return Object.values(chunkModules).map(modules => ({ alias: null, - modules: chunkModules[chunkSignature] + modules })); } diff --git a/src/utils/options/normalizeInputOptions.ts b/src/utils/options/normalizeInputOptions.ts index 649a10c6ff0..52626aff142 100644 --- a/src/utils/options/normalizeInputOptions.ts +++ b/src/utils/options/normalizeInputOptions.ts @@ -187,8 +187,8 @@ const getModuleContext = ( } if (configModuleContext) { const contextByModuleId = Object.create(null); - for (const key of Object.keys(configModuleContext)) { - contextByModuleId[resolve(key)] = configModuleContext[key]; + for (const [key, moduleContext] of Object.entries(configModuleContext)) { + contextByModuleId[resolve(key)] = moduleContext; } return id => contextByModuleId[id] || context; } diff --git a/src/utils/timers.ts b/src/utils/timers.ts index 5f030bbfafb..5ef2f77ba8d 100644 --- a/src/utils/timers.ts +++ b/src/utils/timers.ts @@ -76,8 +76,8 @@ function timeEndImpl(label: string, level = 3) { export function getTimings(): SerializedTimings { const newTimings: SerializedTimings = {}; - for (const label of Object.keys(timers)) { - newTimings[label] = [timers[label].time, timers[label].memory, timers[label].totalMemory]; + for (const [label, { time, memory, totalMemory }] of Object.entries(timers)) { + newTimings[label] = [time, memory, totalMemory]; } return newTimings; } @@ -102,7 +102,7 @@ function getPluginWithTimers(plugin: any, index: number): Plugin { timerLabel += ` (${plugin.name})`; } timerLabel += ` - ${hook}`; - timedPlugin[hook] = function() { + timedPlugin[hook] = function () { timeStart(timerLabel, 4); let result = plugin[hook].apply(this === timedPlugin ? plugin : this, arguments); timeEnd(timerLabel, 4); diff --git a/test/cli/samples/watch/watch-config-no-update/_config.js b/test/cli/samples/watch/watch-config-no-update/_config.js index c93075b0cc3..635d77d42d9 100644 --- a/test/cli/samples/watch/watch-config-no-update/_config.js +++ b/test/cli/samples/watch/watch-config-no-update/_config.js @@ -29,7 +29,7 @@ module.exports = { }, stderr(stderr) { if ( - !/^rollup v\d+\.\d+\.\d+\nbundles main.js → _actual[\\/]main.js...\ncreated _actual[\\/]main.js in \d+ms\n$/.test( + !/^rollup v\d+\.\d+\.\d+(-\d+)?\nbundles main.js → _actual[\\/]main.js...\ncreated _actual[\\/]main.js in \d+ms\n$/.test( stderr ) ) { diff --git a/test/form/samples/array-element-tracking/array-elements/_config.js b/test/form/samples/array-element-tracking/array-elements/_config.js new file mode 100644 index 00000000000..46d8f9b2d3d --- /dev/null +++ b/test/form/samples/array-element-tracking/array-elements/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks elements of arrays' +}; diff --git a/test/form/samples/array-element-tracking/array-elements/_expected.js b/test/form/samples/array-element-tracking/array-elements/_expected.js new file mode 100644 index 00000000000..350400b9918 --- /dev/null +++ b/test/form/samples/array-element-tracking/array-elements/_expected.js @@ -0,0 +1,4 @@ +console.log('retained'); +console.log('retained'); +console.log('retained'); +console.log('retained'); diff --git a/test/form/samples/array-element-tracking/array-elements/main.js b/test/form/samples/array-element-tracking/array-elements/main.js new file mode 100644 index 00000000000..31792cf92ec --- /dev/null +++ b/test/form/samples/array-element-tracking/array-elements/main.js @@ -0,0 +1,10 @@ +const array = [true, false, 3]; + +if (array[0]) console.log('retained'); +else console.log('removed'); +if (array[1]) console.log('removed'); +else console.log('retained'); +if (array[2] === 3) console.log('retained'); +else console.log('removed'); +if (array[3]) console.log('removed'); +else console.log('retained'); diff --git a/test/form/samples/array-element-tracking/array-mutation/_config.js b/test/form/samples/array-element-tracking/array-mutation/_config.js new file mode 100644 index 00000000000..58a8183eebf --- /dev/null +++ b/test/form/samples/array-element-tracking/array-mutation/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks mutations of array elements' +}; diff --git a/test/form/samples/array-element-tracking/array-mutation/_expected.js b/test/form/samples/array-element-tracking/array-mutation/_expected.js new file mode 100644 index 00000000000..b4c4acc5b9a --- /dev/null +++ b/test/form/samples/array-element-tracking/array-mutation/_expected.js @@ -0,0 +1,10 @@ +const array = [true, true]; + +array[1] = false; +array[2] = true; + +console.log('retained'); +if (array[1]) console.log('unimportant'); +else console.log('retained'); +if (array[2]) console.log('retained'); +else console.log('unimportant'); diff --git a/test/form/samples/array-element-tracking/array-mutation/main.js b/test/form/samples/array-element-tracking/array-mutation/main.js new file mode 100644 index 00000000000..37327b20616 --- /dev/null +++ b/test/form/samples/array-element-tracking/array-mutation/main.js @@ -0,0 +1,11 @@ +const array = [true, true]; + +array[1] = false; +array[2] = true; + +if (array[0]) console.log('retained'); +else console.log('removed'); +if (array[1]) console.log('unimportant'); +else console.log('retained'); +if (array[2]) console.log('retained'); +else console.log('unimportant'); diff --git a/test/form/samples/array-element-tracking/array-spread/_config.js b/test/form/samples/array-element-tracking/array-spread/_config.js new file mode 100644 index 00000000000..57b80f7746f --- /dev/null +++ b/test/form/samples/array-element-tracking/array-spread/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks elements of arrays with spread elements' +}; diff --git a/test/form/samples/array-element-tracking/array-spread/_expected.js b/test/form/samples/array-element-tracking/array-spread/_expected.js new file mode 100644 index 00000000000..2d0ec880684 --- /dev/null +++ b/test/form/samples/array-element-tracking/array-spread/_expected.js @@ -0,0 +1,9 @@ +const spread = [true, false]; +const array = [true, false, ...spread]; + +console.log('retained'); +console.log('retained'); +if (array[2]) console.log('retained'); +else console.log('unimportant'); +if (array[3]) console.log('unimportant'); +else console.log('retained'); diff --git a/test/form/samples/array-element-tracking/array-spread/main.js b/test/form/samples/array-element-tracking/array-spread/main.js new file mode 100644 index 00000000000..cf36dae8591 --- /dev/null +++ b/test/form/samples/array-element-tracking/array-spread/main.js @@ -0,0 +1,11 @@ +const spread = [true, false]; +const array = [true, false, ...spread]; + +if (array[0]) console.log('retained'); +else console.log('removed'); +if (array[1]) console.log('removed'); +else console.log('retained'); +if (array[2]) console.log('retained'); +else console.log('unimportant'); +if (array[3]) console.log('unimportant'); +else console.log('retained'); diff --git a/test/form/samples/array-element-tracking/spread-element-deoptimization/_config.js b/test/form/samples/array-element-tracking/spread-element-deoptimization/_config.js new file mode 100644 index 00000000000..b1ce2636a6d --- /dev/null +++ b/test/form/samples/array-element-tracking/spread-element-deoptimization/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'deoptimizes array spread elements' +}; diff --git a/test/form/samples/array-element-tracking/spread-element-deoptimization/_expected.js b/test/form/samples/array-element-tracking/spread-element-deoptimization/_expected.js new file mode 100644 index 00000000000..a2633a81cc0 --- /dev/null +++ b/test/form/samples/array-element-tracking/spread-element-deoptimization/_expected.js @@ -0,0 +1,3 @@ +const spread = [{ effect() {} }]; +[...spread][0].effect = () => console.log('effect'); +spread[0].effect(); diff --git a/test/form/samples/array-element-tracking/spread-element-deoptimization/main.js b/test/form/samples/array-element-tracking/spread-element-deoptimization/main.js new file mode 100644 index 00000000000..a2633a81cc0 --- /dev/null +++ b/test/form/samples/array-element-tracking/spread-element-deoptimization/main.js @@ -0,0 +1,3 @@ +const spread = [{ effect() {} }]; +[...spread][0].effect = () => console.log('effect'); +spread[0].effect(); diff --git a/test/form/samples/arrow-function-return-values/_expected.js b/test/form/samples/arrow-function-return-values/_expected.js index 1bedb0a7bbe..a3fd2df3d87 100644 --- a/test/form/samples/arrow-function-return-values/_expected.js +++ b/test/form/samples/arrow-function-return-values/_expected.js @@ -6,9 +6,5 @@ retained1()(); (() => { return () => console.log( 'effect' ); })()(); - -(() => ({ foo: () => {} }))().foo(); (() => ({ foo: () => console.log( 'effect' ) }))().foo(); - -(() => ({ foo: () => ({ bar: () => ({ baz: () => {} }) }) }))().foo().bar().baz(); (() => ({ foo: () => ({ bar: () => console.log( 'effect' ) }) }))().foo().bar(); diff --git a/test/form/samples/builtin-prototypes/array-expression/_expected.js b/test/form/samples/builtin-prototypes/array-expression/_expected.js index 6d040eeda50..c0eda2afc00 100644 --- a/test/form/samples/builtin-prototypes/array-expression/_expected.js +++ b/test/form/samples/builtin-prototypes/array-expression/_expected.js @@ -1,31 +1,76 @@ -[ 1 ].map( x => console.log( 1 ) ); -[ 1 ].map( x => x ).map( x => console.log( 1 ) ); -[ 1 ].map( x => console.log( 1 ) ).map( x => x ); -[ 1 ].map( x => x ).map( x => x ).map( x => console.log( 1 ) ); -[ 1 ].map( x => x ).map( x => console.log( 1 ) ).map( x => x ); +[1].map(x => console.log(1)); +[1].map(x => x).map(x => console.log(1)); +[1].map(x => console.log(1)).map(x => x); +[1] + .map(x => x) + .map(x => x) + .map(x => console.log(1)); +[1] + .map(x => x) + .map(x => console.log(1)) + .map(x => x); [](); -[ 1 ].every( () => console.log( 1 ) || true ); -[ 1 ].filter( () => console.log( 1 ) || true ); -[ 1 ].find( () => console.log( 1 ) || true ); -[ 1 ].findIndex( () => console.log( 1 ) || true ); -[ 1 ].forEach( () => console.log( 1 ) || true ); -[ 1 ].map( () => console.log( 1 ) || 1 ); -[ 1 ].reduce( () => console.log( 1 ) || 1, 1 ); -[ 1 ].reduceRight( () => console.log( 1 ) || 1, 1 ); -[ 1 ].some( () => console.log( 1 ) || true ); +const _atArray = [{ effect() {} }]; +_atArray.at(0).effect = () => console.log(1); +_atArray[0].effect(); +const _entriesArray = [{ effect() {} }]; +[..._entriesArray.entries()][0][1].effect = () => console.log(1); +_entriesArray[0].effect(); +const _sliceArray = [{ effect() {} }]; +_sliceArray.slice()[0].effect = () => console.log(1); +_sliceArray[0].effect(); +const _valuesArray = [{ effect() {} }]; +[..._valuesArray.values()][0].effect = () => console.log(1); +_valuesArray[0].effect(); +[1].every(() => console.log(1) || true); +const _everyArray = [{ effect() {} }]; +_everyArray.every(element => (element.effect = () => console.log(1))); +_everyArray[0].effect(); +[1].filter(() => console.log(1) || true); +const _filterArray = [{ effect() {} }]; +_filterArray.filter(element => (element.effect = () => console.log(1))); +_filterArray[0].effect(); +[1].find(() => console.log(1) || true); +const _findArray = [{ effect() {} }]; +_findArray.find(element => (element.effect = () => console.log(1))); +_findArray[0].effect(); +[1].findIndex(() => console.log(1) || true); +const _findIndexArray = [{ effect() {} }]; +_findIndexArray.findIndex(element => (element.effect = () => console.log(1))); +_findIndexArray[0].effect(); +[1].forEach(() => console.log(1) || true); +const _forEachArray = [{ effect() {} }]; +_forEachArray.forEach(element => (element.effect = () => console.log(1))); +_forEachArray[0].effect(); +[1].map(() => console.log(1) || 1); +const _mapArray = [{ effect() {} }]; +_mapArray.map(element => (element.effect = () => console.log(1))); +_mapArray[0].effect(); +[1].reduce(() => console.log(1) || 1, 1); +const _reduceArray = [{ effect() {} }]; +_reduceArray.reduce((_, element) => (element.effect = () => console.log(1)), 1); +_reduceArray[0].effect(); +[1].reduceRight(() => console.log(1) || 1, 1); +const _reduceRightArray = [{ effect() {} }]; +_reduceRightArray.reduceRight((_, element) => (element.effect = () => console.log(1)), 1); +_reduceRightArray[0].effect(); +[1].some(() => console.log(1) || true); +const _someArray = [{ effect() {} }]; +_someArray.some(element => (element.effect = () => console.log(1))); +_someArray[0].effect(); // mutator methods -const exported = [ 1 ]; -exported.copyWithin( 0 ); -exported.fill( 0 ); +const exported = [1]; +exported.copyWithin(0); +exported.fill(0); exported.pop(); -exported.push( 0 ); +exported.push(0); exported.reverse(); exported.shift(); -[ 1 ].sort( () => console.log( 1 ) || 0 ); +[1].sort(() => console.log(1) || 0); exported.sort(); -exported.splice( 0 ); -exported.unshift( 0 ); +exported.splice(0); +exported.unshift(0); export { exported }; diff --git a/test/form/samples/builtin-prototypes/array-expression/main.js b/test/form/samples/builtin-prototypes/array-expression/main.js index a04bdfa57e3..0f7b94cf367 100644 --- a/test/form/samples/builtin-prototypes/array-expression/main.js +++ b/test/form/samples/builtin-prototypes/array-expression/main.js @@ -1,73 +1,140 @@ const array = []; -const join1 = array.join( ',' ); -const join2 = [].join( ',' ); -const join3 = [].join( ',' ).trim(); +const join1 = array.join(','); +const join2 = [].join(','); +const join3 = [].join(',').trim(); const length = [].length; -const map1 = [ 1 ].map( x => x ); -const map2 = [ 1 ].map( x => console.log( 1 ) ); -const map3 = [ 1 ].map( x => x ).map( x => x ); -const map4 = [ 1 ].map( x => x ).map( x => console.log( 1 ) ); -const map5 = [ 1 ].map( x => console.log( 1 ) ).map( x => x ); -const map6 = [ 1 ].map( x => x ).map( x => x ).map( x => x ); -const map7 = [ 1 ].map( x => x ).map( x => x ).map( x => console.log( 1 ) ); -const map8 = [ 1 ].map( x => x ).map( x => console.log( 1 ) ).map( x => x ); +const map1 = [1].map(x => x); +const map2 = [1].map(x => console.log(1)); +const map3 = [1].map(x => x).map(x => x); +const map4 = [1].map(x => x).map(x => console.log(1)); +const map5 = [1].map(x => console.log(1)).map(x => x); +const map6 = [1] + .map(x => x) + .map(x => x) + .map(x => x); +const map7 = [1] + .map(x => x) + .map(x => x) + .map(x => console.log(1)); +const map8 = [1] + .map(x => x) + .map(x => console.log(1)) + .map(x => x); [](); // accessor methods -const _includes = [].includes( 1 ).valueOf(); -const _indexOf = [].indexOf( 1 ).toPrecision( 1 ); -const _join = [].join( ',' ).trim(); -const _lastIndexOf = [].lastIndexOf( 1 ).toPrecision( 1 ); -const _slice = [].slice( 1 ).concat( [] ); +const removedTestArray = [{ noEffect() {} }]; +removedTestArray[0].noEffect(); + +const _at = [].at(1); +const _atArray = [{ effect() {} }]; +_atArray.at(0).effect = () => console.log(1); +_atArray[0].effect(); + +const _entries = [].entries(); +const _entriesArray = [{ effect() {} }]; +[..._entriesArray.entries()][0][1].effect = () => console.log(1); +_entriesArray[0].effect(); + +const _includes = [].includes(1).valueOf(); +const _indexOf = [].indexOf(1).toPrecision(1); +const _join = [].join(',').trim(); +const _keys = [].keys(); +const _lastIndexOf = [].lastIndexOf(1).toPrecision(1); + +const _slice = [].slice(1).concat([]); +const _sliceArray = [{ effect() {} }]; +_sliceArray.slice()[0].effect = () => console.log(1); +_sliceArray[0].effect(); + +const _values = [].values(); +const _valuesArray = [{ effect() {} }]; +[..._valuesArray.values()][0].effect = () => console.log(1); +_valuesArray[0].effect(); // iteration methods -const _every = [ 1 ].every( () => true ).valueOf(); -const _everyEffect = [ 1 ].every( () => console.log( 1 ) || true ); -const _filter = [ 1 ].filter( () => true ).join( ',' ); -const _filterEffect = [ 1 ].filter( () => console.log( 1 ) || true ); -const _find = [ 1 ].find( () => true ); -const _findEffect = [ 1 ].find( () => console.log( 1 ) || true ); -const _findIndex = [ 1 ].findIndex( () => true ).toPrecision( 1 ); -const _findIndexEffect = [ 1 ].findIndex( () => console.log( 1 ) || true ); -const _forEach = [ 1 ].forEach( () => {} ); -const _forEachEffect = [ 1 ].forEach( () => console.log( 1 ) || true ); -const _map = [ 1 ].map( () => 1 ).join( ',' ); -const _mapEffect = [ 1 ].map( () => console.log( 1 ) || 1 ); -const _reduce = [ 1 ].reduce( () => 1, 1 ); -const _reduceEffect = [ 1 ].reduce( () => console.log( 1 ) || 1, 1 ); -const _reduceRight = [ 1 ].reduceRight( () => 1, 1 ); -const _reduceRightEffect = [ 1 ].reduceRight( () => console.log( 1 ) || 1, 1 ); -const _some = [ 1 ].some( () => true ).valueOf(); -const _someEffect = [ 1 ].some( () => console.log( 1 ) || true ); +const _every = [1].every(() => true).valueOf(); +const _everyEffect = [1].every(() => console.log(1) || true); +const _everyArray = [{ effect() {} }]; +_everyArray.every(element => (element.effect = () => console.log(1))); +_everyArray[0].effect(); + +const _filter = [1].filter(() => true).join(','); +const _filterEffect = [1].filter(() => console.log(1) || true); +const _filterArray = [{ effect() {} }]; +_filterArray.filter(element => (element.effect = () => console.log(1))); +_filterArray[0].effect(); + +const _find = [1].find(() => true); +const _findEffect = [1].find(() => console.log(1) || true); +const _findArray = [{ effect() {} }]; +_findArray.find(element => (element.effect = () => console.log(1))); +_findArray[0].effect(); + +const _findIndex = [1].findIndex(() => true).toPrecision(1); +const _findIndexEffect = [1].findIndex(() => console.log(1) || true); +const _findIndexArray = [{ effect() {} }]; +_findIndexArray.findIndex(element => (element.effect = () => console.log(1))); +_findIndexArray[0].effect(); + +const _forEach = [1].forEach(() => {}); +const _forEachEffect = [1].forEach(() => console.log(1) || true); +const _forEachArray = [{ effect() {} }]; +_forEachArray.forEach(element => (element.effect = () => console.log(1))); +_forEachArray[0].effect(); + +const _map = [1].map(() => 1).join(','); +const _mapEffect = [1].map(() => console.log(1) || 1); +const _mapArray = [{ effect() {} }]; +_mapArray.map(element => (element.effect = () => console.log(1))); +_mapArray[0].effect(); + +const _reduce = [1].reduce(() => 1, 1); +const _reduceEffect = [1].reduce(() => console.log(1) || 1, 1); +const _reduceArray = [{ effect() {} }]; +_reduceArray.reduce((_, element) => (element.effect = () => console.log(1)), 1); +_reduceArray[0].effect(); + +const _reduceRight = [1].reduceRight(() => 1, 1); +const _reduceRightEffect = [1].reduceRight(() => console.log(1) || 1, 1); +const _reduceRightArray = [{ effect() {} }]; +_reduceRightArray.reduceRight((_, element) => (element.effect = () => console.log(1)), 1); +_reduceRightArray[0].effect(); + +const _some = [1].some(() => true).valueOf(); +const _someEffect = [1].some(() => console.log(1) || true); +const _someArray = [{ effect() {} }]; +_someArray.some(element => (element.effect = () => console.log(1))); +_someArray[0].effect(); // mutator methods -export const exported = [ 1 ]; +export const exported = [1]; -const _copyWithin = [ 1 ].copyWithin( 0 ).join( ',' ); -exported.copyWithin( 0 ); -const _fill = [ 1 ].fill( 0 ).join( ',' ); -exported.fill( 0 ); -const _pop = [ 1 ].pop(); +const _copyWithin = [1].copyWithin(0).join(','); +exported.copyWithin(0); +const _fill = [1].fill(0).join(','); +exported.fill(0); +const _pop = [1].pop(); exported.pop(); -const _push = [ 1 ].push( 0 ).toPrecision( 1 ); -exported.push( 0 ); -const _reverse = [ 1 ].reverse().join( ',' ); +const _push = [1].push(0).toPrecision(1); +exported.push(0); +const _reverse = [1].reverse().join(','); exported.reverse(); -const _shift = [ 1 ].shift(); +const _shift = [1].shift(); exported.shift(); -const _sort = [ 1 ].sort( () => 0 ).join( ',' ); -const _sortEffect = [ 1 ].sort( () => console.log( 1 ) || 0 ); +const _sort = [1].sort(() => 0).join(','); +const _sortEffect = [1].sort(() => console.log(1) || 0); exported.sort(); -const _splice = [ 1 ].splice( 0 ).join( ',' ); -exported.splice( 0 ); -const _unshift = [ 1 ].unshift( 0 ).toPrecision( 1 ); -exported.unshift( 0 ); +const _splice = [1].splice(0).join(','); +exported.splice(0); +const _unshift = [1].unshift(0).toPrecision(1); +exported.unshift(0); // inherited -const _hasOwnProperty = [ 1 ].hasOwnProperty( 'toString' ).valueOf(); -const _isPrototypeOf = [ 1 ].isPrototypeOf( [] ).valueOf(); -const _propertyIsEnumerable = [ 1 ].propertyIsEnumerable( 'toString' ).valueOf(); -const _toLocaleString = [ 1 ].toLocaleString().trim(); -const _toString = [ 1 ].toString().trim(); -const _valueOf = [ 1 ].valueOf(); +const _hasOwnProperty = [1].hasOwnProperty('toString').valueOf(); +const _isPrototypeOf = [1].isPrototypeOf([]).valueOf(); +const _propertyIsEnumerable = [1].propertyIsEnumerable('toString').valueOf(); +const _toLocaleString = [1].toLocaleString().trim(); +const _toString = [1].toString().trim(); +const _valueOf = [1].valueOf(); diff --git a/test/form/samples/builtin-prototypes/literal/_expected.js b/test/form/samples/builtin-prototypes/literal/_expected.js index a2c2161b645..75d80d652e5 100644 --- a/test/form/samples/builtin-prototypes/literal/_expected.js +++ b/test/form/samples/builtin-prototypes/literal/_expected.js @@ -1,3 +1,13 @@ +// retained +true.valueOf().unknown.unknown(); +true.valueOf()(); +(1).valueOf().unknown.unknown(); +(1).valueOf().unknown(); +(1).valueOf()[globalThis.unknown](); +(1).valueOf()(); +'ab'.charAt(1).unknown.unknown(); +'ab'.charAt(1)(); +null.unknown; 'ab'.replace( 'a', () => console.log( 1 ) || 'b' ); // deep property access is forbidden diff --git a/test/form/samples/builtin-prototypes/literal/main.js b/test/form/samples/builtin-prototypes/literal/main.js index edba0b1ee4f..e91c6a56144 100644 --- a/test/form/samples/builtin-prototypes/literal/main.js +++ b/test/form/samples/builtin-prototypes/literal/main.js @@ -4,25 +4,16 @@ const valueOf2 = true.valueOf(); const valueOf3 = true.valueOf().valueOf(); const valueOf4 = true.valueOf().valueOf().valueOf(); -const number = 1; -const toExponential1 = number.toExponential( 2 ); -const toExponential2 = (1).toExponential( 2 ); -const toExponential3 = (1).toExponential( 2 ).trim(); - -const string = ' b '; -const trim1 = string.trim(); -const trim2 = ' x '.trim(); -const trim3 = ' x '.trim().trim(); -const trim4 = ' x '.trim().trim().trim(); - -// boolean prototype -const _booleanValueOf = true.valueOf().valueOf(); -// inherited -const _booleanHasOwnProperty = true.hasOwnProperty( 'toString' ).valueOf(); -const _booleanIsPrototypeOf = true.isPrototypeOf( true ).valueOf(); -const _booleanPropertyIsEnumerable = true.propertyIsEnumerable( 'toString' ).valueOf(); -const _booleanToLocaleString = true.toLocaleString().trim(); -const _booleanToString = true.toString().trim(); +// retained +true.valueOf().unknown.unknown(); +true.valueOf()(); +(1).valueOf().unknown.unknown(); +(1).valueOf().unknown(); +(1).valueOf()[globalThis.unknown](); +(1).valueOf()(); +'ab'.charAt(1).unknown.unknown(); +'ab'.charAt(1)(); +null.unknown; // number prototype const _toExponential = (1).toExponential( 2 ).trim(); @@ -56,7 +47,7 @@ const _replace = 'ab'.replace( 'a', () => 'b' ).trim(); const _replaceEffect = 'ab'.replace( 'a', () => console.log( 1 ) || 'b' ); const _search = 'ab'.search( /a/ ).toExponential( 2 ); const _slice = 'ab'.slice( 0, 1 ).trim(); -const _split = 'ab'.split( 'a' ).join(); +const _split = 'ab'.split( 'a' ); const _startsWith = 'ab'.startsWith( 'a' ).valueOf(); const _substr = 'ab'.substr( 0, 1 ).trim(); const _substring = 'ab'.substring( 0, 1 ).trim(); diff --git a/test/form/samples/builtin-prototypes/object-expression/_expected.js b/test/form/samples/builtin-prototypes/object-expression/_expected.js index 95b55b099ae..e69de29bb2d 100644 --- a/test/form/samples/builtin-prototypes/object-expression/_expected.js +++ b/test/form/samples/builtin-prototypes/object-expression/_expected.js @@ -1,11 +0,0 @@ -const object = {}; -object.propertyIsEnumerable( 'toString' ); -({}).propertyIsEnumerable( 'toString' ); -({}).propertyIsEnumerable( 'toString' ).valueOf(); - -({}).hasOwnProperty( 'toString' ).valueOf(); -({}).isPrototypeOf( {} ).valueOf(); -({}).propertyIsEnumerable( 'toString' ).valueOf(); -({}).toLocaleString().trim(); -({}).toString().trim(); -({}).valueOf(); diff --git a/test/form/samples/class-method-access/_config.js b/test/form/samples/class-method-access/_config.js new file mode 100644 index 00000000000..d44dede48ae --- /dev/null +++ b/test/form/samples/class-method-access/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks side-effects when accessing class methods' +}; diff --git a/test/form/samples/class-method-access/_expected.js b/test/form/samples/class-method-access/_expected.js new file mode 100644 index 00000000000..034e5c35fe7 --- /dev/null +++ b/test/form/samples/class-method-access/_expected.js @@ -0,0 +1,16 @@ +console.log('retained'); + +class Used { + static method() {} + static get getter() { + return { foo: {} }; + } +} +Used.method.doesNotExist.throws; +Used.getter.doesNotExist.throws; +Used.getter.foo.doesNotExist.throws; +Used.getter.throws(); +Used.getter.foo.throws(); + +Used.method.reassigned = 1; +Used.getter.reassigned = 2; diff --git a/test/form/samples/class-method-access/main.js b/test/form/samples/class-method-access/main.js new file mode 100644 index 00000000000..25e980dc120 --- /dev/null +++ b/test/form/samples/class-method-access/main.js @@ -0,0 +1,34 @@ +class Removed { + static get isTrue() { + return true; + } +} + +if (Removed.isTrue) console.log('retained'); +else console.log('removed'); + +class Used { + static method() {} + static get getter() { + return { foo: {} }; + } +} + +Used.method.doesNotExist; +Used.method.doesNotExist.throws; +Used.getter.doesNotExist; +Used.getter.doesNotExist.throws; +Used.getter.foo; +Used.getter.foo.doesNotExist; +Used.getter.foo.doesNotExist.throws; +Used.getter.hasOwnProperty('foo'); +Used.getter.foo.hasOwnProperty('bar'); +Used.getter.throws(); +Used.getter.foo.throws(); + +Used.method.reassigned = 1; +Used.getter.reassigned = 2; + +class ValueEffect { + static foo +} \ No newline at end of file diff --git a/test/form/samples/conditional-expression-paths/_expected.js b/test/form/samples/conditional-expression-paths/_expected.js index f78090cebcb..ae60637a8b9 100644 --- a/test/form/samples/conditional-expression-paths/_expected.js +++ b/test/form/samples/conditional-expression-paths/_expected.js @@ -1,22 +1,11 @@ var unknownValue = globalThis.unknown(); var foo = { x: () => {}, y: {} }; -var bar = { x: () => {}, y: {} }; var baz = { x: () => console.log('effect') }; -// unknown branch without side-effects -(unknownValue ? foo : bar).y.z; -(unknownValue ? foo : bar).x(); - // unknown branch with side-effect (unknownValue ? foo : baz).y.z; (unknownValue ? foo : baz).x(); -// known branch without side-effects -(foo ).y.z; -(foo).y.z; -(foo ).x(); -(foo).x(); - // known branch with side-effect (baz ).y.z; (baz).y.z; diff --git a/test/form/samples/conditional-expression-paths/main.js b/test/form/samples/conditional-expression-paths/main.js index 67d2162a495..bfafbf296c1 100644 --- a/test/form/samples/conditional-expression-paths/main.js +++ b/test/form/samples/conditional-expression-paths/main.js @@ -19,8 +19,8 @@ var d3 = (false ? baz : foo).x(); var foo2 = { y: {} }; var baz2 = {}; -(true ? foo2 : baz).y.z = 1; -(false ? baz : foo2).y.z = 1; +(true ? foo2 : baz2).y.z = 1; +(false ? baz2 : foo2).y.z = 1; // known branch with side-effect var a4 = (true ? baz : foo).y.z; diff --git a/test/form/samples/function-body-return-values/_expected.js b/test/form/samples/function-body-return-values/_expected.js index cd4f77d4ce6..0064a3f4c4a 100644 --- a/test/form/samples/function-body-return-values/_expected.js +++ b/test/form/samples/function-body-return-values/_expected.js @@ -1,9 +1,3 @@ -function removed3 () { - return { x: () => {} }; -} - -removed3().x(); - function retained1 () { return () => console.log( 'effect' ); } diff --git a/test/form/samples/literals-from-class-statics/_config.js b/test/form/samples/literals-from-class-statics/_config.js new file mode 100644 index 00000000000..0847b183aac --- /dev/null +++ b/test/form/samples/literals-from-class-statics/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks literal values in class static fields' +}; diff --git a/test/form/samples/literals-from-class-statics/_expected.js b/test/form/samples/literals-from-class-statics/_expected.js new file mode 100644 index 00000000000..09ce1e861ac --- /dev/null +++ b/test/form/samples/literals-from-class-statics/_expected.js @@ -0,0 +1,13 @@ +log("t"); +log("x"); + +class Undef { + static 'y'; +} +if (Undef.y) log("y"); + +class Deopt { + static z = false; +} +unknown(Deopt); +if (Deopt.z) log("z"); diff --git a/test/form/samples/literals-from-class-statics/main.js b/test/form/samples/literals-from-class-statics/main.js new file mode 100644 index 00000000000..a8cdcb481d4 --- /dev/null +++ b/test/form/samples/literals-from-class-statics/main.js @@ -0,0 +1,21 @@ +class Static { + static t() { return true; } + static f() { return false; } + static x = 10; +} + +if (Static.t()) log("t"); +if (Static.f()) log("f"); +if (!Static.t()) log("!t"); +if (Static.x) log("x"); + +class Undef { + static 'y'; +} +if (Undef.y) log("y"); + +class Deopt { + static z = false; +} +unknown(Deopt); +if (Deopt.z) log("z"); diff --git a/test/form/samples/minimal-this-mutation/_config.js b/test/form/samples/minimal-this-mutation/_config.js new file mode 100644 index 00000000000..d0cecf5de3d --- /dev/null +++ b/test/form/samples/minimal-this-mutation/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'mutates "this" only if necessary on function calls' +}; diff --git a/test/form/samples/minimal-this-mutation/_expected.js b/test/form/samples/minimal-this-mutation/_expected.js new file mode 100644 index 00000000000..bf850340e03 --- /dev/null +++ b/test/form/samples/minimal-this-mutation/_expected.js @@ -0,0 +1,48 @@ +console.log('retained'); + +const obj2 = { + noMutationEffect() { + console.log('effect'); + }, + prop: true +}; +obj2.noMutationEffect(); +console.log('retained'); + +const obj3 = { + mutateProp() { + this.prop = false; + }, + prop: true +}; +obj3.mutateProp(); +if (obj3.prop) console.log('unimportant'); +else console.log('retained'); + +const obj4 = { + mutateUnknownProp() { + this[globalThis.unknown] = false; + }, + prop: true +}; +obj4.mutateUnknownProp(); +if (obj4.prop) console.log('retained'); +else console.log('retained'); + +const obj5 = { + mutateNestedProp() { + this.nested.prop = false; + }, + nested: { + prop: true + } +}; +obj5.mutateNestedProp(); +if (obj5.nested.prop) console.log('unimportant'); +else console.log('retained'); + +const obj6 = { + prop: true +}; +obj6.doesNotExist(); +console.log('retained'); diff --git a/test/form/samples/minimal-this-mutation/main.js b/test/form/samples/minimal-this-mutation/main.js new file mode 100644 index 00000000000..a70c3d521e5 --- /dev/null +++ b/test/form/samples/minimal-this-mutation/main.js @@ -0,0 +1,56 @@ +const obj1 = { + noMutation() {}, + prop: true +}; +obj1.noMutation(); +if (obj1.prop) console.log('retained'); +else console.log('removed'); + +const obj2 = { + noMutationEffect() { + console.log('effect'); + }, + prop: true +}; +obj2.noMutationEffect(); +if (obj2.prop) console.log('retained'); +else console.log('removed'); + +const obj3 = { + mutateProp() { + this.prop = false; + }, + prop: true +}; +obj3.mutateProp(); +if (obj3.prop) console.log('unimportant'); +else console.log('retained'); + +const obj4 = { + mutateUnknownProp() { + this[globalThis.unknown] = false; + }, + prop: true +}; +obj4.mutateUnknownProp(); +if (obj4.prop) console.log('retained'); +else console.log('retained'); + +const obj5 = { + mutateNestedProp() { + this.nested.prop = false; + }, + nested: { + prop: true + } +}; +obj5.mutateNestedProp(); +if (obj5.nested.prop) console.log('unimportant'); +else console.log('retained'); + +const obj6 = { + prop: true +}; +obj6.doesNotExist(); +if (obj6.prop) console.log('retained'); +else console.log('removed'); diff --git a/test/form/samples/modify-class-prototype/_expected.js b/test/form/samples/modify-class-prototype/_expected.js index 61e4f3c329f..8dd53679723 100644 --- a/test/form/samples/modify-class-prototype/_expected.js +++ b/test/form/samples/modify-class-prototype/_expected.js @@ -3,4 +3,12 @@ Retained.prop = 42; Retained.prototype.prop = 43; Retained.prototype.prop2 = Retained.prototype.prop; +class RetainedSuper { + set prop(v) { + console.log('effect', v); + } +} +class RetainedWithSuper extends RetainedSuper {} +RetainedWithSuper.prototype.prop = 42; + export { Retained }; diff --git a/test/form/samples/modify-class-prototype/main.js b/test/form/samples/modify-class-prototype/main.js index 38f8fa9abf0..1bf13d724dc 100644 --- a/test/form/samples/modify-class-prototype/main.js +++ b/test/form/samples/modify-class-prototype/main.js @@ -7,3 +7,18 @@ export class Retained {} Retained.prop = 42; Retained.prototype.prop = 43; Retained.prototype.prop2 = Retained.prototype.prop; + +class RemovedSuper { + prop() {} +} +class RemovedWithSuper extends RemovedSuper {} +RemovedWithSuper.prototype.prop = 42; +RemovedWithSuper.prototype.prop2 = 43; + +class RetainedSuper { + set prop(v) { + console.log('effect', v); + } +} +class RetainedWithSuper extends RetainedSuper {} +RetainedWithSuper.prototype.prop = 42; diff --git a/test/form/samples/namespace-missing-export-effects/_config.js b/test/form/samples/namespace-missing-export-effects/_config.js new file mode 100644 index 00000000000..87a35ab8485 --- /dev/null +++ b/test/form/samples/namespace-missing-export-effects/_config.js @@ -0,0 +1,4 @@ +module.exports = { + description: 'handles interacting with missing namespace members', + expectedWarnings: ['MISSING_EXPORT'] +}; diff --git a/test/form/samples/namespace-missing-export-effects/_expected.js b/test/form/samples/namespace-missing-export-effects/_expected.js new file mode 100644 index 00000000000..8895a217864 --- /dev/null +++ b/test/form/samples/namespace-missing-export-effects/_expected.js @@ -0,0 +1,5 @@ +if (!undefined) console.log(1); +if (undefined()) console.log(2); +const foo = undefined; +foo.bar; +(0, undefined)(); diff --git a/test/form/samples/namespace-missing-export-effects/main.js b/test/form/samples/namespace-missing-export-effects/main.js new file mode 100644 index 00000000000..e37bad3f8ac --- /dev/null +++ b/test/form/samples/namespace-missing-export-effects/main.js @@ -0,0 +1,7 @@ +import * as ns from './other.js'; + +if (!ns.foo) console.log(1); +if (ns.foo()) console.log(2); +const foo = ns.foo; +foo.bar; +(true && ns.foo)(); diff --git a/test/form/samples/namespace-missing-export-effects/other.js b/test/form/samples/namespace-missing-export-effects/other.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/form/samples/namespace-optimization/_expected/es.js b/test/form/samples/namespace-optimization/_expected.js similarity index 100% rename from test/form/samples/namespace-optimization/_expected/es.js rename to test/form/samples/namespace-optimization/_expected.js diff --git a/test/form/samples/namespace-optimization/_expected/amd.js b/test/form/samples/namespace-optimization/_expected/amd.js deleted file mode 100644 index 95231f1878b..00000000000 --- a/test/form/samples/namespace-optimization/_expected/amd.js +++ /dev/null @@ -1,7 +0,0 @@ -define(function () { 'use strict'; - - function a () {} - - console.log( a() ); - -}); diff --git a/test/form/samples/namespace-optimization/_expected/cjs.js b/test/form/samples/namespace-optimization/_expected/cjs.js deleted file mode 100644 index 33a83e92c84..00000000000 --- a/test/form/samples/namespace-optimization/_expected/cjs.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -function a () {} - -console.log( a() ); diff --git a/test/form/samples/namespace-optimization/_expected/iife.js b/test/form/samples/namespace-optimization/_expected/iife.js deleted file mode 100644 index 64673c93d43..00000000000 --- a/test/form/samples/namespace-optimization/_expected/iife.js +++ /dev/null @@ -1,8 +0,0 @@ -(function () { - 'use strict'; - - function a () {} - - console.log( a() ); - -}()); diff --git a/test/form/samples/namespace-optimization/_expected/system.js b/test/form/samples/namespace-optimization/_expected/system.js deleted file mode 100644 index dacc31b2984..00000000000 --- a/test/form/samples/namespace-optimization/_expected/system.js +++ /dev/null @@ -1,12 +0,0 @@ -System.register([], function () { - 'use strict'; - return { - execute: function () { - - function a () {} - - console.log( a() ); - - } - }; -}); diff --git a/test/form/samples/namespace-optimization/_expected/umd.js b/test/form/samples/namespace-optimization/_expected/umd.js deleted file mode 100644 index 2163f63c851..00000000000 --- a/test/form/samples/namespace-optimization/_expected/umd.js +++ /dev/null @@ -1,10 +0,0 @@ -(function (factory) { - typeof define === 'function' && define.amd ? define(factory) : - factory(); -}((function () { 'use strict'; - - function a () {} - - console.log( a() ); - -}))); diff --git a/test/form/samples/namespace-optimization/bar.js b/test/form/samples/namespace-optimization/bar.js index aa96676744d..ad92a1a3d6b 100644 --- a/test/form/samples/namespace-optimization/bar.js +++ b/test/form/samples/namespace-optimization/bar.js @@ -1,3 +1,3 @@ import * as quux from './quux'; -export { quux }; +export { quux }; diff --git a/test/form/samples/namespace-optimization/foo.js b/test/form/samples/namespace-optimization/foo.js index 42a93ae6fe9..d7132d920d3 100644 --- a/test/form/samples/namespace-optimization/foo.js +++ b/test/form/samples/namespace-optimization/foo.js @@ -1,3 +1,3 @@ import * as bar from './bar'; -export { bar }; +export { bar }; diff --git a/test/form/samples/nested-deoptimization/_config.js b/test/form/samples/nested-deoptimization/_config.js new file mode 100644 index 00000000000..440f76d3543 --- /dev/null +++ b/test/form/samples/nested-deoptimization/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles deoptimization of nested properties' +}; diff --git a/test/form/samples/nested-deoptimization/_expected.js b/test/form/samples/nested-deoptimization/_expected.js new file mode 100644 index 00000000000..04bb1a1620c --- /dev/null +++ b/test/form/samples/nested-deoptimization/_expected.js @@ -0,0 +1,11 @@ +const obj = { + foo: { prop: true }, + bar: { otherProp: true }, + prop: true +}; +obj[globalThis.unknown].prop = false; + +if (obj.foo.prop) console.log('retained'); +else console.log('also retained'); +console.log('retained'); +console.log('retained'); diff --git a/test/form/samples/nested-deoptimization/main.js b/test/form/samples/nested-deoptimization/main.js new file mode 100644 index 00000000000..df9ff8d41f7 --- /dev/null +++ b/test/form/samples/nested-deoptimization/main.js @@ -0,0 +1,13 @@ +const obj = { + foo: { prop: true }, + bar: { otherProp: true }, + prop: true +}; +obj[globalThis.unknown].prop = false; + +if (obj.foo.prop) console.log('retained'); +else console.log('also retained'); +if (obj.bar.otherProp) console.log('retained'); +else console.log('removed'); +if (obj.prop) console.log('retained'); +else console.log('removed'); diff --git a/test/form/samples/nested-member-access/main.js b/test/form/samples/nested-member-access/main.js index 323ca9e7885..6a46a73687e 100644 --- a/test/form/samples/nested-member-access/main.js +++ b/test/form/samples/nested-member-access/main.js @@ -4,16 +4,9 @@ const removedResult1 = removed1.foo; const removed2 = { foo: {} }; const removedResult2 = removed2.foo.bar; -const removed3 = new function () {}(); -const removedResult3 = removed3.foo; - const removed4 = !{}; const removedResult4 = removed4.foo; -let removed5a; -const removed5b = removed5a = {}; -const removedResult5 = removed5b.foo; - const removed6 = 1 + 2; const removedResult6 = removed6.foo; diff --git a/test/form/samples/object-expression/return-expressions/_expected.js b/test/form/samples/object-expression/return-expressions/_expected.js index 0405706b107..3538f6d9b82 100644 --- a/test/form/samples/object-expression/return-expressions/_expected.js +++ b/test/form/samples/object-expression/return-expressions/_expected.js @@ -12,8 +12,6 @@ const z = { z.a()(); const v = {}; - -v.toString().charCodeAt(0); // removed v.toString().doesNotExist(0); // retained const w = { diff --git a/test/form/samples/property-setters-and-getters/shadowed-setters/_config.js b/test/form/samples/property-setters-and-getters/shadowed-setters/_config.js new file mode 100644 index 00000000000..f220613b295 --- /dev/null +++ b/test/form/samples/property-setters-and-getters/shadowed-setters/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles setters shadowed by computed setters' +}; diff --git a/test/form/samples/property-setters-and-getters/shadowed-setters/_expected.js b/test/form/samples/property-setters-and-getters/shadowed-setters/_expected.js new file mode 100644 index 00000000000..2758779af18 --- /dev/null +++ b/test/form/samples/property-setters-and-getters/shadowed-setters/_expected.js @@ -0,0 +1,8 @@ +const objRetained = { + set value(v) {}, + set [globalThis.unknown](v) { + console.log('effect'); + } +}; + +objRetained.value = 'retained'; diff --git a/test/form/samples/property-setters-and-getters/shadowed-setters/main.js b/test/form/samples/property-setters-and-getters/shadowed-setters/main.js new file mode 100644 index 00000000000..e5b1a9baf01 --- /dev/null +++ b/test/form/samples/property-setters-and-getters/shadowed-setters/main.js @@ -0,0 +1,17 @@ +const objRemoved = { + set value(v) { + console.log('shadowed'); + }, + set value(v) {} +}; + +objRemoved.value = 'removed'; + +const objRetained = { + set value(v) {}, + set [globalThis.unknown](v) { + console.log('effect'); + } +}; + +objRetained.value = 'retained'; diff --git a/test/form/samples/recursive-calls/_expected.js b/test/form/samples/recursive-calls/_expected.js index 00f0e5d1be2..b5e78683edd 100644 --- a/test/form/samples/recursive-calls/_expected.js +++ b/test/form/samples/recursive-calls/_expected.js @@ -1,13 +1,3 @@ -const removed4 = () => globalThis.unknown ? removed4() : { x: () => {} }; -removed4().x(); - -const removed8 = { - get x () { - return globalThis.unknown ? removed8.x : { y: () => {} }; - } -}; -removed8.x.y(); - const retained1 = () => globalThis.unknown ? retained1() : console.log( 'effect' ); retained1(); diff --git a/test/form/samples/recursive-calls/main.js b/test/form/samples/recursive-calls/main.js index 89bae2a7738..306972ec96b 100644 --- a/test/form/samples/recursive-calls/main.js +++ b/test/form/samples/recursive-calls/main.js @@ -17,13 +17,6 @@ const removed5 = { }; removed5.x; -const removed6 = { - get x () { - return globalThis.unknown ? removed6.x : () => {}; - } -}; -removed6.x(); - const removed7 = { get x () { return globalThis.unknown ? removed7.x : {}; diff --git a/test/form/samples/side-effects-class-getters-setters/_config.js b/test/form/samples/side-effects-class-getters-setters/_config.js new file mode 100644 index 00000000000..201e3e082f9 --- /dev/null +++ b/test/form/samples/side-effects-class-getters-setters/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'treat getters and setters on classes as function calls' +}; diff --git a/test/form/samples/side-effects-class-getters-setters/_expected.js b/test/form/samples/side-effects-class-getters-setters/_expected.js new file mode 100644 index 00000000000..c09724d4c6a --- /dev/null +++ b/test/form/samples/side-effects-class-getters-setters/_expected.js @@ -0,0 +1,38 @@ +class RetainedByGetter { + get a() { log(); } +} +RetainedByGetter.prototype.a; + +class RetainedBySetter { + set a(v) { log(); } +} +RetainedBySetter.prototype.a = 10; + +class RetainedByStaticGetter { + static get a() { log(); } +} +RetainedByStaticGetter.a; + +class RetainedByStaticSetter { + static set a(v) { log(); } +} +RetainedByStaticSetter.a = 10; + +class RetainedSuper { + static get a() { log(); } +} +class RetainedSub extends RetainedSuper {} +RetainedSub.a; +log(); + +class DeoptProto { + a = true; +} +globalThis.unknown(DeoptProto.prototype); +if (!DeoptProto.prototype.a) log(); + +class DeoptComputed { + static get a() {} + static get [globalThis.unknown]() { log(); } +} +DeoptComputed.a; diff --git a/test/form/samples/side-effects-class-getters-setters/main.js b/test/form/samples/side-effects-class-getters-setters/main.js new file mode 100644 index 00000000000..9a8c95b93cb --- /dev/null +++ b/test/form/samples/side-effects-class-getters-setters/main.js @@ -0,0 +1,78 @@ +class Removed { + get a() { log(); } + set a(v) { log(); } + static get a() { log(); } + static set a(v) { log(); } +} + +class RemovedNoEffect { + get a() {} + set a(v) {} + static get a() {} + static set a(v) {} +} +RemovedNoEffect.prototype.a; +RemovedNoEffect.prototype.a = 1; +RemovedNoEffect.a; +RemovedNoEffect.a = 1; + +class RetainedByGetter { + get a() { log(); } +} +RetainedByGetter.prototype.a; + +class RetainedBySetter { + set a(v) { log(); } +} +RetainedBySetter.prototype.a = 10; + +class RetainedByStaticGetter { + static get a() { log(); } +} +RetainedByStaticGetter.a; + +class RetainedByStaticSetter { + static set a(v) { log(); } +} +RetainedByStaticSetter.a = 10; + +class RemovedSetters { + set a(v) { log(); } + static set a(v) { log(); } +} +RemovedSetters.prototype.a; +RemovedSetters.a; + +class RemovedWrongProp { + get a() { log(); } + static get a() { log(); } +} +RemovedWrongProp.prototype.b +RemovedWrongProp.b + +class RetainedSuper { + static get a() { log(); } +} +class RetainedSub extends RetainedSuper {} +RetainedSub.a; + +class RemovedSub extends RetainedSuper {} +RemovedSub.b; + +// class fields are not part of the prototype +class RemovedProtoValue { + a = true; +} +if (!RemovedProtoValue.prototype.a) log(); + +class DeoptProto { + a = true; +} +globalThis.unknown(DeoptProto.prototype); +if (!DeoptProto.prototype.a) log(); + +class DeoptComputed { + static get a() {} + static get [globalThis.unknown]() { log(); } +} +DeoptComputed.a; diff --git a/test/form/samples/side-effects-getters-and-setters/_expected.js b/test/form/samples/side-effects-getters-and-setters/_expected.js index 4100b6ca31b..31917077c8e 100644 --- a/test/form/samples/side-effects-getters-and-setters/_expected.js +++ b/test/form/samples/side-effects-getters-and-setters/_expected.js @@ -1,34 +1,36 @@ -const retained1a = { - get effect () { - console.log( 'effect' ); +const retained1 = { + get effect() { + console.log('effect'); }, - get noEffect () { + get noEffect() { const x = 1; return x; } }; -retained1a.effect; -retained1a[ 'eff' + 'ect' ]; + +//retained +retained1.effect; +retained1['eff' + 'ect']; const retained3 = { - set effect ( value ) { - console.log( value ); + set effect(value) { + console.log(value); } }; retained3.effect = 'retained'; const retained4 = { - set effect ( value ) { - console.log( value ); + set effect(value) { + console.log(value); } }; -retained4[ 'eff' + 'ect' ] = 'retained'; +retained4['eff' + 'ect'] = 'retained'; const retained7 = { foo: () => {}, - get foo () { + get foo() { return 1; } }; diff --git a/test/form/samples/side-effects-getters-and-setters/main.js b/test/form/samples/side-effects-getters-and-setters/main.js index b3ce3d4a9b6..66cb31f8451 100644 --- a/test/form/samples/side-effects-getters-and-setters/main.js +++ b/test/form/samples/side-effects-getters-and-setters/main.js @@ -1,68 +1,60 @@ -const retained1a = { - get effect () { - console.log( 'effect' ); +const retained1 = { + get effect() { + console.log('effect'); }, - get noEffect () { + get noEffect() { const x = 1; return x; } }; -const removed1 = retained1a.noEffect; -const retained1b = retained1a.effect; -const retained1c = retained1a[ 'eff' + 'ect' ]; +// removed +retained1.noEffect; -const removed2a = { - get shadowedEffect () { - console.log( 'effect' ); +//retained +retained1.effect; +retained1['eff' + 'ect']; + +const removed2 = { + get shadowedEffect() { + console.log('effect'); return 1; }, shadowedEffect: true, - set shadowedEffect ( value ) { - console.log( value ); + set shadowedEffect(value) { + console.log(value); } }; -const removed2b = removed2a.shadowedEffect; -const removed2c = removed2a.missingProp; +removed2.shadowedEffect; const retained3 = { - set effect ( value ) { - console.log( value ); + set effect(value) { + console.log(value); } }; retained3.effect = 'retained'; const retained4 = { - set effect ( value ) { - console.log( value ); + set effect(value) { + console.log(value); } }; -retained4[ 'eff' + 'ect' ] = 'retained'; +retained4['eff' + 'ect'] = 'retained'; const removed5 = { - set noEffect ( value ) { + set noEffect(value) { const x = value; } }; removed5.noEffect = 'removed'; -const removed6 = { - set shadowedEffect ( value ) { - console.log( value ); - }, - shadowedEffect: true -}; - -removed6.shadowedEffect = true; -removed6.missingProp = true; - const retained7 = { foo: () => {}, - get foo () { + get foo() { return 1; } }; diff --git a/test/form/samples/side-effects-object-literal-mutation/_config.js b/test/form/samples/side-effects-object-literal-mutation/_config.js index 2eaf74d0b0b..ffc52c259c1 100644 --- a/test/form/samples/side-effects-object-literal-mutation/_config.js +++ b/test/form/samples/side-effects-object-literal-mutation/_config.js @@ -1,4 +1,3 @@ module.exports = { - description: 'detects side-effects when mutating object literals', - options: { output: { name: 'bundle' } } + description: 'detects side-effects when mutating object literals' }; diff --git a/test/form/samples/side-effects-static-methods/_config.js b/test/form/samples/side-effects-static-methods/_config.js new file mode 100644 index 00000000000..129d0e1d6de --- /dev/null +++ b/test/form/samples/side-effects-static-methods/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'allow calls to pure static methods to be tree-shaken' +}; diff --git a/test/form/samples/side-effects-static-methods/_expected.js b/test/form/samples/side-effects-static-methods/_expected.js new file mode 100644 index 00000000000..3b12efac3e3 --- /dev/null +++ b/test/form/samples/side-effects-static-methods/_expected.js @@ -0,0 +1,38 @@ +class Effect { + static a() { log(); } +} +Effect.a(); + +class DeoptComputed { + static a() {} + static [foo]() { log(); } +} +DeoptComputed.a(); + +class DeoptGetter { + static a() {} + static get a() {} +} +DeoptGetter.a(); + +class DeoptAssign { + static a() {} +} +DeoptAssign.a = log; +DeoptAssign.a(); + +class DeoptFully { + static a() {} +} +unknown(DeoptFully); +DeoptFully.a(); + +class DeepAssign { + static a = {} + a = {} +} +DeepAssign.a.b = 1; +DeepAssign.prototype.a.b = 1; + +class DynamicAssign {} +DynamicAssign[foo()] = 1; diff --git a/test/form/samples/side-effects-static-methods/main.js b/test/form/samples/side-effects-static-methods/main.js new file mode 100644 index 00000000000..f2d3e1036b4 --- /dev/null +++ b/test/form/samples/side-effects-static-methods/main.js @@ -0,0 +1,43 @@ +class NoEffect { + static a() {} +} +NoEffect.a(); + +class Effect { + static a() { log(); } +} +Effect.a(); + +class DeoptComputed { + static a() {} + static [foo]() { log(); } +} +DeoptComputed.a(); + +class DeoptGetter { + static a() {} + static get a() {} +} +DeoptGetter.a(); + +class DeoptAssign { + static a() {} +} +DeoptAssign.a = log; +DeoptAssign.a(); + +class DeoptFully { + static a() {} +} +unknown(DeoptFully); +DeoptFully.a(); + +class DeepAssign { + static a = {} + a = {} +} +DeepAssign.a.b = 1; +DeepAssign.prototype.a.b = 1; + +class DynamicAssign {} +DynamicAssign[foo()] = 1; diff --git a/test/form/samples/skips-dead-branches/_expected/es.js b/test/form/samples/skips-dead-branches/_expected.js similarity index 100% rename from test/form/samples/skips-dead-branches/_expected/es.js rename to test/form/samples/skips-dead-branches/_expected.js diff --git a/test/form/samples/skips-dead-branches/_expected/amd.js b/test/form/samples/skips-dead-branches/_expected/amd.js deleted file mode 100644 index c192900373e..00000000000 --- a/test/form/samples/skips-dead-branches/_expected/amd.js +++ /dev/null @@ -1,9 +0,0 @@ -define(function () { 'use strict'; - - function bar () { - console.log( 'this should be included' ); - } - - bar(); - -}); diff --git a/test/form/samples/skips-dead-branches/_expected/cjs.js b/test/form/samples/skips-dead-branches/_expected/cjs.js deleted file mode 100644 index b3e974bb4e6..00000000000 --- a/test/form/samples/skips-dead-branches/_expected/cjs.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -function bar () { - console.log( 'this should be included' ); -} - -bar(); diff --git a/test/form/samples/skips-dead-branches/_expected/iife.js b/test/form/samples/skips-dead-branches/_expected/iife.js deleted file mode 100644 index 3b894d92a46..00000000000 --- a/test/form/samples/skips-dead-branches/_expected/iife.js +++ /dev/null @@ -1,10 +0,0 @@ -(function () { - 'use strict'; - - function bar () { - console.log( 'this should be included' ); - } - - bar(); - -}()); diff --git a/test/form/samples/skips-dead-branches/_expected/system.js b/test/form/samples/skips-dead-branches/_expected/system.js deleted file mode 100644 index 606d00fb15e..00000000000 --- a/test/form/samples/skips-dead-branches/_expected/system.js +++ /dev/null @@ -1,14 +0,0 @@ -System.register([], function () { - 'use strict'; - return { - execute: function () { - - function bar () { - console.log( 'this should be included' ); - } - - bar(); - - } - }; -}); diff --git a/test/form/samples/skips-dead-branches/_expected/umd.js b/test/form/samples/skips-dead-branches/_expected/umd.js deleted file mode 100644 index 06df11106d6..00000000000 --- a/test/form/samples/skips-dead-branches/_expected/umd.js +++ /dev/null @@ -1,12 +0,0 @@ -(function (factory) { - typeof define === 'function' && define.amd ? define(factory) : - factory(); -}((function () { 'use strict'; - - function bar () { - console.log( 'this should be included' ); - } - - bar(); - -}))); diff --git a/test/form/samples/static-class-property-calls/_config.js b/test/form/samples/static-class-property-calls/_config.js new file mode 100644 index 00000000000..2983907efde --- /dev/null +++ b/test/form/samples/static-class-property-calls/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles effects when calling static class properties' +}; diff --git a/test/form/samples/static-class-property-calls/_expected.js b/test/form/samples/static-class-property-calls/_expected.js new file mode 100644 index 00000000000..e2589637c16 --- /dev/null +++ b/test/form/samples/static-class-property-calls/_expected.js @@ -0,0 +1,21 @@ +class Foo { + static isTrue = () => true; + static noEffect = () => {}; + static effect = () => console.log('effect'); + static missing; +} + +console.log('retained'); +Foo.effect(); +Foo.missing(); + +class Bar { + static flag = false + static mutate = function() { + this.flag = true; + } +} + +Bar.mutate(); +if (Bar.flag) console.log('retained'); +else console.log('unimportant'); diff --git a/test/form/samples/static-class-property-calls/main.js b/test/form/samples/static-class-property-calls/main.js new file mode 100644 index 00000000000..4259adcdb71 --- /dev/null +++ b/test/form/samples/static-class-property-calls/main.js @@ -0,0 +1,23 @@ +class Foo { + static isTrue = () => true; + static noEffect = () => {}; + static effect = () => console.log('effect'); + static missing; +} + +if (Foo.isTrue()) console.log('retained'); +else console.log('removed'); +Foo.noEffect(); +Foo.effect(); +Foo.missing(); + +class Bar { + static flag = false + static mutate = function() { + this.flag = true; + } +} + +Bar.mutate(); +if (Bar.flag) console.log('retained'); +else console.log('unimportant'); diff --git a/test/form/samples/super-classes/super-class-prototype-access/_config.js b/test/form/samples/super-classes/super-class-prototype-access/_config.js new file mode 100644 index 00000000000..6bac45041bb --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-access/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'correctly resolves the prototype of the super class when accessing properties' +}; diff --git a/test/form/samples/super-classes/super-class-prototype-access/_expected.js b/test/form/samples/super-classes/super-class-prototype-access/_expected.js new file mode 100644 index 00000000000..936fab7c5a4 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-access/_expected.js @@ -0,0 +1,8 @@ +class SuperAccess { + prop = {}; + method() {} +} +class Access extends SuperAccess {} +Access.prototype.doesNoExist.throws; +Access.prototype.method.doesNoExist.throws; +Access.prototype.prop.throws; diff --git a/test/form/samples/super-classes/super-class-prototype-access/main.js b/test/form/samples/super-classes/super-class-prototype-access/main.js new file mode 100644 index 00000000000..fcf38d4ead6 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-access/main.js @@ -0,0 +1,11 @@ +class SuperAccess { + prop = {}; + method() {} +} +class Access extends SuperAccess {} + +Access.prototype.doesNoExist; +Access.prototype.doesNoExist.throws; +Access.prototype.method.doesNoExist; +Access.prototype.method.doesNoExist.throws; +Access.prototype.prop.throws; diff --git a/test/form/samples/super-classes/super-class-prototype-assignment/_config.js b/test/form/samples/super-classes/super-class-prototype-assignment/_config.js new file mode 100644 index 00000000000..dd974952e7f --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-assignment/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'correctly resolves the prototype of the super class when assigning properites' +}; diff --git a/test/form/samples/super-classes/super-class-prototype-assignment/_expected.js b/test/form/samples/super-classes/super-class-prototype-assignment/_expected.js new file mode 100644 index 00000000000..840f9a4d6c1 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-assignment/_expected.js @@ -0,0 +1,31 @@ +class SuperUsedAssign { + method() {} +} +class UsedAssign extends SuperUsedAssign {} +UsedAssign.prototype.doesNotExist = 1; +UsedAssign.prototype.method.doesNotExist = 1; +console.log(UsedAssign); + +class SuperAssign1 {} +class Assign1 extends SuperAssign1 {} +Assign1.prototype.doesNotExist.throws = 1; + +class SuperAssign2 { + prop = {}; +} +class Assign2 extends SuperAssign2 {} +Assign2.prototype.prop.throws = 1; + +class SuperAssign3 { + method() {} +} +class Assign3 extends SuperAssign3 {} +Assign3.prototype.method.doesNotExist.throws = 1; + +class SuperAssign4 { + set prop(v) { + console.log('effect', v); + } +} +class Assign4 extends SuperAssign4 {} +Assign4.prototype.prop = 1; diff --git a/test/form/samples/super-classes/super-class-prototype-assignment/main.js b/test/form/samples/super-classes/super-class-prototype-assignment/main.js new file mode 100644 index 00000000000..d47b557daf5 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-assignment/main.js @@ -0,0 +1,40 @@ +class SuperRemovedAssign { + method() {} + set prop(v) {} +} +class RemovedAssign extends SuperRemovedAssign {} +RemovedAssign.prototype.doesNotExist = 1; +RemovedAssign.prototype.method.doesNotExist = 1; +RemovedAssign.prototype.prop = 1; + +class SuperUsedAssign { + method() {} +} +class UsedAssign extends SuperUsedAssign {} +UsedAssign.prototype.doesNotExist = 1; +UsedAssign.prototype.method.doesNotExist = 1; +console.log(UsedAssign); + +class SuperAssign1 {} +class Assign1 extends SuperAssign1 {} +Assign1.prototype.doesNotExist.throws = 1; + +class SuperAssign2 { + prop = {}; +} +class Assign2 extends SuperAssign2 {} +Assign2.prototype.prop.throws = 1; + +class SuperAssign3 { + method() {} +} +class Assign3 extends SuperAssign3 {} +Assign3.prototype.method.doesNotExist.throws = 1; + +class SuperAssign4 { + set prop(v) { + console.log('effect', v); + } +} +class Assign4 extends SuperAssign4 {} +Assign4.prototype.prop = 1; diff --git a/test/form/samples/super-classes/super-class-prototype-calls/_config.js b/test/form/samples/super-classes/super-class-prototype-calls/_config.js new file mode 100644 index 00000000000..f0d4045a4f9 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-calls/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'correctly resolves the prototype of the super class when calling properties' +}; diff --git a/test/form/samples/super-classes/super-class-prototype-calls/_expected.js b/test/form/samples/super-classes/super-class-prototype-calls/_expected.js new file mode 100644 index 00000000000..478810e2b22 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-calls/_expected.js @@ -0,0 +1,23 @@ +class SuperValues { + get prop() { + return { + effect(used) { + console.log('effect', used); + }, + isTrue() { + return true; + } + }; + } + effect(used) { + console.log('effect', used); + } + isTrue() { + return true; + } +} +class Values extends SuperValues {} +console.log('retained'); +console.log('retained'); +Values.prototype.effect(); +Values.prototype.prop.effect(); diff --git a/test/form/samples/super-classes/super-class-prototype-calls/main.js b/test/form/samples/super-classes/super-class-prototype-calls/main.js new file mode 100644 index 00000000000..43c62658ce6 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-calls/main.js @@ -0,0 +1,25 @@ +class SuperValues { + get prop() { + return { + effect(used) { + console.log('effect', used); + }, + isTrue() { + return true; + } + }; + } + effect(used) { + console.log('effect', used); + } + isTrue() { + return true; + } +} +class Values extends SuperValues {} +if (Values.prototype.isTrue()) console.log('retained'); +else console.log('removed'); +if (Values.prototype.prop.isTrue()) console.log('retained'); +else console.log('removed'); +Values.prototype.effect(); +Values.prototype.prop.effect(); diff --git a/test/form/samples/super-classes/super-class-prototype-values/_config.js b/test/form/samples/super-classes/super-class-prototype-values/_config.js new file mode 100644 index 00000000000..0f3f274db77 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-values/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'correctly resolves the prototype of the super class when reading property values' +}; diff --git a/test/form/samples/super-classes/super-class-prototype-values/_expected.js b/test/form/samples/super-classes/super-class-prototype-values/_expected.js new file mode 100644 index 00000000000..09f0c0d87ce --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-values/_expected.js @@ -0,0 +1,15 @@ +console.log('retained'); +console.log('retained'); +// Note that isTrueProp is not part of the prototype +console.log('retained'); + +const prop = { isTrue: true }; +class SuperDeopt { + get prop() { + return prop; + } +} +class Deopt extends SuperDeopt {} +Deopt.prototype.prop.isTrue = false; +if (Deopt.prototype.prop.isTrue) console.log('unimportant'); +else console.log('retained'); diff --git a/test/form/samples/super-classes/super-class-prototype-values/main.js b/test/form/samples/super-classes/super-class-prototype-values/main.js new file mode 100644 index 00000000000..0fb92a2f737 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-values/main.js @@ -0,0 +1,28 @@ +class SuperValues { + isTrueProp = true; + get isTrue() { + return true; + } + get prop() { + return { isTrue: true }; + } +} +class Values extends SuperValues {} +if (Values.prototype.isTrue) console.log('retained'); +else console.log('removed'); +if (Values.prototype.prop.isTrue) console.log('retained'); +else console.log('removed'); +// Note that isTrueProp is not part of the prototype +if (Values.prototype.isTrueProp) console.log('removed'); +else console.log('retained'); + +const prop = { isTrue: true }; +class SuperDeopt { + get prop() { + return prop; + } +} +class Deopt extends SuperDeopt {} +Deopt.prototype.prop.isTrue = false; +if (Deopt.prototype.prop.isTrue) console.log('unimportant'); +else console.log('retained'); diff --git a/test/form/samples/this-in-class-body/_config.js b/test/form/samples/this-in-class-body/_config.js new file mode 100644 index 00000000000..1ade8977cfe --- /dev/null +++ b/test/form/samples/this-in-class-body/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'uses the correct "this" value in class properties' +}; diff --git a/test/form/samples/this-in-class-body/_expected.js b/test/form/samples/this-in-class-body/_expected.js new file mode 100644 index 00000000000..ff7c88e7839 --- /dev/null +++ b/test/form/samples/this-in-class-body/_expected.js @@ -0,0 +1,37 @@ +class Used { + static flag = false + static mutate = () => { + this.flag = true; + } +} +Used.mutate(); + +if (Used.flag) console.log('retained'); +else console.log('unimportant'); + +class InstanceMutation { + static flag = false + flag = false + mutate = () => { + this.flag = true; + } +} +(new InstanceMutation).mutate(); + +console.log('retained'); + +class UsedSuper { + static flag = false +} +class UsedWithSuper extends UsedSuper{ + static mutate = () => { + super.flag = true; + } +} +UsedWithSuper.mutate(); + +if (UsedWithSuper.flag) console.log('retained'); +else console.log('unimportant'); + +// Assignments via "super" do NOT mutate the super class! +console.log('retained'); diff --git a/test/form/samples/this-in-class-body/main.js b/test/form/samples/this-in-class-body/main.js new file mode 100644 index 00000000000..6fa4fc5ed65 --- /dev/null +++ b/test/form/samples/this-in-class-body/main.js @@ -0,0 +1,47 @@ +class Unused { + static flag = false + static mutate = () => { + this.flag = true; + } +} +Unused.mutate(); + +class Used { + static flag = false + static mutate = () => { + this.flag = true; + } +} +Used.mutate(); + +if (Used.flag) console.log('retained'); +else console.log('unimportant'); + +class InstanceMutation { + static flag = false + flag = false + mutate = () => { + this.flag = true; + } +} +(new InstanceMutation).mutate(); + +if (InstanceMutation.flag) console.log('removed'); +else console.log('retained'); + +class UsedSuper { + static flag = false +} +class UsedWithSuper extends UsedSuper{ + static mutate = () => { + super.flag = true; + } +} +UsedWithSuper.mutate(); + +if (UsedWithSuper.flag) console.log('retained'); +else console.log('unimportant'); + +// Assignments via "super" do NOT mutate the super class! +if (UsedSuper.flag) console.log('removed'); +else console.log('retained'); diff --git a/test/form/samples/undefined-properties/_config.js b/test/form/samples/undefined-properties/_config.js index a89e4af5ef0..29b3cc673cb 100644 --- a/test/form/samples/undefined-properties/_config.js +++ b/test/form/samples/undefined-properties/_config.js @@ -1,4 +1,3 @@ module.exports = { - description: 'detects undefined properties', - expectedWarnings: ['EMPTY_BUNDLE'] + description: 'detects undefined properties' }; diff --git a/test/form/samples/undefined-properties/_expected.js b/test/form/samples/undefined-properties/_expected.js index 8b137891791..f97408e6838 100644 --- a/test/form/samples/undefined-properties/_expected.js +++ b/test/form/samples/undefined-properties/_expected.js @@ -1 +1,10 @@ +var a = { + b: { + c: 'd' + } +}; +console.log('retained'); + +if (a.c.d) console.log('retained for effect'); +else console.log('retained for effect'); diff --git a/test/form/samples/undefined-properties/main.js b/test/form/samples/undefined-properties/main.js index e7df0af9514..bf2ad33721a 100644 --- a/test/form/samples/undefined-properties/main.js +++ b/test/form/samples/undefined-properties/main.js @@ -4,5 +4,8 @@ var a = { } }; -if (a.b.d) - console.log('yes'); \ No newline at end of file +if (a.b.d) console.log('removed'); +else console.log('retained'); + +if (a.c.d) console.log('retained for effect'); +else console.log('retained for effect'); \ No newline at end of file diff --git a/test/function/samples/access-instance-prop/_config.js b/test/function/samples/access-instance-prop/_config.js new file mode 100644 index 00000000000..98b5968822b --- /dev/null +++ b/test/function/samples/access-instance-prop/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'respects getters when accessing properties of an instance' +}; diff --git a/test/function/samples/access-instance-prop/main.js b/test/function/samples/access-instance-prop/main.js new file mode 100644 index 00000000000..69b8f8f7946 --- /dev/null +++ b/test/function/samples/access-instance-prop/main.js @@ -0,0 +1,13 @@ +let effect = false; + +var b = { + get a() { + effect = true; + } +}; + +function X() {} +X.prototype = b; +new X().a; + +assert.ok(effect); diff --git a/test/function/samples/array-getter/_config.js b/test/function/samples/array-getter/_config.js new file mode 100644 index 00000000000..e0437548252 --- /dev/null +++ b/test/function/samples/array-getter/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles getters defined on arrays' +}; diff --git a/test/function/samples/array-getter/main.js b/test/function/samples/array-getter/main.js new file mode 100644 index 00000000000..9ac16a7b6e6 --- /dev/null +++ b/test/function/samples/array-getter/main.js @@ -0,0 +1,14 @@ +let flag = false; +const array = []; + +Object.defineProperty(array, 'prop', { + get() { + flag = true; + } +}); + +array.prop; + +if (!flag) { + throw new Error('Mutation not detected'); +} diff --git a/test/function/samples/array-mutation/_config.js b/test/function/samples/array-mutation/_config.js new file mode 100644 index 00000000000..612a42d2e21 --- /dev/null +++ b/test/function/samples/array-mutation/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks array mutations' +}; diff --git a/test/function/samples/array-mutation/main.js b/test/function/samples/array-mutation/main.js new file mode 100644 index 00000000000..98363f0f469 --- /dev/null +++ b/test/function/samples/array-mutation/main.js @@ -0,0 +1,4 @@ +const array = []; +array.push(true); + +assert.strictEqual(array[0] || false, true); diff --git a/test/function/samples/conditionals-deoptimization/_config.js b/test/function/samples/conditionals-deoptimization/_config.js new file mode 100644 index 00000000000..48ed684f81d --- /dev/null +++ b/test/function/samples/conditionals-deoptimization/_config.js @@ -0,0 +1,8 @@ +const assert = require('assert'); + +module.exports = { + description: 'handles deoptimization of conditionals', + exports(exports) { + assert.deepStrictEqual(exports, { first: true, second: true, third: true, fourth: true }); + } +}; diff --git a/test/function/samples/conditionals-deoptimization/main.js b/test/function/samples/conditionals-deoptimization/main.js new file mode 100644 index 00000000000..324e03b344e --- /dev/null +++ b/test/function/samples/conditionals-deoptimization/main.js @@ -0,0 +1,21 @@ +export let first = false; +export let second = false; +export let third = false; +export let fourth = false; + +let flag = false; +checkConditional(); +checkLogical(); +flag = true; +checkConditional(); +checkLogical(); + +function checkConditional() { + if (flag ? true : false) first = true; + else second = true; +} + +function checkLogical() { + if (flag && true) third = true; + else fourth = true; +} diff --git a/test/function/samples/deoptimize-assumes-getters/_config.js b/test/function/samples/deoptimize-assumes-getters/_config.js new file mode 100644 index 00000000000..c04d33fbc9b --- /dev/null +++ b/test/function/samples/deoptimize-assumes-getters/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'assume full deoptimization may inject side-effectful getters' +}; diff --git a/test/function/samples/deoptimize-assumes-getters/main.js b/test/function/samples/deoptimize-assumes-getters/main.js new file mode 100644 index 00000000000..c11b529a8ff --- /dev/null +++ b/test/function/samples/deoptimize-assumes-getters/main.js @@ -0,0 +1,14 @@ +const obj = {}; +addGetter(obj); + +let mutated = false; +obj.prop; +assert.ok(mutated); + +function addGetter(o) { + Object.defineProperty(o, 'prop', { + get() { + mutated = true; + } + }); +} diff --git a/test/function/samples/deoptimize-class/_config.js b/test/function/samples/deoptimize-class/_config.js new file mode 100644 index 00000000000..14bcdb722ee --- /dev/null +++ b/test/function/samples/deoptimize-class/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles deoptimization of static class properties' +}; diff --git a/test/function/samples/deoptimize-class/main.js b/test/function/samples/deoptimize-class/main.js new file mode 100644 index 00000000000..f67b5d66413 --- /dev/null +++ b/test/function/samples/deoptimize-class/main.js @@ -0,0 +1,20 @@ +let trueProp = 'old'; +let falseProp = 'new'; + +function getClass() { + return class { + static [trueProp]() { + return true; + } + static [falseProp]() { + return false; + } + }; +} + +if (!getClass().old()) throw new Error('old missing'); +if (getClass().new()) throw new Error('new present'); +trueProp = 'new'; +falseProp = 'old'; +if (getClass().old()) throw new Error('old present'); +if (!getClass().new()) throw new Error('new missing'); diff --git a/test/function/samples/deoptimize-computed-class-keys/_config.js b/test/function/samples/deoptimize-computed-class-keys/_config.js new file mode 100644 index 00000000000..53e617bd7fe --- /dev/null +++ b/test/function/samples/deoptimize-computed-class-keys/_config.js @@ -0,0 +1,4 @@ +module.exports = { + description: 'deoptimizes computed class property keys', + minNodeVersion: 12 +}; diff --git a/test/function/samples/deoptimize-computed-class-keys/main.js b/test/function/samples/deoptimize-computed-class-keys/main.js new file mode 100644 index 00000000000..bad340b2305 --- /dev/null +++ b/test/function/samples/deoptimize-computed-class-keys/main.js @@ -0,0 +1,17 @@ +var key1 = 'x'; +var key2 = 'y'; + +changeKeys(); + +class Foo { + static [key1] = true; + static [key2] = false; +} + +assert.strictEqual(Foo.x, false); +assert.strictEqual(Foo.y, true); + +function changeKeys() { + key1 = 'y'; + key2 = 'x'; +} diff --git a/test/function/samples/deoptimize-computed-keys/_config.js b/test/function/samples/deoptimize-computed-keys/_config.js new file mode 100644 index 00000000000..2ac75a24810 --- /dev/null +++ b/test/function/samples/deoptimize-computed-keys/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'deoptimizes computed object property keys' +}; diff --git a/test/function/samples/deoptimize-computed-keys/main.js b/test/function/samples/deoptimize-computed-keys/main.js new file mode 100644 index 00000000000..273087a4e6a --- /dev/null +++ b/test/function/samples/deoptimize-computed-keys/main.js @@ -0,0 +1,14 @@ +var key1 = 'x' +var key2 = 'y' + +changeKeys(); + +var foo = { [key1]: true, [key2]: false }; + +assert.strictEqual(foo.x, false); +assert.strictEqual(foo.y, true); + +function changeKeys() { + key1 = 'y' + key2 = 'x' +} \ No newline at end of file diff --git a/test/function/samples/deoptimize-object/_config.js b/test/function/samples/deoptimize-object/_config.js new file mode 100644 index 00000000000..2906b6780d7 --- /dev/null +++ b/test/function/samples/deoptimize-object/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles deoptimization of object properties' +}; diff --git a/test/function/samples/deoptimize-object/main.js b/test/function/samples/deoptimize-object/main.js new file mode 100644 index 00000000000..bfd1b80873e --- /dev/null +++ b/test/function/samples/deoptimize-object/main.js @@ -0,0 +1,13 @@ +let prop = 'old'; + +function getObj() { + return { + [prop]: true + }; +} + +if (!getObj().old) throw new Error('old missing'); +if (getObj().new) throw new Error('new present'); +prop = 'new'; +if (getObj().old) throw new Error('old present'); +if (!getObj().new) throw new Error('new missing'); diff --git a/test/function/samples/deoptimize-this-parameters/_config.js b/test/function/samples/deoptimize-this-parameters/_config.js new file mode 100644 index 00000000000..518f65d290a --- /dev/null +++ b/test/function/samples/deoptimize-this-parameters/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles deoptimization of this parameters' +}; diff --git a/test/function/samples/deoptimize-this-parameters/main.js b/test/function/samples/deoptimize-this-parameters/main.js new file mode 100644 index 00000000000..89792dd4aea --- /dev/null +++ b/test/function/samples/deoptimize-this-parameters/main.js @@ -0,0 +1,21 @@ +const obj = { + flag: false, + otherFlag: false, + otherProp() { + this.otherFlag = true; + }, + prop() { + this.flag = true; + this.otherProp(); + } +}; + +const otherObj = { + prop: obj.prop, + otherProp: obj.otherProp +}; + +obj.prop(); +otherObj.prop(); +if (!obj.flag) throw new Error('first flag missing'); +if (!otherObj.flag) throw new Error('second flag missing'); diff --git a/test/function/samples/modify-object-via-this-d/_config.js b/test/function/samples/modify-object-via-this-d/_config.js new file mode 100644 index 00000000000..0c4423de923 --- /dev/null +++ b/test/function/samples/modify-object-via-this-d/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks object modification via "this"' +}; diff --git a/test/function/samples/modify-object-via-this-d/main.js b/test/function/samples/modify-object-via-this-d/main.js new file mode 100644 index 00000000000..55adf0b053b --- /dev/null +++ b/test/function/samples/modify-object-via-this-d/main.js @@ -0,0 +1,13 @@ +const obj = { + modify() {} +}; + +obj.modify = modify; + +function modify() { + this.modified = true; +} + +obj.modify(); + +assert.strictEqual(obj.modified ? 'MODIFIED' : 'BROKEN', 'MODIFIED'); diff --git a/test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/_config.js b/test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/_config.js new file mode 100644 index 00000000000..4b7c9b3ebb3 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles fully deoptimized objects', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/main.js b/test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/main.js new file mode 100644 index 00000000000..def94e30b12 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/main.js @@ -0,0 +1,17 @@ +import { unknown } from 'external'; + +const obj = { + flag: false +}; + +Object.defineProperty(obj, unknown, { + get() { + this.flag = true; + } +}); + +obj.prop; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/getter-in-assignment/_config.js b/test/function/samples/modify-this-via-getter/getter-in-assignment/_config.js new file mode 100644 index 00000000000..4f3496ac6bc --- /dev/null +++ b/test/function/samples/modify-this-via-getter/getter-in-assignment/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles known getters that are triggered via an assignment' +}; diff --git a/test/function/samples/modify-this-via-getter/getter-in-assignment/main.js b/test/function/samples/modify-this-via-getter/getter-in-assignment/main.js new file mode 100644 index 00000000000..4244e759d89 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/getter-in-assignment/main.js @@ -0,0 +1,14 @@ +const obj = { + flag: false, + get prop() { + this.flag = true; + return 1; + }, + set prop(v) {} +}; + +obj.prop += 1; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/getters-on-this/_config.js b/test/function/samples/modify-this-via-getter/getters-on-this/_config.js new file mode 100644 index 00000000000..6bd8c9213a8 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/getters-on-this/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles calling getters on "this"', +}; diff --git a/test/function/samples/modify-this-via-getter/getters-on-this/main.js b/test/function/samples/modify-this-via-getter/getters-on-this/main.js new file mode 100644 index 00000000000..24f3b72fdbd --- /dev/null +++ b/test/function/samples/modify-this-via-getter/getters-on-this/main.js @@ -0,0 +1,39 @@ +const obj = { + flag1: false, + flag2: false, + flag3: false, + flag4: false, + prop1() { + this.flag1 = true; + this.prop2(); + }, + prop2() { + this.flag2 = true; + this.prop3; + }, + get prop3() { + this.flag3 = true; + this.prop4 = true; + }, + set prop4(value) { + this.flag4 = value; + } +}; + +obj.prop1(); + +if (!obj.flag1) { + throw new Error('mutation 1 not detected'); +} + +if (!obj.flag2) { + throw new Error('mutation 2 not detected'); +} + +if (!obj.flag3) { + throw new Error('mutation 3 not detected'); +} + +if (!obj.flag4) { + throw new Error('mutation 4 not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/known-getter/_config.js b/test/function/samples/modify-this-via-getter/known-getter/_config.js new file mode 100644 index 00000000000..b608017f8e2 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/known-getter/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles known getters that modify "this"' +}; diff --git a/test/function/samples/modify-this-via-getter/known-getter/main.js b/test/function/samples/modify-this-via-getter/known-getter/main.js new file mode 100644 index 00000000000..9fa5e548b09 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/known-getter/main.js @@ -0,0 +1,12 @@ +const obj = { + flag: false, + get prop() { + this.flag = true; + } +}; + +obj.prop; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/known-super-prop/_config.js b/test/function/samples/modify-this-via-getter/known-super-prop/_config.js new file mode 100644 index 00000000000..52280f050d4 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/known-super-prop/_config.js @@ -0,0 +1,4 @@ +module.exports = { + description: 'handles getters that modify "this" on prototypes for known properties', + minNodeVersion: 12 +}; diff --git a/test/function/samples/modify-this-via-getter/known-super-prop/main.js b/test/function/samples/modify-this-via-getter/known-super-prop/main.js new file mode 100644 index 00000000000..ca4cd5a954c --- /dev/null +++ b/test/function/samples/modify-this-via-getter/known-super-prop/main.js @@ -0,0 +1,14 @@ +class proto { + static flag = false; + static get prop() { + this.flag = true; + } +} + +class obj extends proto {} + +obj.prop; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/unknown-prop-getter/_config.js b/test/function/samples/modify-this-via-getter/unknown-prop-getter/_config.js new file mode 100644 index 00000000000..4af96c05a70 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-prop-getter/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles unknown getters that modify "this"', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-getter/unknown-prop-getter/main.js b/test/function/samples/modify-this-via-getter/unknown-prop-getter/main.js new file mode 100644 index 00000000000..c8829e86915 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-prop-getter/main.js @@ -0,0 +1,14 @@ +import { unknown } from 'external'; + +const obj = { + get [unknown]() { + this.flag = true; + }, + flag: false +}; + +obj.prop; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/_config.js b/test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/_config.js new file mode 100644 index 00000000000..884460d181b --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles unknown getters that modify "this" for unknown property access', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/main.js b/test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/main.js new file mode 100644 index 00000000000..40846145270 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/main.js @@ -0,0 +1,14 @@ +import { unknown } from 'external'; + +const obj = { + get [unknown]() { + this.flag = true; + }, + flag: false +}; + +obj[unknown]; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/unknown-property-access/_config.js b/test/function/samples/modify-this-via-getter/unknown-property-access/_config.js new file mode 100644 index 00000000000..98690a2ad8d --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-property-access/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles getters that modify "this" for unknown property access', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-getter/unknown-property-access/main.js b/test/function/samples/modify-this-via-getter/unknown-property-access/main.js new file mode 100644 index 00000000000..0c790f0347f --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-property-access/main.js @@ -0,0 +1,14 @@ +import { unknown } from 'external'; + +const obj = { + flag: false, + get prop() { + this.flag = true; + } +}; + +obj[unknown]; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/unknown-super-prop/_config.js b/test/function/samples/modify-this-via-getter/unknown-super-prop/_config.js new file mode 100644 index 00000000000..7e4188cbd13 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-super-prop/_config.js @@ -0,0 +1,12 @@ +module.exports = { + description: 'handles getters that modify "this" on prototypes for unknown properties', + minNodeVersion: 12, + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-getter/unknown-super-prop/main.js b/test/function/samples/modify-this-via-getter/unknown-super-prop/main.js new file mode 100644 index 00000000000..9530c344172 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-super-prop/main.js @@ -0,0 +1,16 @@ +import { unknown } from 'external'; + +class proto { + static flag = false; + static get prop() { + this.flag = true; + } +} + +class obj extends proto {} + +obj[unknown]; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/_config.js b/test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/_config.js new file mode 100644 index 00000000000..4b7c9b3ebb3 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles fully deoptimized objects', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/main.js b/test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/main.js new file mode 100644 index 00000000000..73c7d3a1af3 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/main.js @@ -0,0 +1,17 @@ +import { unknown } from 'external'; + +const obj = { + flag: false +}; + +Object.defineProperty(obj, unknown, { + set(value) { + this.flag = value; + } +}); + +obj.prop = true; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-setter/known-setter/_config.js b/test/function/samples/modify-this-via-setter/known-setter/_config.js new file mode 100644 index 00000000000..449c895ba6d --- /dev/null +++ b/test/function/samples/modify-this-via-setter/known-setter/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles known setters that modify "this"' +}; diff --git a/test/function/samples/modify-this-via-setter/known-setter/main.js b/test/function/samples/modify-this-via-setter/known-setter/main.js new file mode 100644 index 00000000000..74930dba6fb --- /dev/null +++ b/test/function/samples/modify-this-via-setter/known-setter/main.js @@ -0,0 +1,12 @@ +const obj = { + flag: false, + set prop(value) { + this.flag = value; + } +}; + +obj.prop = true; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-setter/known-super-prop/_config.js b/test/function/samples/modify-this-via-setter/known-super-prop/_config.js new file mode 100644 index 00000000000..1a2b7ea0df4 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/known-super-prop/_config.js @@ -0,0 +1,4 @@ +module.exports = { + description: 'handles setters that modify "this" on prototypes for known properties', + minNodeVersion: 12 +}; diff --git a/test/function/samples/modify-this-via-setter/known-super-prop/main.js b/test/function/samples/modify-this-via-setter/known-super-prop/main.js new file mode 100644 index 00000000000..d6f72d34575 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/known-super-prop/main.js @@ -0,0 +1,14 @@ +class proto { + static flag = false; + static set prop(value) { + this.flag = value; + } +} + +class obj extends proto {} + +obj.prop = true; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-setter/unknown-prop-setter/_config.js b/test/function/samples/modify-this-via-setter/unknown-prop-setter/_config.js new file mode 100644 index 00000000000..d6305f6c204 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/unknown-prop-setter/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles unknown setters that modify "this"', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-setter/unknown-prop-setter/main.js b/test/function/samples/modify-this-via-setter/unknown-prop-setter/main.js new file mode 100644 index 00000000000..4285ed33781 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/unknown-prop-setter/main.js @@ -0,0 +1,14 @@ +import { unknown } from 'external'; + +const obj = { + set [unknown](value) { + this.flag = value; + }, + flag: false +}; + +obj.prop = true; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/_config.js b/test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/_config.js new file mode 100644 index 00000000000..5b67fd6ea0e --- /dev/null +++ b/test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles unknown setters that modify "this" for unknown property access', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/main.js b/test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/main.js new file mode 100644 index 00000000000..536e3c7fa79 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/main.js @@ -0,0 +1,14 @@ +import { unknown } from 'external'; + +const obj = { + set [unknown](value) { + this.flag = value; + }, + flag: false +}; + +obj[unknown] = true; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-setter/unknown-super-prop/_config.js b/test/function/samples/modify-this-via-setter/unknown-super-prop/_config.js new file mode 100644 index 00000000000..c68617f4906 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/unknown-super-prop/_config.js @@ -0,0 +1,12 @@ +module.exports = { + description: 'handles setters that modify "this" on prototypes for unknown properties', + minNodeVersion: 12, + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-setter/unknown-super-prop/main.js b/test/function/samples/modify-this-via-setter/unknown-super-prop/main.js new file mode 100644 index 00000000000..f1e106b4b70 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/unknown-super-prop/main.js @@ -0,0 +1,16 @@ +import { unknown } from 'external'; + +class proto { + static flag = false; + static set prop(value) { + this.flag = value; + } +} + +class obj extends proto {} + +obj[unknown] = true; + +if (!obj.flag) { + throw new Error('mutation not detected'); +}