Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement try-statement-deoptimization for feature detection, tree-shake unused arguments #2892

Merged
merged 8 commits into from Jun 5, 2019
50 changes: 50 additions & 0 deletions docs/999-big-list-of-options.md
Expand Up @@ -825,6 +825,56 @@ const result = foo.bar;
const illegalAccess = foo.quux.tooDeep;
```

**treeshake.tryCatchDeoptimization**
Type: `boolean`<br>
CLI: `--treeshake.tryCatchDeoptimization`/`--no-treeshake.tryCatchDeoptimization`<br>
Default: `true`

By default, Rollup assumes that many builtin globals of the runtime behave according to the latest specs when tree-shaking and do not throw unexpected errors. In order to support e.g. feature detection workflows that rely on those errors being thrown, Rollup will by default deactivate tree-shaking inside try-statements. Furthermore, it will also deactivate tree-shaking inside functions that are called directly from a try-statement if Rollup can resolve the function. Set `treeshake.tryCatchDeoptimization` to `false` if you do not need this feature and want to have tree-shaking inside try-statements as well as inside functions called from those statements.

```js
function directlyCalled1() {
// as this function is directly called from a try-statement, it will be
// retained unmodified for tryCatchDeoptimization: true including staements
// that would usually be removed
Object.create(null);
notDirectlyCalled();
}

function directlyCalled2() {
Object.create(null);
notDirectlyCalled();
}

function notDirectlyCalled() {
// even if this function is retained, this will be removed as the function is
// never directly called from a try-statement
Object.create(null);
}

function test(callback) {
try {
// calls to otherwise side-effect-free global functions are retained
// inside try-statements for tryCatchDeoptimization: true
Object.create(null);

// directly resolvable calls will also be deoptimized
directlyCalled1();

// if a parameter is called, then all arguments passed to that function
// parameter will be deoptimized
callback();

// all calls will be retained but only calls of the form
// "identifier(someArguments)" will also deoptimize the target
(notDirectlyCalled && notDirectlyCalled)();
} catch {}
}

test(directlyCalled2);

