From f805c840232680862c693c2a56d997e0c49cff38 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sun, 30 May 2021 06:39:36 +0200 Subject: [PATCH] Tracks side effects of thenables --- src/ast/nodes/AwaitExpression.ts | 17 +++- src/ast/nodes/shared/FunctionNode.ts | 16 ++- src/ast/nodes/shared/knownGlobals.ts | 7 +- .../samples/async-function-effects/_config.js | 5 + .../async-function-effects/_expected.js | 74 ++++++++++++++ .../samples/async-function-effects/main.js | 97 +++++++++++++++++++ test/form/samples/promises/_config.js | 3 +- test/form/samples/promises/_expected.js | 49 ++++++++++ test/form/samples/promises/_expected/amd.js | 21 ---- test/form/samples/promises/_expected/cjs.js | 19 ---- test/form/samples/promises/_expected/es.js | 15 --- test/form/samples/promises/_expected/iife.js | 24 ----- .../form/samples/promises/_expected/system.js | 22 ----- test/form/samples/promises/_expected/umd.js | 25 ----- test/form/samples/promises/main.js | 59 ++++++++--- test/form/samples/side-effects-await/main.js | 6 -- .../samples/supports-es6-shim/_expected.js | 3 +- 17 files changed, 307 insertions(+), 155 deletions(-) create mode 100644 test/form/samples/async-function-effects/_config.js create mode 100644 test/form/samples/async-function-effects/_expected.js create mode 100644 test/form/samples/async-function-effects/main.js create mode 100644 test/form/samples/promises/_expected.js delete mode 100644 test/form/samples/promises/_expected/amd.js delete mode 100644 test/form/samples/promises/_expected/cjs.js delete mode 100644 test/form/samples/promises/_expected/es.js delete mode 100644 test/form/samples/promises/_expected/iife.js delete mode 100644 test/form/samples/promises/_expected/system.js delete mode 100644 test/form/samples/promises/_expected/umd.js diff --git a/src/ast/nodes/AwaitExpression.ts b/src/ast/nodes/AwaitExpression.ts index 3361537b541..cbe54cc392d 100644 --- a/src/ast/nodes/AwaitExpression.ts +++ b/src/ast/nodes/AwaitExpression.ts @@ -1,3 +1,5 @@ +import { NormalizedTreeshakingOptions } from '../../rollup/types'; +import { NO_ARGS } from '../CallOptions'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import ArrowFunctionExpression from './ArrowFunctionExpression'; import * as NodeType from './NodeType'; @@ -9,7 +11,20 @@ export default class AwaitExpression extends NodeBase { type!: NodeType.tAwaitExpression; hasEffects(context: HasEffectsContext): boolean { - return !context.ignore.returnAwaitYield || this.argument.hasEffects(context); + const { propertyReadSideEffects } = this.context.options + .treeshake as NormalizedTreeshakingOptions; + return ( + !context.ignore.returnAwaitYield || + this.argument.hasEffects(context) || + this.argument.hasEffectsWhenCalledAtPath( + ['then'], + { args: NO_ARGS, thisParam: null, withNew: false }, + context + ) || + (propertyReadSideEffects && + (propertyReadSideEffects === 'always' || + this.argument.hasEffectsWhenAccessedAtPath(['then'], context))) + ); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index c2c2592ef15..adb52a60435 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -1,4 +1,4 @@ -import { CallOptions } from '../../CallOptions'; +import { CallOptions, NO_ARGS } from '../../CallOptions'; import { BROKEN_FLOW_NONE, HasEffectsContext, InclusionContext } from '../../ExecutionContext'; import { EVENT_CALLED, NodeEvent } from '../../NodeEvents'; import FunctionScope from '../../scopes/FunctionScope'; @@ -56,7 +56,7 @@ export default class FunctionNode extends NodeBase { } getReturnExpressionWhenCalledAtPath(path: ObjectPath): ExpressionEntity { - return path.length === 0 ? this.scope.getReturnExpression() : UNKNOWN_EXPRESSION; + return !this.async && path.length === 0 ? this.scope.getReturnExpression() : UNKNOWN_EXPRESSION; } hasEffects(): boolean { @@ -81,6 +81,18 @@ export default class FunctionNode extends NodeBase { context: HasEffectsContext ): boolean { if (path.length > 0) return true; + if ( + this.async && + this.scope + .getReturnExpression() + .hasEffectsWhenCalledAtPath( + ['then'], + { args: NO_ARGS, thisParam: null, withNew: false }, + context + ) + ) { + return true; + } for (const param of this.params) { if (param.hasEffects(context)) return true; } diff --git a/src/ast/nodes/shared/knownGlobals.ts b/src/ast/nodes/shared/knownGlobals.ts index 268a02e5164..a78f36637ef 100644 --- a/src/ast/nodes/shared/knownGlobals.ts +++ b/src/ast/nodes/shared/knownGlobals.ts @@ -191,10 +191,11 @@ const knownGlobals: GlobalDescription = { Promise: { __proto__: null, [ValueProperties]: IMPURE, - all: PF, + all: O, prototype: O, - race: PF, - resolve: PF + race: O, + reject: O, + resolve: O }, propertyIsEnumerable: O, Proxy: O, diff --git a/test/form/samples/async-function-effects/_config.js b/test/form/samples/async-function-effects/_config.js new file mode 100644 index 00000000000..9918fc8d66a --- /dev/null +++ b/test/form/samples/async-function-effects/_config.js @@ -0,0 +1,5 @@ +const path = require('path'); + +module.exports = { + description: 'tracks effects when awaiting thenables' +}; diff --git a/test/form/samples/async-function-effects/_expected.js b/test/form/samples/async-function-effects/_expected.js new file mode 100644 index 00000000000..4885bad6ffa --- /dev/null +++ b/test/form/samples/async-function-effects/_expected.js @@ -0,0 +1,74 @@ +(async function () { + return { + then() { + console.log(1); + } + }; +})(); + +(async function () { + await { + then: function () { + console.log(2); + } + }; + return { then() {} }; +})(); + +(async function () { + await { + get then() { + console.log(3); + return () => {}; + } + }; + return { then() {} }; +})(); + +(async function () { + await { + get then() { + return () => console.log(4); + } + }; + return { then() {} }; +})(); + +(async function () { + await await { + then(resolve) { + resolve({ + then() { + console.log(5); + } + }); + } + }; + return { then() {} }; +})(); + +async function asyncIdentity(x) { + return x; +} + +asyncIdentity({}); // no side effects - may be dropped + +const promise = asyncIdentity(6); + +promise.then(x => console.log(x)); + +asyncIdentity({ + then(success, fail) { + success(console.log(7)); + } +}); + +asyncIdentity({ + then(resolve) { + resolve({ + then() { + console.log(8); + } + }); + } +}); diff --git a/test/form/samples/async-function-effects/main.js b/test/form/samples/async-function-effects/main.js new file mode 100644 index 00000000000..86528d39642 --- /dev/null +++ b/test/form/samples/async-function-effects/main.js @@ -0,0 +1,97 @@ +(async function () { + return { + then() { + console.log(1); + } + }; +})(); + +// removed +(async function () { + return { then() {} }; +})(); + +(async function () { + await { + then: function () { + console.log(2); + } + }; + return { then() {} }; +})(); + +// removed +(async function () { + await { + then: function () {} + }; + return { then() {} }; +})(); + +(async function () { + await { + get then() { + console.log(3); + return () => {}; + } + }; + return { then() {} }; +})(); + +(async function () { + await { + get then() { + return () => console.log(4); + } + }; + return { then() {} }; +})(); + +// removed +(async function () { + await { + get then() { + return () => {}; + } + }; + return { then() {} }; +})(); + +(async function () { + await await { + then(resolve) { + resolve({ + then() { + console.log(5); + } + }); + } + }; + return { then() {} }; +})(); + +async function asyncIdentity(x) { + return x; +} + +asyncIdentity({}); // no side effects - may be dropped + +const promise = asyncIdentity(6); + +promise.then(x => console.log(x)); + +asyncIdentity({ + then(success, fail) { + success(console.log(7)); + } +}); + +asyncIdentity({ + then(resolve) { + resolve({ + then() { + console.log(8); + } + }); + } +}); diff --git a/test/form/samples/promises/_config.js b/test/form/samples/promises/_config.js index 5d746d2a93d..c4e9dee1854 100644 --- a/test/form/samples/promises/_config.js +++ b/test/form/samples/promises/_config.js @@ -1,4 +1,3 @@ module.exports = { - description: 'do not remove promise creations', - options: { output: { name: 'bundle' } } + description: 'do not remove promise creations' }; diff --git a/test/form/samples/promises/_expected.js b/test/form/samples/promises/_expected.js new file mode 100644 index 00000000000..12b24ec5ee2 --- /dev/null +++ b/test/form/samples/promises/_expected.js @@ -0,0 +1,49 @@ +Promise.resolve({ + then() { + console.log(1); + } +}); + +Promise.resolve({ + get then() { + return () => console.log(2); + } +}); + +Promise.reject('should be kept for uncaught rejections'); + +Promise.all([ + { + then() { + console.log(3); + } + }, + null +]); + +Promise.all([ + null, + { + get then() { + return () => console.log(4); + } + } +]); + +Promise.race([ + { + then() { + console.log(5); + } + }, + null +]); + +Promise.race([ + null, + { + get then() { + return () => console.log(6); + } + } +]); diff --git a/test/form/samples/promises/_expected/amd.js b/test/form/samples/promises/_expected/amd.js deleted file mode 100644 index 3abe869be19..00000000000 --- a/test/form/samples/promises/_expected/amd.js +++ /dev/null @@ -1,21 +0,0 @@ -define(['exports'], function (exports) { 'use strict'; - - new Promise( () => { - console.log( 'fire & forget' ); - } ); - - const p2 = new Promise( () => { - console.info( 'forget me as well' ); - } ); - - const p3 = new Promise( () => { - console.info( 'and me too' ); - } ); - Promise.reject('should be kept for uncaught rejections'); - const allExported = Promise.all([p2, p3]); - - exports.allExported = allExported; - - Object.defineProperty(exports, '__esModule', { value: true }); - -}); diff --git a/test/form/samples/promises/_expected/cjs.js b/test/form/samples/promises/_expected/cjs.js deleted file mode 100644 index b6613199bbd..00000000000 --- a/test/form/samples/promises/_expected/cjs.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, '__esModule', { value: true }); - -new Promise( () => { - console.log( 'fire & forget' ); -} ); - -const p2 = new Promise( () => { - console.info( 'forget me as well' ); -} ); - -const p3 = new Promise( () => { - console.info( 'and me too' ); -} ); -Promise.reject('should be kept for uncaught rejections'); -const allExported = Promise.all([p2, p3]); - -exports.allExported = allExported; diff --git a/test/form/samples/promises/_expected/es.js b/test/form/samples/promises/_expected/es.js deleted file mode 100644 index 5b295ae61bc..00000000000 --- a/test/form/samples/promises/_expected/es.js +++ /dev/null @@ -1,15 +0,0 @@ -new Promise( () => { - console.log( 'fire & forget' ); -} ); - -const p2 = new Promise( () => { - console.info( 'forget me as well' ); -} ); - -const p3 = new Promise( () => { - console.info( 'and me too' ); -} ); -Promise.reject('should be kept for uncaught rejections'); -const allExported = Promise.all([p2, p3]); - -export { allExported }; diff --git a/test/form/samples/promises/_expected/iife.js b/test/form/samples/promises/_expected/iife.js deleted file mode 100644 index 66392a8ab0d..00000000000 --- a/test/form/samples/promises/_expected/iife.js +++ /dev/null @@ -1,24 +0,0 @@ -var bundle = (function (exports) { - 'use strict'; - - new Promise( () => { - console.log( 'fire & forget' ); - } ); - - const p2 = new Promise( () => { - console.info( 'forget me as well' ); - } ); - - const p3 = new Promise( () => { - console.info( 'and me too' ); - } ); - Promise.reject('should be kept for uncaught rejections'); - const allExported = Promise.all([p2, p3]); - - exports.allExported = allExported; - - Object.defineProperty(exports, '__esModule', { value: true }); - - return exports; - -}({})); diff --git a/test/form/samples/promises/_expected/system.js b/test/form/samples/promises/_expected/system.js deleted file mode 100644 index f77b8fa2796..00000000000 --- a/test/form/samples/promises/_expected/system.js +++ /dev/null @@ -1,22 +0,0 @@ -System.register('bundle', [], function (exports) { - 'use strict'; - return { - execute: function () { - - new Promise( () => { - console.log( 'fire & forget' ); - } ); - - const p2 = new Promise( () => { - console.info( 'forget me as well' ); - } ); - - const p3 = new Promise( () => { - console.info( 'and me too' ); - } ); - Promise.reject('should be kept for uncaught rejections'); - const allExported = exports('allExported', Promise.all([p2, p3])); - - } - }; -}); diff --git a/test/form/samples/promises/_expected/umd.js b/test/form/samples/promises/_expected/umd.js deleted file mode 100644 index 7d28fb7c257..00000000000 --- a/test/form/samples/promises/_expected/umd.js +++ /dev/null @@ -1,25 +0,0 @@ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : - typeof define === 'function' && define.amd ? define(['exports'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.bundle = {})); -}(this, (function (exports) { 'use strict'; - - new Promise( () => { - console.log( 'fire & forget' ); - } ); - - const p2 = new Promise( () => { - console.info( 'forget me as well' ); - } ); - - const p3 = new Promise( () => { - console.info( 'and me too' ); - } ); - Promise.reject('should be kept for uncaught rejections'); - const allExported = Promise.all([p2, p3]); - - exports.allExported = allExported; - - Object.defineProperty(exports, '__esModule', { value: true }); - -}))); diff --git a/test/form/samples/promises/main.js b/test/form/samples/promises/main.js index a42566cf26e..12b24ec5ee2 100644 --- a/test/form/samples/promises/main.js +++ b/test/form/samples/promises/main.js @@ -1,18 +1,49 @@ -const p1 = new Promise( () => { - console.log( 'fire & forget' ); -} ); +Promise.resolve({ + then() { + console.log(1); + } +}); -const p2 = new Promise( () => { - console.info( 'forget me as well' ); -} ); +Promise.resolve({ + get then() { + return () => console.log(2); + } +}); -const p3 = new Promise( () => { - console.info( 'and me too' ); -} ); +Promise.reject('should be kept for uncaught rejections'); -const p4 = Promise.resolve('no side effect'); -const p5 = Promise.reject('should be kept for uncaught rejections'); +Promise.all([ + { + then() { + console.log(3); + } + }, + null +]); -const all = Promise.all([p2, p3]); -export const allExported = Promise.all([p2, p3]); -const race = Promise.race([p2, p3]); +Promise.all([ + null, + { + get then() { + return () => console.log(4); + } + } +]); + +Promise.race([ + { + then() { + console.log(5); + } + }, + null +]); + +Promise.race([ + null, + { + get then() { + return () => console.log(6); + } + } +]); diff --git a/test/form/samples/side-effects-await/main.js b/test/form/samples/side-effects-await/main.js index 0641421d34a..623a857a000 100644 --- a/test/form/samples/side-effects-await/main.js +++ b/test/form/samples/side-effects-await/main.js @@ -11,9 +11,3 @@ async function hasEffects2 () { hasEffects2(); -async function isRemoved () { - await globalThis.unknown; -} - -isRemoved(); - diff --git a/test/form/samples/supports-es6-shim/_expected.js b/test/form/samples/supports-es6-shim/_expected.js index eb47b8951c0..d4bf613751a 100644 --- a/test/form/samples/supports-es6-shim/_expected.js +++ b/test/form/samples/supports-es6-shim/_expected.js @@ -2662,7 +2662,8 @@ var es6Shim = {exports: {}}; var getsThenSynchronously = supportsDescriptors && (function () { var count = 0; // eslint-disable-next-line getter-return - Object.defineProperty({}, 'then', { get: function () { count += 1; } }); + var thenable = Object.defineProperty({}, 'then', { get: function () { count += 1; } }); + Promise.resolve(thenable); return count === 1; }());