Skip to content

Commit

Permalink
Handle optional control flow in labeled statements, remove unused lab…
Browse files Browse the repository at this point in the history
…els (#3170)

* Fix conditional break in labeled statement handling

* Track and remove unused labels

* Rename breakFlow -> brokenFlow

* Improve coverage
  • Loading branch information
lukastaegert committed Oct 18, 2019
1 parent 836a106 commit 121a7f4
Show file tree
Hide file tree
Showing 32 changed files with 454 additions and 360 deletions.
17 changes: 10 additions & 7 deletions src/ast/ExecutionContext.ts
Expand Up @@ -9,18 +9,19 @@ interface ExecutionContextIgnore {
returnAwaitYield: boolean;
}

export const BREAKFLOW_NONE: false = false;
export const BREAKFLOW_ERROR_RETURN: true = true;

export type BreakFlow = typeof BREAKFLOW_NONE | typeof BREAKFLOW_ERROR_RETURN | Set<string | null>;
export const BROKEN_FLOW_NONE = 0;
export const BROKEN_FLOW_BREAK_CONTINUE = 1;
export const BROKEN_FLOW_ERROR_RETURN_LABEL = 2;

export interface InclusionContext {
breakFlow: BreakFlow;
brokenFlow: number;
includedLabels: Set<string>;
}

export interface HasEffectsContext extends InclusionContext {
accessed: PathTracker;
assigned: PathTracker;
brokenFlow: number;
called: PathTracker;
ignore: ExecutionContextIgnore;
instantiated: PathTracker;
Expand All @@ -29,22 +30,24 @@ export interface HasEffectsContext extends InclusionContext {

export function createInclusionContext(): InclusionContext {
return {
breakFlow: BREAKFLOW_NONE
brokenFlow: BROKEN_FLOW_NONE,
includedLabels: new Set()
};
}

export function createHasEffectsContext(): HasEffectsContext {
return {
accessed: new PathTracker(),
assigned: new PathTracker(),
breakFlow: BREAKFLOW_NONE,
brokenFlow: BROKEN_FLOW_NONE,
called: new PathTracker(),
ignore: {
breaks: false,
continues: false,
labels: new Set(),
returnAwaitYield: false
},
includedLabels: new Set(),
instantiated: new PathTracker(),
replacedVariableInits: new Map()
};
Expand Down
12 changes: 6 additions & 6 deletions src/ast/nodes/ArrowFunctionExpression.ts
@@ -1,5 +1,5 @@
import { CallOptions } from '../CallOptions';
import { BREAKFLOW_NONE, HasEffectsContext, InclusionContext } from '../ExecutionContext';
import { BROKEN_FLOW_NONE, HasEffectsContext, InclusionContext } from '../ExecutionContext';
import ReturnValueScope from '../scopes/ReturnValueScope';
import Scope from '../scopes/Scope';
import { ObjectPath, UNKNOWN_PATH, UnknownKey } from '../utils/PathTracker';
Expand Down Expand Up @@ -56,7 +56,7 @@ export default class ArrowFunctionExpression extends NodeBase {
for (const param of this.params) {
if (param.hasEffects(context)) return true;
}
const { ignore, breakFlow } = context;
const { ignore, brokenFlow } = context;
context.ignore = {
breaks: false,
continues: false,
Expand All @@ -65,7 +65,7 @@ export default class ArrowFunctionExpression extends NodeBase {
};
if (this.body.hasEffects(context)) return true;
context.ignore = ignore;
context.breakFlow = breakFlow;
context.brokenFlow = brokenFlow;
return false;
}

Expand All @@ -76,10 +76,10 @@ export default class ArrowFunctionExpression extends NodeBase {
param.include(context, includeChildrenRecursively);
}
}
const { breakFlow } = context;
context.breakFlow = BREAKFLOW_NONE;
const { brokenFlow } = context;
context.brokenFlow = BROKEN_FLOW_NONE;
this.body.include(context, includeChildrenRecursively);
context.breakFlow = breakFlow;
context.brokenFlow = brokenFlow;
}

includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void {
Expand Down
2 changes: 1 addition & 1 deletion src/ast/nodes/BlockStatement.ts
Expand Up @@ -28,7 +28,7 @@ export default class BlockStatement extends StatementBase {
hasEffects(context: HasEffectsContext) {
for (const node of this.body) {
if (node.hasEffects(context)) return true;
if (context.breakFlow) break;
if (context.brokenFlow) break;
}
return false;
}
Expand Down
25 changes: 19 additions & 6 deletions src/ast/nodes/BreakStatement.ts
@@ -1,4 +1,9 @@
import { HasEffectsContext, InclusionContext } from '../ExecutionContext';
import {
BROKEN_FLOW_BREAK_CONTINUE,
BROKEN_FLOW_ERROR_RETURN_LABEL,
HasEffectsContext,
InclusionContext
} from '../ExecutionContext';
import Identifier from './Identifier';
import * as NodeType from './NodeType';
import { StatementBase } from './shared/Node';
Expand All @@ -8,15 +13,23 @@ export default class BreakStatement extends StatementBase {
type!: NodeType.tBreakStatement;

hasEffects(context: HasEffectsContext) {
if (!(this.label ? context.ignore.labels.has(this.label.name) : context.ignore.breaks))
return true;
context.breakFlow = new Set([this.label && this.label.name]);
if (this.label) {
if (!context.ignore.labels.has(this.label.name)) return true;
context.includedLabels.add(this.label.name);
context.brokenFlow = BROKEN_FLOW_ERROR_RETURN_LABEL;
} else {
if (!context.ignore.breaks) return true;
context.brokenFlow = BROKEN_FLOW_BREAK_CONTINUE;
}
return false;
}

include(context: InclusionContext) {
this.included = true;
if (this.label) this.label.include(context);
context.breakFlow = new Set([this.label && this.label.name]);
if (this.label) {
this.label.include(context);
context.includedLabels.add(this.label.name);
}
context.brokenFlow = this.label ? BROKEN_FLOW_ERROR_RETURN_LABEL : BROKEN_FLOW_BREAK_CONTINUE;
}
}
25 changes: 19 additions & 6 deletions src/ast/nodes/ContinueStatement.ts
@@ -1,4 +1,9 @@
import { HasEffectsContext, InclusionContext } from '../ExecutionContext';
import {
BROKEN_FLOW_BREAK_CONTINUE,
BROKEN_FLOW_ERROR_RETURN_LABEL,
HasEffectsContext,
InclusionContext
} from '../ExecutionContext';
import Identifier from './Identifier';
import * as NodeType from './NodeType';
import { StatementBase } from './shared/Node';
Expand All @@ -8,15 +13,23 @@ export default class ContinueStatement extends StatementBase {
type!: NodeType.tContinueStatement;

hasEffects(context: HasEffectsContext) {
if (!(this.label ? context.ignore.labels.has(this.label.name) : context.ignore.continues))
return true;
context.breakFlow = new Set([this.label && this.label.name]);
if (this.label) {
if (!context.ignore.labels.has(this.label.name)) return true;
context.includedLabels.add(this.label.name);
context.brokenFlow = BROKEN_FLOW_ERROR_RETURN_LABEL;
} else {
if (!context.ignore.continues) return true;
context.brokenFlow = BROKEN_FLOW_BREAK_CONTINUE;
}
return false;
}

include(context: InclusionContext) {
this.included = true;
if (this.label) this.label.include(context);
context.breakFlow = new Set([this.label && this.label.name]);
if (this.label) {
this.label.include(context);
context.includedLabels.add(this.label.name);
}
context.brokenFlow = this.label ? BROKEN_FLOW_ERROR_RETURN_LABEL : BROKEN_FLOW_BREAK_CONTINUE;
}
}
18 changes: 11 additions & 7 deletions src/ast/nodes/DoWhileStatement.ts
@@ -1,4 +1,8 @@
import { HasEffectsContext, InclusionContext } from '../ExecutionContext';
import {
BROKEN_FLOW_BREAK_CONTINUE,
HasEffectsContext,
InclusionContext
} from '../ExecutionContext';
import * as NodeType from './NodeType';
import { ExpressionNode, IncludeChildren, StatementBase, StatementNode } from './shared/Node';

Expand All @@ -10,27 +14,27 @@ export default class DoWhileStatement extends StatementBase {
hasEffects(context: HasEffectsContext): boolean {
if (this.test.hasEffects(context)) return true;
const {
breakFlow,
brokenFlow,
ignore: { breaks, continues }
} = context;
context.ignore.breaks = true;
context.ignore.continues = true;
if (this.body.hasEffects(context)) return true;
context.ignore.breaks = breaks;
context.ignore.continues = continues;
if (context.breakFlow instanceof Set && context.breakFlow.has(null)) {
context.breakFlow = breakFlow;
if (context.brokenFlow === BROKEN_FLOW_BREAK_CONTINUE) {
context.brokenFlow = brokenFlow;
}
return false;
}

include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) {
this.included = true;
this.test.include(context, includeChildrenRecursively);
const { breakFlow } = context;
const { brokenFlow } = context;
this.body.include(context, includeChildrenRecursively);
if (context.breakFlow instanceof Set && context.breakFlow.has(null)) {
context.breakFlow = breakFlow;
if (context.brokenFlow === BROKEN_FLOW_BREAK_CONTINUE) {
context.brokenFlow = brokenFlow;
}
}
}
8 changes: 4 additions & 4 deletions src/ast/nodes/ForInStatement.ts
Expand Up @@ -35,15 +35,15 @@ export default class ForInStatement extends StatementBase {
)
return true;
const {
breakFlow,
brokenFlow,
ignore: { breaks, continues }
} = context;
context.ignore.breaks = true;
context.ignore.continues = true;
if (this.body.hasEffects(context)) return true;
context.ignore.breaks = breaks;
context.ignore.continues = continues;
context.breakFlow = breakFlow;
context.brokenFlow = brokenFlow;
return false;
}

Expand All @@ -52,9 +52,9 @@ export default class ForInStatement extends StatementBase {
this.left.includeWithAllDeclaredVariables(includeChildrenRecursively, context);
this.left.deoptimizePath(EMPTY_PATH);
this.right.include(context, includeChildrenRecursively);
const { breakFlow } = context;
const { brokenFlow } = context;
this.body.include(context, includeChildrenRecursively);
context.breakFlow = breakFlow;
context.brokenFlow = brokenFlow;
}

render(code: MagicString, options: RenderOptions) {
Expand Down
4 changes: 2 additions & 2 deletions src/ast/nodes/ForOfStatement.ts
Expand Up @@ -37,9 +37,9 @@ export default class ForOfStatement extends StatementBase {
this.left.includeWithAllDeclaredVariables(includeChildrenRecursively, context);
this.left.deoptimizePath(EMPTY_PATH);
this.right.include(context, includeChildrenRecursively);
const { breakFlow } = context;
const { brokenFlow } = context;
this.body.include(context, includeChildrenRecursively);
context.breakFlow = breakFlow;
context.brokenFlow = brokenFlow;
}

render(code: MagicString, options: RenderOptions) {
Expand Down
8 changes: 4 additions & 4 deletions src/ast/nodes/ForStatement.ts
Expand Up @@ -26,26 +26,26 @@ export default class ForStatement extends StatementBase {
)
return true;
const {
breakFlow,
brokenFlow,
ignore: { breaks, continues }
} = context;
context.ignore.breaks = true;
context.ignore.continues = true;
if (this.body.hasEffects(context)) return true;
context.ignore.breaks = breaks;
context.ignore.continues = continues;
context.breakFlow = breakFlow;
context.brokenFlow = brokenFlow;
return false;
}

include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) {
this.included = true;
if (this.init) this.init.include(context, includeChildrenRecursively);
if (this.test) this.test.include(context, includeChildrenRecursively);
const { breakFlow } = context;
const { brokenFlow } = context;
if (this.update) this.update.include(context, includeChildrenRecursively);
this.body.include(context, includeChildrenRecursively);
context.breakFlow = breakFlow;
context.brokenFlow = brokenFlow;
}

render(code: MagicString, options: RenderOptions) {
Expand Down
44 changes: 12 additions & 32 deletions src/ast/nodes/IfStatement.ts
Expand Up @@ -2,12 +2,7 @@ import MagicString from 'magic-string';
import { RenderOptions } from '../../utils/renderHelpers';
import { removeAnnotations } from '../../utils/treeshakeNode';
import { DeoptimizableEntity } from '../DeoptimizableEntity';
import {
BreakFlow,
BREAKFLOW_NONE,
HasEffectsContext,
InclusionContext
} from '../ExecutionContext';
import { BROKEN_FLOW_NONE, HasEffectsContext, InclusionContext } from '../ExecutionContext';
import { EMPTY_IMMUTABLE_TRACKER, EMPTY_PATH } from '../utils/PathTracker';
import { LiteralValueOrUnknown, UnknownValue } from '../values';
import * as NodeType from './NodeType';
Expand All @@ -33,13 +28,14 @@ export default class IfStatement extends StatementBase implements DeoptimizableE
hasEffects(context: HasEffectsContext): boolean {
if (this.test.hasEffects(context)) return true;
if (this.testValue === UnknownValue) {
const { breakFlow } = context;
const { brokenFlow } = context;
if (this.consequent.hasEffects(context)) return true;
const consequentBreakFlow = context.breakFlow;
context.breakFlow = breakFlow;
const consequentBrokenFlow = context.brokenFlow;
context.brokenFlow = brokenFlow;
if (this.alternate === null) return false;
if (this.alternate.hasEffects(context)) return true;
this.updateBreakFlowUnknownCondition(consequentBreakFlow, context);
context.brokenFlow =
context.brokenFlow < consequentBrokenFlow ? context.brokenFlow : consequentBrokenFlow;
return false;
}
return this.testValue
Expand Down Expand Up @@ -119,33 +115,17 @@ export default class IfStatement extends StatementBase implements DeoptimizableE

private includeUnknownTest(context: InclusionContext) {
this.test.include(context, false);
const { breakFlow } = context;
let consequentBreakFlow: BreakFlow | false = false;
const { brokenFlow } = context;
let consequentBrokenFlow = BROKEN_FLOW_NONE;
if (this.consequent.shouldBeIncluded(context)) {
this.consequent.include(context, false);
consequentBreakFlow = context.breakFlow;
context.breakFlow = breakFlow;
consequentBrokenFlow = context.brokenFlow;
context.brokenFlow = brokenFlow;
}
if (this.alternate !== null && this.alternate.shouldBeIncluded(context)) {
this.alternate.include(context, false);
this.updateBreakFlowUnknownCondition(consequentBreakFlow, context);
}
}

private updateBreakFlowUnknownCondition(
consequentBreakFlow: BreakFlow | false,
context: InclusionContext
) {
if (!(consequentBreakFlow && context.breakFlow)) {
context.breakFlow = BREAKFLOW_NONE;
} else if (context.breakFlow instanceof Set) {
if (consequentBreakFlow instanceof Set) {
for (const label of consequentBreakFlow) {
context.breakFlow.add(label);
}
}
} else {
context.breakFlow = consequentBreakFlow;
context.brokenFlow =
context.brokenFlow < consequentBrokenFlow ? context.brokenFlow : consequentBrokenFlow;
}
}
}

0 comments on commit 121a7f4

Please sign in to comment.