```

### Experimental options

These options reflect new features that have not yet been fully finalized. Availability, behaviour and usage may therefore be subject to change between minor versions.
Expand Down
7 changes: 5 additions & 2 deletions src/Graph.ts
Expand Up @@ -117,13 +117,16 @@ export default class Graph {
moduleSideEffects: (options.treeshake as TreeshakingOptions).moduleSideEffects,
propertyReadSideEffects:
(options.treeshake as TreeshakingOptions).propertyReadSideEffects !== false,
pureExternalModules: (options.treeshake as TreeshakingOptions).pureExternalModules
pureExternalModules: (options.treeshake as TreeshakingOptions).pureExternalModules,
tryCatchDeoptimization:
(options.treeshake as TreeshakingOptions).tryCatchDeoptimization !== false
}
: {
annotations: true,
moduleSideEffects: true,
propertyReadSideEffects: true,
pureExternalModules: false
pureExternalModules: false,
tryCatchDeoptimization: true
};
}

Expand Down
5 changes: 4 additions & 1 deletion src/Module.ts
Expand Up @@ -109,6 +109,7 @@ export interface AstContext {
traceExport: (name: string) => Variable;
traceVariable: (name: string) => Variable | null;
treeshake: boolean;
tryCatchDeoptimization: boolean;
usesTopLevelAwait: boolean;
warn: (warning: RollupWarning, pos: number) => void;
}
Expand Down Expand Up @@ -589,6 +590,8 @@ export default class Module {
traceExport: this.getVariableForExportName.bind(this),
traceVariable: this.traceVariable.bind(this),
treeshake: !!this.graph.treeshakingOptions,
tryCatchDeoptimization: (!this.graph.treeshakingOptions ||
this.graph.treeshakingOptions.tryCatchDeoptimization) as boolean,
usesTopLevelAwait: false,
warn: this.warn.bind(this)
};
Expand Down Expand Up @@ -631,7 +634,7 @@ export default class Module {
const otherModule = importDeclaration.module as Module | ExternalModule;

if (otherModule instanceof Module && importDeclaration.name === '*') {
return (otherModule).getOrCreateNamespace();
return otherModule.getOrCreateNamespace();
}

const declaration = otherModule.getVariableForExportName(importDeclaration.name);
Expand Down
9 changes: 0 additions & 9 deletions src/ast/ExecutionPathOptions.ts
Expand Up @@ -10,7 +10,6 @@ import ThisVariable from './variables/ThisVariable';
export enum OptionTypes {
IGNORED_LABELS,
ACCESSED_NODES,
ARGUMENTS_VARIABLES,
ASSIGNED_NODES,
IGNORE_BREAK_STATEMENTS,
IGNORE_RETURN_AWAIT_YIELD,
Expand Down Expand Up @@ -78,10 +77,6 @@ export class ExecutionPathOptions {
);
}

getArgumentsVariables(): ExpressionEntity[] {
return (this.get(OptionTypes.ARGUMENTS_VARIABLES) || []) as ExpressionEntity[];
}

getHasEffectsWhenCalledOptions() {
return this.setIgnoreReturnAwaitYield()
.setIgnoreBreakStatements(false)
Expand Down Expand Up @@ -171,10 +166,6 @@ export class ExecutionPathOptions {
return this.setIn([OptionTypes.REPLACED_VARIABLE_INITS, variable], init);
}

setArgumentsVariables(variables: ExpressionEntity[]) {
return this.set(OptionTypes.ARGUMENTS_VARIABLES, variables);
}

setIgnoreBreakStatements(value = true) {
return this.set(OptionTypes.IGNORE_BREAK_STATEMENTS, value);
}
Expand Down
4 changes: 3 additions & 1 deletion src/ast/nodes/ArrayPattern.ts
Expand Up @@ -19,11 +19,13 @@ export default class ArrayPattern extends NodeBase implements PatternNode {
}

declare(kind: string, _init: ExpressionEntity) {
const variables = [];
for (const element of this.elements) {
if (element !== null) {
element.declare(kind, UNKNOWN_EXPRESSION);
variables.push(...element.declare(kind, UNKNOWN_EXPRESSION));
}
}
return variables;
}

deoptimizePath(path: ObjectPath) {
Expand Down
22 changes: 20 additions & 2 deletions src/ast/nodes/ArrowFunctionExpression.ts
Expand Up @@ -4,9 +4,12 @@ import ReturnValueScope from '../scopes/ReturnValueScope';
import Scope from '../scopes/Scope';
import { ObjectPath, UNKNOWN_EXPRESSION, UNKNOWN_KEY, UNKNOWN_PATH } from '../values';
import BlockStatement from './BlockStatement';
import Identifier from './Identifier';
import * as NodeType from './NodeType';
import RestElement from './RestElement';
import { ExpressionNode, GenericEsTreeNode, NodeBase } from './shared/Node';
import { PatternNode } from './shared/Pattern';
import SpreadElement from './SpreadElement';

export default class ArrowFunctionExpression extends NodeBase {
body!: BlockStatement | ExpressionNode;
Expand Down Expand Up @@ -57,10 +60,25 @@ export default class ArrowFunctionExpression extends NodeBase {
return this.body.hasEffects(options);
}

initialise() {
include(includeChildrenRecursively: boolean | 'variables') {
this.included = true;
this.body.include(includeChildrenRecursively);
for (const param of this.params) {
param.declare('parameter', UNKNOWN_EXPRESSION);
if (!(param instanceof Identifier)) {
param.include(includeChildrenRecursively);
}
}
}

includeCallArguments(args: (ExpressionNode | SpreadElement)[]): void {
this.scope.includeCallArguments(args);
}

initialise() {
this.scope.addParameterVariables(
this.params.map(param => param.declare('parameter', UNKNOWN_EXPRESSION)),
this.params[this.params.length - 1] instanceof RestElement
);
if (this.body instanceof BlockStatement) {
this.body.addImplicitReturnExpressionToScope();
} else {
Expand Down
2 changes: 1 addition & 1 deletion src/ast/nodes/AssignmentPattern.ts
Expand Up @@ -25,7 +25,7 @@ export default class AssignmentPattern extends NodeBase implements PatternNode {
}

declare(kind: string, init: ExpressionEntity) {
this.left.declare(kind, init);
return this.left.declare(kind, init);
}

deoptimizePath(path: ObjectPath) {
Expand Down
8 changes: 4 additions & 4 deletions src/ast/nodes/AwaitExpression.ts
Expand Up @@ -4,7 +4,7 @@ import { ExecutionPathOptions } from '../ExecutionPathOptions';
import ArrowFunctionExpression from './ArrowFunctionExpression';
import * as NodeType from './NodeType';
import FunctionNode from './shared/FunctionNode';
import { ExpressionNode, Node, NodeBase } from './shared/Node';
import { ExpressionNode, IncludeChildren, Node, NodeBase } from './shared/Node';

export default class AwaitExpression extends NodeBase {
argument!: ExpressionNode;
Expand All @@ -14,15 +14,15 @@ export default class AwaitExpression extends NodeBase {
return super.hasEffects(options) || !options.ignoreReturnAwaitYield();
}

include(includeAllChildrenRecursively: boolean) {
super.include(includeAllChildrenRecursively);
if (!this.context.usesTopLevelAwait) {
include(includeChildrenRecursively: IncludeChildren) {
if (!this.included && !this.context.usesTopLevelAwait) {
let parent = this.parent;
do {
if (parent instanceof FunctionNode || parent instanceof ArrowFunctionExpression) return;
} while ((parent = (parent as Node).parent as Node));
this.context.usesTopLevelAwait = true;
}
super.include(includeChildrenRecursively);
}

render(code: MagicString, options: RenderOptions) {
Expand Down
8 changes: 4 additions & 4 deletions src/ast/nodes/BlockStatement.ts
Expand Up @@ -6,7 +6,7 @@ import ChildScope from '../scopes/ChildScope';
import Scope from '../scopes/Scope';
import { UNKNOWN_EXPRESSION } from '../values';
import * as NodeType from './NodeType';
import { Node, StatementBase, StatementNode } from './shared/Node';
import { IncludeChildren, Node, StatementBase, StatementNode } from './shared/Node';

export default class BlockStatement extends StatementBase {
body!: StatementNode[];
Expand All @@ -32,11 +32,11 @@ export default class BlockStatement extends StatementBase {
return false;
}

include(includeAllChildrenRecursively: boolean) {
include(includeChildrenRecursively: IncludeChildren) {
this.included = true;
for (const node of this.body) {
if (includeAllChildrenRecursively || node.shouldBeIncluded())
node.include(includeAllChildrenRecursively);
if (includeChildrenRecursively || node.shouldBeIncluded())
node.include(includeChildrenRecursively);
}
}

Expand Down
50 changes: 45 additions & 5 deletions src/ast/nodes/CallExpression.ts
@@ -1,6 +1,10 @@
import MagicString from 'magic-string';
import { BLANK } from '../../utils/blank';
import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers';
import {
findFirstOccurrenceOutsideComment,
NodeRenderOptions,
RenderOptions
} from '../../utils/renderHelpers';
import CallOptions from '../CallOptions';
import { DeoptimizableEntity } from '../DeoptimizableEntity';
import { ExecutionPathOptions } from '../ExecutionPathOptions';
Expand All @@ -19,7 +23,7 @@ import {
import Identifier from './Identifier';
import * as NodeType from './NodeType';
import { ExpressionEntity } from './shared/Expression';
import { ExpressionNode, NodeBase } from './shared/Node';
import { ExpressionNode, INCLUDE_VARIABLES, IncludeChildren, NodeBase } from './shared/Node';
import SpreadElement from './SpreadElement';

export default class CallExpression extends NodeBase implements DeoptimizableEntity {
Expand Down Expand Up @@ -196,8 +200,21 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt
);
}

include(includeAllChildrenRecursively: boolean) {
super.include(includeAllChildrenRecursively);
include(includeChildrenRecursively: IncludeChildren) {
if (includeChildrenRecursively) {
super.include(includeChildrenRecursively);
if (
includeChildrenRecursively === INCLUDE_VARIABLES &&
this.callee instanceof Identifier &&
this.callee.variable
) {
this.callee.variable.includeInitRecursively();
}
} else {
this.included = true;
this.callee.include(false);
}
this.callee.includeCallArguments(this.arguments);
if (!(this.returnExpression as ExpressionEntity).included) {
(this.returnExpression as ExpressionEntity).include(false);
}
Expand All @@ -216,7 +233,30 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt
options: RenderOptions,
{ renderedParentType }: NodeRenderOptions = BLANK
) {
super.render(code, options);
this.callee.render(code, options);
if (this.arguments.length > 0) {
if (this.arguments[this.arguments.length - 1].included) {
for (const arg of this.arguments) {
arg.render(code, options);
}
} else {
let lastIncludedIndex = this.arguments.length - 2;
while (lastIncludedIndex >= 0 && !this.arguments[lastIncludedIndex].included) {
lastIncludedIndex--;
}
if (lastIncludedIndex >= 0) {
for (let index = 0; index <= lastIncludedIndex; index++) {
this.arguments[index].render(code, options);
}
code.remove(this.arguments[lastIncludedIndex].end, this.end - 1);
} else {
code.remove(
findFirstOccurrenceOutsideComment(code.original, '(', this.callee.end) + 1,
this.end - 1
);
}
}
}
if (
renderedParentType === NodeType.ExpressionStatement &&
this.callee.type === NodeType.FunctionExpression
Expand Down
14 changes: 7 additions & 7 deletions src/ast/nodes/ConditionalExpression.ts
Expand Up @@ -20,7 +20,7 @@ import CallExpression from './CallExpression';
import * as NodeType from './NodeType';
import { ExpressionEntity } from './shared/Expression';
import { MultiExpression } from './shared/MultiExpression';
import { ExpressionNode, NodeBase } from './shared/Node';
import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node';

export default class ConditionalExpression extends NodeBase implements DeoptimizableEntity {
alternate!: ExpressionNode;
Expand Down Expand Up @@ -133,14 +133,14 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz
return this.usedBranch.hasEffectsWhenCalledAtPath(path, callOptions, options);
}

include(includeAllChildrenRecursively: boolean) {
include(includeChildrenRecursively: IncludeChildren) {
this.included = true;
if (includeAllChildrenRecursively || this.usedBranch === null || this.test.shouldBeIncluded()) {
this.test.include(includeAllChildrenRecursively);
this.consequent.include(includeAllChildrenRecursively);
this.alternate.include(includeAllChildrenRecursively);
if (includeChildrenRecursively || this.usedBranch === null || this.test.shouldBeIncluded()) {
this.test.include(includeChildrenRecursively);
this.consequent.include(includeChildrenRecursively);
this.alternate.include(includeChildrenRecursively);
} else {
this.usedBranch.include(includeAllChildrenRecursively);
this.usedBranch.include(includeChildrenRecursively);
}
}

Expand Down
8 changes: 4 additions & 4 deletions src/ast/nodes/ExportDefaultDeclaration.ts
Expand Up @@ -12,7 +12,7 @@ import ClassDeclaration from './ClassDeclaration';
import FunctionDeclaration from './FunctionDeclaration';
import Identifier from './Identifier';
import * as NodeType from './NodeType';
import { ExpressionNode, NodeBase } from './shared/Node';
import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node';

const WHITESPACE = /\s/;

Expand Down Expand Up @@ -43,9 +43,9 @@ export default class ExportDefaultDeclaration extends NodeBase {

private declarationName: string | undefined;

include(includeAllChildrenRecursively: boolean) {
super.include(includeAllChildrenRecursively);
if (includeAllChildrenRecursively) {
include(includeChildrenRecursively: IncludeChildren) {
super.include(includeChildrenRecursively);
if (includeChildrenRecursively) {
this.context.includeVariable(this.variable);
}
}
Expand Down