diff --git a/src/ast/ExecutionContext.ts b/src/ast/ExecutionContext.ts index 3be3d1937f2..d4ae7695c63 100644 --- a/src/ast/ExecutionContext.ts +++ b/src/ast/ExecutionContext.ts @@ -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; +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; } export interface HasEffectsContext extends InclusionContext { accessed: PathTracker; assigned: PathTracker; + brokenFlow: number; called: PathTracker; ignore: ExecutionContextIgnore; instantiated: PathTracker; @@ -29,7 +30,8 @@ export interface HasEffectsContext extends InclusionContext { export function createInclusionContext(): InclusionContext { return { - breakFlow: BREAKFLOW_NONE + brokenFlow: BROKEN_FLOW_NONE, + includedLabels: new Set() }; } @@ -37,7 +39,7 @@ export function createHasEffectsContext(): HasEffectsContext { return { accessed: new PathTracker(), assigned: new PathTracker(), - breakFlow: BREAKFLOW_NONE, + brokenFlow: BROKEN_FLOW_NONE, called: new PathTracker(), ignore: { breaks: false, @@ -45,6 +47,7 @@ export function createHasEffectsContext(): HasEffectsContext { labels: new Set(), returnAwaitYield: false }, + includedLabels: new Set(), instantiated: new PathTracker(), replacedVariableInits: new Map() }; diff --git a/src/ast/nodes/ArrowFunctionExpression.ts b/src/ast/nodes/ArrowFunctionExpression.ts index a687067dfbf..005499bc725 100644 --- a/src/ast/nodes/ArrowFunctionExpression.ts +++ b/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'; @@ -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, @@ -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; } @@ -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 { diff --git a/src/ast/nodes/BlockStatement.ts b/src/ast/nodes/BlockStatement.ts index 1752d5907dd..9a01cf0972c 100644 --- a/src/ast/nodes/BlockStatement.ts +++ b/src/ast/nodes/BlockStatement.ts @@ -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; } diff --git a/src/ast/nodes/BreakStatement.ts b/src/ast/nodes/BreakStatement.ts index 7e3cc267d45..98c8851bd1a 100644 --- a/src/ast/nodes/BreakStatement.ts +++ b/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'; @@ -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; } } diff --git a/src/ast/nodes/ContinueStatement.ts b/src/ast/nodes/ContinueStatement.ts index 5db16a88b3d..337fdf977a1 100644 --- a/src/ast/nodes/ContinueStatement.ts +++ b/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'; @@ -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; } } diff --git a/src/ast/nodes/DoWhileStatement.ts b/src/ast/nodes/DoWhileStatement.ts index b4c0228ebe3..d578895dcf5 100644 --- a/src/ast/nodes/DoWhileStatement.ts +++ b/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'; @@ -10,7 +14,7 @@ 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; @@ -18,8 +22,8 @@ export default class DoWhileStatement extends StatementBase { 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; } @@ -27,10 +31,10 @@ export default class DoWhileStatement extends StatementBase { 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; } } } diff --git a/src/ast/nodes/ForInStatement.ts b/src/ast/nodes/ForInStatement.ts index 4962edc896b..09dba4bb930 100644 --- a/src/ast/nodes/ForInStatement.ts +++ b/src/ast/nodes/ForInStatement.ts @@ -35,7 +35,7 @@ export default class ForInStatement extends StatementBase { ) return true; const { - breakFlow, + brokenFlow, ignore: { breaks, continues } } = context; context.ignore.breaks = true; @@ -43,7 +43,7 @@ export default class ForInStatement extends StatementBase { if (this.body.hasEffects(context)) return true; context.ignore.breaks = breaks; context.ignore.continues = continues; - context.breakFlow = breakFlow; + context.brokenFlow = brokenFlow; return false; } @@ -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) { diff --git a/src/ast/nodes/ForOfStatement.ts b/src/ast/nodes/ForOfStatement.ts index 71cd0e0b1bc..29b0a081f1a 100644 --- a/src/ast/nodes/ForOfStatement.ts +++ b/src/ast/nodes/ForOfStatement.ts @@ -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) { diff --git a/src/ast/nodes/ForStatement.ts b/src/ast/nodes/ForStatement.ts index b033833a9cb..1e25463d10b 100644 --- a/src/ast/nodes/ForStatement.ts +++ b/src/ast/nodes/ForStatement.ts @@ -26,7 +26,7 @@ export default class ForStatement extends StatementBase { ) return true; const { - breakFlow, + brokenFlow, ignore: { breaks, continues } } = context; context.ignore.breaks = true; @@ -34,7 +34,7 @@ export default class ForStatement extends StatementBase { if (this.body.hasEffects(context)) return true; context.ignore.breaks = breaks; context.ignore.continues = continues; - context.breakFlow = breakFlow; + context.brokenFlow = brokenFlow; return false; } @@ -42,10 +42,10 @@ export default class ForStatement extends StatementBase { 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) { diff --git a/src/ast/nodes/IfStatement.ts b/src/ast/nodes/IfStatement.ts index 73b6bb944f6..5b91d33d2c6 100644 --- a/src/ast/nodes/IfStatement.ts +++ b/src/ast/nodes/IfStatement.ts @@ -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'; @@ -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 @@ -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; } } } diff --git a/src/ast/nodes/LabeledStatement.ts b/src/ast/nodes/LabeledStatement.ts index 1abb654f568..78eb0daffdc 100644 --- a/src/ast/nodes/LabeledStatement.ts +++ b/src/ast/nodes/LabeledStatement.ts @@ -1,4 +1,6 @@ -import { BREAKFLOW_NONE, HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import MagicString from 'magic-string'; +import { findFirstOccurrenceOutsideComment, RenderOptions } from '../../utils/renderHelpers'; +import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import Identifier from './Identifier'; import * as NodeType from './NodeType'; import { IncludeChildren, StatementBase, StatementNode } from './shared/Node'; @@ -9,21 +11,37 @@ export default class LabeledStatement extends StatementBase { type!: NodeType.tLabeledStatement; hasEffects(context: HasEffectsContext) { + const brokenFlow = context.brokenFlow; context.ignore.labels.add(this.label.name); if (this.body.hasEffects(context)) return true; context.ignore.labels.delete(this.label.name); - if (context.breakFlow instanceof Set && context.breakFlow.has(this.label.name)) { - context.breakFlow = BREAKFLOW_NONE; + if (context.includedLabels.has(this.label.name)) { + context.includedLabels.delete(this.label.name); + context.brokenFlow = brokenFlow; } return false; } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { this.included = true; - this.label.include(context); + const brokenFlow = context.brokenFlow; this.body.include(context, includeChildrenRecursively); - if (context.breakFlow instanceof Set && context.breakFlow.has(this.label.name)) { - context.breakFlow = BREAKFLOW_NONE; + if (context.includedLabels.has(this.label.name)) { + this.label.include(context); + context.includedLabels.delete(this.label.name); + context.brokenFlow = brokenFlow; } } + + render(code: MagicString, options: RenderOptions) { + if (this.label.included) { + this.label.render(code, options); + } else { + code.remove( + this.start, + findFirstOccurrenceOutsideComment(code.original, ':', this.label.end) + 1 + ); + } + this.body.render(code, options); + } } diff --git a/src/ast/nodes/ReturnStatement.ts b/src/ast/nodes/ReturnStatement.ts index 9b434540943..87bec1a78c2 100644 --- a/src/ast/nodes/ReturnStatement.ts +++ b/src/ast/nodes/ReturnStatement.ts @@ -1,6 +1,10 @@ import MagicString from 'magic-string'; import { RenderOptions } from '../../utils/renderHelpers'; -import { BREAKFLOW_ERROR_RETURN, HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { + BROKEN_FLOW_ERROR_RETURN_LABEL, + HasEffectsContext, + InclusionContext +} from '../ExecutionContext'; import { UNKNOWN_EXPRESSION } from '../values'; import * as NodeType from './NodeType'; import { ExpressionNode, IncludeChildren, StatementBase } from './shared/Node'; @@ -15,7 +19,7 @@ export default class ReturnStatement extends StatementBase { (this.argument !== null && this.argument.hasEffects(context)) ) return true; - context.breakFlow = BREAKFLOW_ERROR_RETURN; + context.brokenFlow = BROKEN_FLOW_ERROR_RETURN_LABEL; return false; } @@ -24,7 +28,7 @@ export default class ReturnStatement extends StatementBase { if (this.argument) { this.argument.include(context, includeChildrenRecursively); } - context.breakFlow = BREAKFLOW_ERROR_RETURN; + context.brokenFlow = BROKEN_FLOW_ERROR_RETURN_LABEL; } initialise() { diff --git a/src/ast/nodes/SwitchCase.ts b/src/ast/nodes/SwitchCase.ts index 28be3c788ab..376f72cb521 100644 --- a/src/ast/nodes/SwitchCase.ts +++ b/src/ast/nodes/SwitchCase.ts @@ -18,7 +18,7 @@ export default class SwitchCase extends NodeBase { hasEffects(context: HasEffectsContext): boolean { if (this.test && this.test.hasEffects(context)) return true; for (const node of this.consequent) { - if (context.breakFlow) break; + if (context.brokenFlow) break; if (node.hasEffects(context)) return true; } return false; diff --git a/src/ast/nodes/SwitchStatement.ts b/src/ast/nodes/SwitchStatement.ts index 49c820821a5..c97b64b75f7 100644 --- a/src/ast/nodes/SwitchStatement.ts +++ b/src/ast/nodes/SwitchStatement.ts @@ -1,9 +1,7 @@ import MagicString from 'magic-string'; import { RenderOptions, renderStatementList } from '../../utils/renderHelpers'; import { - BreakFlow, - BREAKFLOW_ERROR_RETURN, - BREAKFLOW_NONE, + BROKEN_FLOW_BREAK_CONTINUE, createHasEffectsContext, HasEffectsContext, InclusionContext @@ -14,24 +12,6 @@ import * as NodeType from './NodeType'; import { ExpressionNode, IncludeChildren, StatementBase } from './shared/Node'; import SwitchCase from './SwitchCase'; -function getMinBreakflowAfterCase( - minBreakFlow: BreakFlow | false, - context: InclusionContext -): BreakFlow | false { - if (!(minBreakFlow && context.breakFlow)) { - return BREAKFLOW_NONE; - } - if (minBreakFlow instanceof Set) { - if (context.breakFlow instanceof Set) { - for (const label of context.breakFlow) { - minBreakFlow.add(label); - } - } - return minBreakFlow; - } - return context.breakFlow; -} - export default class SwitchStatement extends StatementBase { cases!: SwitchCase[]; discriminant!: ExpressionNode; @@ -46,18 +26,18 @@ export default class SwitchStatement extends StatementBase { hasEffects(context: HasEffectsContext) { if (this.discriminant.hasEffects(context)) return true; const { - breakFlow, + brokenFlow, ignore: { breaks } } = context; - let minBreakFlow: BreakFlow | false = BREAKFLOW_ERROR_RETURN; + let minBrokenFlow = Infinity; context.ignore.breaks = true; for (const switchCase of this.cases) { if (switchCase.hasEffects(context)) return true; - minBreakFlow = getMinBreakflowAfterCase(minBreakFlow, context); - context.breakFlow = breakFlow; + minBrokenFlow = context.brokenFlow < minBrokenFlow ? context.brokenFlow : minBrokenFlow; + context.brokenFlow = brokenFlow; } - if (this.defaultCase !== null && !(minBreakFlow instanceof Set && minBreakFlow.has(null))) { - context.breakFlow = minBreakFlow; + if (this.defaultCase !== null && !(minBrokenFlow === BROKEN_FLOW_BREAK_CONTINUE)) { + context.brokenFlow = minBrokenFlow; } context.ignore.breaks = breaks; return false; @@ -66,8 +46,8 @@ export default class SwitchStatement extends StatementBase { include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { this.included = true; this.discriminant.include(context, includeChildrenRecursively); - const { breakFlow } = context; - let minBreakFlow: BreakFlow | false = BREAKFLOW_ERROR_RETURN; + const { brokenFlow } = context; + let minBrokenFlow = Infinity; let isCaseIncluded = includeChildrenRecursively || (this.defaultCase !== null && this.defaultCase < this.cases.length - 1); @@ -83,12 +63,12 @@ export default class SwitchStatement extends StatementBase { } if (isCaseIncluded) { switchCase.include(context, includeChildrenRecursively); - minBreakFlow = getMinBreakflowAfterCase(minBreakFlow, context); - context.breakFlow = breakFlow; + minBrokenFlow = minBrokenFlow < context.brokenFlow ? minBrokenFlow : context.brokenFlow; + context.brokenFlow = brokenFlow; } } - if (this.defaultCase !== null && !(minBreakFlow instanceof Set && minBreakFlow.has(null))) { - context.breakFlow = minBreakFlow; + if (this.defaultCase !== null && !(minBrokenFlow === BROKEN_FLOW_BREAK_CONTINUE)) { + context.brokenFlow = minBrokenFlow; } } diff --git a/src/ast/nodes/ThrowStatement.ts b/src/ast/nodes/ThrowStatement.ts index aab2d944b34..3c6bbfffef5 100644 --- a/src/ast/nodes/ThrowStatement.ts +++ b/src/ast/nodes/ThrowStatement.ts @@ -1,6 +1,6 @@ import MagicString from 'magic-string'; import { RenderOptions } from '../../utils/renderHelpers'; -import { BREAKFLOW_ERROR_RETURN, InclusionContext } from '../ExecutionContext'; +import { BROKEN_FLOW_ERROR_RETURN_LABEL, InclusionContext } from '../ExecutionContext'; import * as NodeType from './NodeType'; import { ExpressionNode, IncludeChildren, StatementBase } from './shared/Node'; @@ -15,7 +15,7 @@ export default class ThrowStatement extends StatementBase { include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { this.included = true; this.argument.include(context, includeChildrenRecursively); - context.breakFlow = BREAKFLOW_ERROR_RETURN; + context.brokenFlow = BROKEN_FLOW_ERROR_RETURN_LABEL; } render(code: MagicString, options: RenderOptions) { diff --git a/src/ast/nodes/TryStatement.ts b/src/ast/nodes/TryStatement.ts index fa265171639..d6f499ca88c 100644 --- a/src/ast/nodes/TryStatement.ts +++ b/src/ast/nodes/TryStatement.ts @@ -22,7 +22,7 @@ export default class TryStatement extends StatementBase { } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { - const { breakFlow } = context; + const { brokenFlow } = context; if (!this.directlyIncluded || !this.context.tryCatchDeoptimization) { this.included = true; this.directlyIncluded = true; @@ -30,11 +30,11 @@ export default class TryStatement extends StatementBase { context, this.context.tryCatchDeoptimization ? INCLUDE_PARAMETERS : includeChildrenRecursively ); - context.breakFlow = breakFlow; + context.brokenFlow = brokenFlow; } if (this.handler !== null) { this.handler.include(context, includeChildrenRecursively); - context.breakFlow = breakFlow; + context.brokenFlow = brokenFlow; } if (this.finalizer !== null) { this.finalizer.include(context, includeChildrenRecursively); diff --git a/src/ast/nodes/WhileStatement.ts b/src/ast/nodes/WhileStatement.ts index ff9d1ad9ea9..5deb69d1479 100644 --- a/src/ast/nodes/WhileStatement.ts +++ b/src/ast/nodes/WhileStatement.ts @@ -10,7 +10,7 @@ export default class WhileStatement extends StatementBase { hasEffects(context: HasEffectsContext): boolean { if (this.test.hasEffects(context)) return true; const { - breakFlow, + brokenFlow, ignore: { breaks, continues } } = context; context.ignore.breaks = true; @@ -18,15 +18,15 @@ export default class WhileStatement extends StatementBase { 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; this.test.include(context, includeChildrenRecursively); - const { breakFlow } = context; + const { brokenFlow } = context; this.body.include(context, includeChildrenRecursively); - context.breakFlow = breakFlow; + context.brokenFlow = brokenFlow; } } diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index 0e17ad156ad..5b7bc4503f8 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.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 FunctionScope from '../../scopes/FunctionScope'; import { ObjectPath, UNKNOWN_PATH, UnknownKey } from '../../utils/PathTracker'; import { UNKNOWN_EXPRESSION, UnknownObjectExpression } from '../../values'; @@ -72,7 +72,7 @@ export default class FunctionNode extends NodeBase { this.scope.thisVariable, callOptions.withNew ? new UnknownObjectExpression() : UNKNOWN_EXPRESSION ); - const { breakFlow, ignore } = context; + const { brokenFlow, ignore } = context; context.ignore = { breaks: false, continues: false, @@ -80,7 +80,7 @@ export default class FunctionNode extends NodeBase { returnAwaitYield: true }; if (this.body.hasEffects(context)) return true; - context.breakFlow = breakFlow; + context.brokenFlow = brokenFlow; if (thisInit) { context.replacedVariableInits.set(this.scope.thisVariable, thisInit); } else { @@ -99,10 +99,10 @@ export default class FunctionNode 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 { diff --git a/src/ast/nodes/shared/Node.ts b/src/ast/nodes/shared/Node.ts index 6e5842aabd3..31f908e784b 100644 --- a/src/ast/nodes/shared/Node.ts +++ b/src/ast/nodes/shared/Node.ts @@ -277,7 +277,7 @@ export class NodeBase implements ExpressionNode { } shouldBeIncluded(context: InclusionContext): boolean { - return this.included || (!context.breakFlow && this.hasEffects(createHasEffectsContext())); + return this.included || (!context.brokenFlow && this.hasEffects(createHasEffectsContext())); } toString() { diff --git a/test/form/samples/break-control-flow/break-statement-labels-do-while/_expected.js b/test/form/samples/break-control-flow/break-statement-labels-do-while/_expected.js index 26d7470af53..a2119dbc632 100644 --- a/test/form/samples/break-control-flow/break-statement-labels-do-while/_expected.js +++ b/test/form/samples/break-control-flow/break-statement-labels-do-while/_expected.js @@ -1,4 +1,4 @@ -label: do { +do { console.log('retained'); } while (globalThis.unknown); @@ -6,7 +6,7 @@ do { console.log('retained'); } while (globalThis.unknown); -label: do { + do { console.log('retained'); } while (globalThis.unknown); diff --git a/test/form/samples/break-control-flow/break-statement-labels-in-loops/_expected.js b/test/form/samples/break-control-flow/break-statement-labels-in-loops/_expected.js index 61e440a1a43..13a13529a71 100644 --- a/test/form/samples/break-control-flow/break-statement-labels-in-loops/_expected.js +++ b/test/form/samples/break-control-flow/break-statement-labels-in-loops/_expected.js @@ -1,6 +1,6 @@ while (globalThis.unknown) { console.log('retained'); - label: { + { break; } } diff --git a/test/form/samples/break-control-flow/break-statement-labels/_expected.js b/test/form/samples/break-control-flow/break-statement-labels/_expected.js index d7080a80bb6..623a4b6c02d 100644 --- a/test/form/samples/break-control-flow/break-statement-labels/_expected.js +++ b/test/form/samples/break-control-flow/break-statement-labels/_expected.js @@ -13,7 +13,7 @@ outer: { } outer: { - inner: { + /* retained comment */ { console.log('retained'); break outer; } @@ -26,21 +26,21 @@ outer: { outer: { inner: { if (globalThis.unknown) break inner; - else break outer; + break outer; } console.log('retained'); } function withConsequentReturn() { - outer: { + { inner: { if (globalThis.unknown) return; else break inner; } console.log('retained'); } - outer: { - inner: { + { + { return; } } @@ -49,7 +49,7 @@ function withConsequentReturn() { withConsequentReturn(); function withAlternateReturn() { - outer: { + { inner: { if (globalThis.unknown) break inner; else return; diff --git a/test/form/samples/break-control-flow/break-statement-labels/main.js b/test/form/samples/break-control-flow/break-statement-labels/main.js index b7ac267b7f1..688bb5cba69 100644 --- a/test/form/samples/break-control-flow/break-statement-labels/main.js +++ b/test/form/samples/break-control-flow/break-statement-labels/main.js @@ -20,7 +20,7 @@ outer: { } outer: { - inner: { + inner:/* retained comment */ { console.log('retained'); break outer; console.log('removed'); @@ -42,7 +42,7 @@ outer: { outer: { inner: { if (globalThis.unknown) break inner; - else break outer; + break outer; console.log('removed'); } console.log('retained'); diff --git a/test/form/samples/break-control-flow/caught-errors/_expected.js b/test/form/samples/break-control-flow/caught-errors/_expected.js index db3d5a2dc25..81df5882ea4 100644 --- a/test/form/samples/break-control-flow/caught-errors/_expected.js +++ b/test/form/samples/break-control-flow/caught-errors/_expected.js @@ -59,3 +59,17 @@ function tryAfterError() { try { tryAfterError(); } catch {} + +function errorTryNoCatch() { + try { + throw new Error('Break'); + } finally { + console.log('retained'); + } + + console.log('retained'); +} + +try { + errorTryNoCatch(); +} catch {} diff --git a/test/form/samples/break-control-flow/caught-errors/main.js b/test/form/samples/break-control-flow/caught-errors/main.js index 432952087f8..4ff0eb06c67 100644 --- a/test/form/samples/break-control-flow/caught-errors/main.js +++ b/test/form/samples/break-control-flow/caught-errors/main.js @@ -68,3 +68,18 @@ function tryAfterError() { try { tryAfterError(); } catch {} + +function errorTryNoCatch() { + try { + throw new Error('Break'); + console.log('removed'); + } finally { + console.log('retained'); + } + + console.log('retained'); +} + +try { + errorTryNoCatch(); +} catch {} diff --git a/test/form/samples/break-control-flow/if-statement-breaks/_config.js b/test/form/samples/break-control-flow/if-statement-breaks/_config.js new file mode 100644 index 00000000000..642237e7aa8 --- /dev/null +++ b/test/form/samples/break-control-flow/if-statement-breaks/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles conditionally broken execution' +}; diff --git a/test/form/samples/break-control-flow/if-statement-breaks/_expected.js b/test/form/samples/break-control-flow/if-statement-breaks/_expected.js new file mode 100644 index 00000000000..cc62b9924d5 --- /dev/null +++ b/test/form/samples/break-control-flow/if-statement-breaks/_expected.js @@ -0,0 +1,104 @@ +function unknownValueConsequent() { + if (globalThis.unknownValue) { + throw new Error(); + } else { + console.log('retained'); + } + console.log('retained'); +} + +try { + unknownValueConsequent(); +} catch {} + +function unknownValueOnlyConsequent() { + if (globalThis.unknownValue) { + throw new Error(); + } + console.log('retained'); +} + +try { + unknownValueOnlyConsequent(); +} catch {} + +function unknownValueAlternate() { + if (globalThis.unknownValue) { + console.log('retained'); + } else { + throw new Error(); + } + console.log('retained'); +} + +try { + unknownValueAlternate(); +} catch {} + +function unknownValueBoth() { + if (globalThis.unknownValue) { + throw new Error(); + } else { + throw new Error(); + } +} + +try { + unknownValueBoth(); +} catch {} + +function unknownValueAfterError() { + console.log(hoisted1, hoisted2); + throw new Error(); + if (globalThis.unknownValue) { + var hoisted1; + } else { + var hoisted2; + } +} + +try { + unknownValueAfterError(); +} catch {} + +function truthyValueConsequent() { + { + throw new Error(); + } +} + +try { + truthyValueConsequent(); +} catch {} + +function truthyValueAlternate() { + { + console.log('retained'); + } + console.log('retained'); +} + +try { + truthyValueAlternate(); +} catch {} + +function falsyValueConsequent() { + { + console.log('retained'); + } + console.log('retained'); +} + +try { + falsyValueConsequent(); +} catch {} + +function falsyValueAlternate() { + { + throw new Error(); + } +} + +try { + falsyValueAlternate(); +} catch {} diff --git a/test/form/samples/break-control-flow/if-statement-breaks/main.js b/test/form/samples/break-control-flow/if-statement-breaks/main.js new file mode 100644 index 00000000000..804d058fa8e --- /dev/null +++ b/test/form/samples/break-control-flow/if-statement-breaks/main.js @@ -0,0 +1,125 @@ +function unknownValueConsequent() { + if (globalThis.unknownValue) { + throw new Error(); + console.log('removed'); + } else { + console.log('retained'); + } + console.log('retained'); +} + +try { + unknownValueConsequent(); +} catch {} + +function unknownValueOnlyConsequent() { + if (globalThis.unknownValue) { + throw new Error(); + console.log('removed'); + } + console.log('retained'); +} + +try { + unknownValueOnlyConsequent(); +} catch {} + +function unknownValueAlternate() { + if (globalThis.unknownValue) { + console.log('retained'); + } else { + throw new Error(); + console.log('removed'); + } + console.log('retained'); +} + +try { + unknownValueAlternate(); +} catch {} + +function unknownValueBoth() { + if (globalThis.unknownValue) { + throw new Error(); + console.log('removed'); + } else { + throw new Error(); + console.log('removed'); + } + console.log('removed'); +} + +try { + unknownValueBoth(); +} catch {} + +function unknownValueAfterError() { + console.log(hoisted1, hoisted2); + throw new Error(); + if (globalThis.unknownValue) { + console.log('removed'); + var hoisted1; + } else { + console.log('removed'); + var hoisted2; + } + console.log('removed'); +} + +try { + unknownValueAfterError(); +} catch {} + +function truthyValueConsequent() { + if (true) { + throw new Error(); + console.log('removed'); + } else { + console.log('removed'); + } + console.log('removed'); +} + +try { + truthyValueConsequent(); +} catch {} + +function truthyValueAlternate() { + if (true) { + console.log('retained'); + } else { + throw new Error(); + } + console.log('retained'); +} + +try { + truthyValueAlternate(); +} catch {} + +function falsyValueConsequent() { + if (false) { + throw new Error(); + } else { + console.log('retained'); + } + console.log('retained'); +} + +try { + falsyValueConsequent(); +} catch {} + +function falsyValueAlternate() { + if (false) { + console.log('removed'); + } else { + throw new Error(); + console.log('removed'); + } + console.log('removed'); +} + +try { + falsyValueAlternate(); +} catch {} diff --git a/test/form/samples/break-control-flow/if-statement-errors/_expected.js b/test/form/samples/break-control-flow/if-statement-errors/_expected.js index cc62b9924d5..def27731e3e 100644 --- a/test/form/samples/break-control-flow/if-statement-errors/_expected.js +++ b/test/form/samples/break-control-flow/if-statement-errors/_expected.js @@ -1,104 +1,11 @@ -function unknownValueConsequent() { - if (globalThis.unknownValue) { - throw new Error(); - } else { - console.log('retained'); - } - console.log('retained'); -} - -try { - unknownValueConsequent(); -} catch {} - -function unknownValueOnlyConsequent() { - if (globalThis.unknownValue) { - throw new Error(); - } - console.log('retained'); -} - -try { - unknownValueOnlyConsequent(); -} catch {} - -function unknownValueAlternate() { - if (globalThis.unknownValue) { - console.log('retained'); - } else { - throw new Error(); - } - console.log('retained'); -} - -try { - unknownValueAlternate(); -} catch {} - -function unknownValueBoth() { - if (globalThis.unknownValue) { - throw new Error(); - } else { - throw new Error(); - } -} - -try { - unknownValueBoth(); -} catch {} - -function unknownValueAfterError() { - console.log(hoisted1, hoisted2); - throw new Error(); - if (globalThis.unknownValue) { - var hoisted1; - } else { - var hoisted2; - } -} - -try { - unknownValueAfterError(); -} catch {} - -function truthyValueConsequent() { - { - throw new Error(); - } -} - -try { - truthyValueConsequent(); -} catch {} - -function truthyValueAlternate() { - { - console.log('retained'); - } +while (true) { + if (globalThis.unknown) break; console.log('retained'); } -try { - truthyValueAlternate(); -} catch {} - -function falsyValueConsequent() { - { - console.log('retained'); +while (true) { + if (globalThis.unknown) ; else { + break; } console.log('retained'); } - -try { - falsyValueConsequent(); -} catch {} - -function falsyValueAlternate() { - { - throw new Error(); - } -} - -try { - falsyValueAlternate(); -} catch {} diff --git a/test/form/samples/break-control-flow/if-statement-errors/main.js b/test/form/samples/break-control-flow/if-statement-errors/main.js index 804d058fa8e..53e94b74dcf 100644 --- a/test/form/samples/break-control-flow/if-statement-errors/main.js +++ b/test/form/samples/break-control-flow/if-statement-errors/main.js @@ -1,125 +1,16 @@ -function unknownValueConsequent() { - if (globalThis.unknownValue) { - throw new Error(); - console.log('removed'); - } else { - console.log('retained'); - } - console.log('retained'); -} - -try { - unknownValueConsequent(); -} catch {} - -function unknownValueOnlyConsequent() { - if (globalThis.unknownValue) { - throw new Error(); - console.log('removed'); - } - console.log('retained'); -} - -try { - unknownValueOnlyConsequent(); -} catch {} - -function unknownValueAlternate() { - if (globalThis.unknownValue) { - console.log('retained'); - } else { - throw new Error(); - console.log('removed'); - } - console.log('retained'); -} - -try { - unknownValueAlternate(); -} catch {} - -function unknownValueBoth() { - if (globalThis.unknownValue) { - throw new Error(); - console.log('removed'); - } else { - throw new Error(); - console.log('removed'); - } - console.log('removed'); -} - -try { - unknownValueBoth(); -} catch {} - -function unknownValueAfterError() { - console.log(hoisted1, hoisted2); - throw new Error(); - if (globalThis.unknownValue) { - console.log('removed'); - var hoisted1; - } else { - console.log('removed'); - var hoisted2; - } - console.log('removed'); -} - -try { - unknownValueAfterError(); -} catch {} - -function truthyValueConsequent() { - if (true) { - throw new Error(); - console.log('removed'); - } else { - console.log('removed'); - } - console.log('removed'); -} - -try { - truthyValueConsequent(); -} catch {} - -function truthyValueAlternate() { - if (true) { - console.log('retained'); - } else { - throw new Error(); +while (true) { + if (globalThis.unknown) break; + else { + const unused = 1; } console.log('retained'); } -try { - truthyValueAlternate(); -} catch {} - -function falsyValueConsequent() { - if (false) { - throw new Error(); +while (true) { + if (globalThis.unknown) { + const unused = 1; } else { - console.log('retained'); + break; } console.log('retained'); } - -try { - falsyValueConsequent(); -} catch {} - -function falsyValueAlternate() { - if (false) { - console.log('removed'); - } else { - throw new Error(); - console.log('removed'); - } - console.log('removed'); -} - -try { - falsyValueAlternate(); -} catch {} diff --git a/test/function/samples/labeled-statement-conditional-break/_config.js b/test/function/samples/labeled-statement-conditional-break/_config.js new file mode 100644 index 00000000000..01c552f861b --- /dev/null +++ b/test/function/samples/labeled-statement-conditional-break/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles conditional control flow due to labeled statements' +}; diff --git a/test/function/samples/labeled-statement-conditional-break/main.js b/test/function/samples/labeled-statement-conditional-break/main.js new file mode 100644 index 00000000000..9a9aa59f32e --- /dev/null +++ b/test/function/samples/labeled-statement-conditional-break/main.js @@ -0,0 +1,17 @@ +let ok = false; + +function test() { + a: { + for (let i = 0; i < 2; i++) { + if (i === 1) { + break a; + } + } + return; + } + ok = true; +} + +test(); + +assert.ok(ok);