diff --git a/rollup.config.ts b/rollup.config.ts index 4bf5ee3c383..77c5cac5bec 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -6,7 +6,7 @@ import commonjs from '@rollup/plugin-commonjs'; import json from '@rollup/plugin-json'; import { nodeResolve } from '@rollup/plugin-node-resolve'; import typescript from '@rollup/plugin-typescript'; -import type { RollupOptions, WarningHandlerWithDefault } from 'rollup'; +import type { Plugin, RollupOptions, WarningHandlerWithDefault } from 'rollup'; import { string } from 'rollup-plugin-string'; import { terser } from 'rollup-plugin-terser'; import addCliEntry from './build-plugins/add-cli-entry'; @@ -65,7 +65,7 @@ const treeshake = { tryCatchDeoptimization: false }; -const nodePlugins = [ +const nodePlugins: Plugin[] = [ alias(moduleAliases), nodeResolve(), json(), diff --git a/src/ast/nodes/ArrayExpression.ts b/src/ast/nodes/ArrayExpression.ts index 406687d99fb..d8c6f9193af 100644 --- a/src/ast/nodes/ArrayExpression.ts +++ b/src/ast/nodes/ArrayExpression.ts @@ -2,7 +2,12 @@ import type { CallOptions } from '../CallOptions'; import type { DeoptimizableEntity } from '../DeoptimizableEntity'; import type { HasEffectsContext } from '../ExecutionContext'; import type { NodeEvent } from '../NodeEvents'; -import { type ObjectPath, type PathTracker, UnknownInteger } from '../utils/PathTracker'; +import { + type ObjectPath, + type PathTracker, + UNKNOWN_PATH, + UnknownInteger +} from '../utils/PathTracker'; import { UNDEFINED_EXPRESSION, UNKNOWN_LITERAL_NUMBER } from '../values'; import type * as NodeType from './NodeType'; import SpreadElement from './SpreadElement'; @@ -14,6 +19,7 @@ import { ObjectEntity, type ObjectProperty } from './shared/ObjectEntity'; export default class ArrayExpression extends NodeBase { declare elements: readonly (ExpressionNode | SpreadElement | null)[]; declare type: NodeType.tArrayExpression; + protected deoptimized = false; private objectEntity: ObjectEntity | null = null; deoptimizePath(path: ObjectPath): void { @@ -72,6 +78,21 @@ export default class ArrayExpression extends NodeBase { return this.getObjectEntity().hasEffectsWhenCalledAtPath(path, callOptions, context); } + protected applyDeoptimizations(): void { + this.deoptimized = true; + let hasSpread = false; + for (let index = 0; index < this.elements.length; index++) { + const element = this.elements[index]; + if (hasSpread || element instanceof SpreadElement) { + if (element) { + hasSpread = true; + element.deoptimizePath(UNKNOWN_PATH); + } + } + } + this.context.requestTreeshakingPass(); + } + private getObjectEntity(): ObjectEntity { if (this.objectEntity !== null) { return this.objectEntity; @@ -82,7 +103,7 @@ export default class ArrayExpression extends NodeBase { let hasSpread = false; for (let index = 0; index < this.elements.length; index++) { const element = this.elements[index]; - if (element instanceof SpreadElement || hasSpread) { + if (hasSpread || element instanceof SpreadElement) { if (element) { hasSpread = true; properties.unshift({ key: UnknownInteger, kind: 'init', property: element }); diff --git a/src/ast/nodes/ArrayPattern.ts b/src/ast/nodes/ArrayPattern.ts index 36c98606ddf..aab64901f49 100644 --- a/src/ast/nodes/ArrayPattern.ts +++ b/src/ast/nodes/ArrayPattern.ts @@ -16,9 +16,7 @@ export default class ArrayPattern extends NodeBase implements PatternNode { exportNamesByVariable: ReadonlyMap ): void { for (const element of this.elements) { - if (element !== null) { - element.addExportedVariables(variables, exportNamesByVariable); - } + element?.addExportedVariables(variables, exportNamesByVariable); } } @@ -32,30 +30,24 @@ export default class ArrayPattern extends NodeBase implements PatternNode { return variables; } - deoptimizePath(path: ObjectPath): void { - if (path.length === 0) { - for (const element of this.elements) { - if (element !== null) { - element.deoptimizePath(path); - } - } + // Patterns can only be deoptimized at the empty path at the moment + deoptimizePath(): void { + for (const element of this.elements) { + element?.deoptimizePath(EMPTY_PATH); } } - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length > 0) return true; + // Patterns are only checked at the emtpy path at the moment + hasEffectsWhenAssignedAtPath(_path: ObjectPath, context: HasEffectsContext): boolean { for (const element of this.elements) { - if (element !== null && element.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context)) - return true; + if (element?.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context)) return true; } return false; } markDeclarationReached(): void { for (const element of this.elements) { - if (element !== null) { - element.markDeclarationReached(); - } + element?.markDeclarationReached(); } } } diff --git a/src/ast/nodes/ArrowFunctionExpression.ts b/src/ast/nodes/ArrowFunctionExpression.ts index 9f90349b429..095be186886 100644 --- a/src/ast/nodes/ArrowFunctionExpression.ts +++ b/src/ast/nodes/ArrowFunctionExpression.ts @@ -1,13 +1,12 @@ import { type CallOptions } from '../CallOptions'; -import { type HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { type HasEffectsContext } from '../ExecutionContext'; import ReturnValueScope from '../scopes/ReturnValueScope'; import type Scope from '../scopes/Scope'; import { type ObjectPath } from '../utils/PathTracker'; import BlockStatement from './BlockStatement'; -import Identifier from './Identifier'; import * as NodeType from './NodeType'; import FunctionBase from './shared/FunctionBase'; -import { type ExpressionNode, IncludeChildren } from './shared/Node'; +import { type ExpressionNode } from './shared/Node'; import { ObjectEntity } from './shared/ObjectEntity'; import { OBJECT_PROTOTYPE } from './shared/ObjectPrototype'; import type { PatternNode } from './shared/Pattern'; @@ -48,15 +47,6 @@ export default class ArrowFunctionExpression extends FunctionBase { return false; } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { - super.include(context, includeChildrenRecursively); - for (const param of this.params) { - if (!(param instanceof Identifier)) { - param.include(context, includeChildrenRecursively); - } - } - } - protected getObjectEntity(): ObjectEntity { if (this.objectEntity !== null) { return this.objectEntity; diff --git a/src/ast/nodes/AssignmentPattern.ts b/src/ast/nodes/AssignmentPattern.ts index 21459c772a6..8575ce56d00 100644 --- a/src/ast/nodes/AssignmentPattern.ts +++ b/src/ast/nodes/AssignmentPattern.ts @@ -2,12 +2,13 @@ import type MagicString from 'magic-string'; import { BLANK } from '../../utils/blank'; import type { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import type { HasEffectsContext } from '../ExecutionContext'; +import { InclusionContext } from '../ExecutionContext'; import { EMPTY_PATH, type ObjectPath, UNKNOWN_PATH } from '../utils/PathTracker'; import type LocalVariable from '../variables/LocalVariable'; import type Variable from '../variables/Variable'; import type * as NodeType from './NodeType'; import type { ExpressionEntity } from './shared/Expression'; -import { type ExpressionNode, NodeBase } from './shared/Node'; +import { type ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; import type { PatternNode } from './shared/Pattern'; export default class AssignmentPattern extends NodeBase implements PatternNode { @@ -35,6 +36,15 @@ export default class AssignmentPattern extends NodeBase implements PatternNode { return path.length > 0 || this.left.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context); } + // Note that FunctionBase may directly include .left and .right without + // including the pattern itself. This is how default parameter tree-shaking + // works at the moment. + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { + this.included = true; + this.left.include(context, includeChildrenRecursively); + this.right.include(context, includeChildrenRecursively); + } + markDeclarationReached(): void { this.left.markDeclarationReached(); } @@ -45,7 +55,11 @@ export default class AssignmentPattern extends NodeBase implements PatternNode { { isShorthandProperty }: NodeRenderOptions = BLANK ): void { this.left.render(code, options, { isShorthandProperty }); - this.right.render(code, options); + if (this.right.included) { + this.right.render(code, options); + } else { + code.remove(this.left.end, this.end); + } } protected applyDeoptimizations(): void { diff --git a/src/ast/nodes/AwaitExpression.ts b/src/ast/nodes/AwaitExpression.ts index d0f26fe8b23..2f27a1b47d4 100644 --- a/src/ast/nodes/AwaitExpression.ts +++ b/src/ast/nodes/AwaitExpression.ts @@ -1,5 +1,4 @@ import type { InclusionContext } from '../ExecutionContext'; -import { UNKNOWN_PATH } from '../utils/PathTracker'; import ArrowFunctionExpression from './ArrowFunctionExpression'; import type * as NodeType from './NodeType'; import FunctionNode from './shared/FunctionNode'; @@ -30,10 +29,4 @@ export default class AwaitExpression extends NodeBase { } this.argument.include(context, includeChildrenRecursively); } - - protected applyDeoptimizations(): void { - this.deoptimized = true; - this.argument.deoptimizePath(UNKNOWN_PATH); - this.context.requestTreeshakingPass(); - } } diff --git a/src/ast/nodes/BinaryExpression.ts b/src/ast/nodes/BinaryExpression.ts index 8b1bf5be0cf..4f38da0106b 100644 --- a/src/ast/nodes/BinaryExpression.ts +++ b/src/ast/nodes/BinaryExpression.ts @@ -40,9 +40,10 @@ const binaryOperators: { '>>': (left: any, right: any) => left >> right, '>>>': (left: any, right: any) => left >>> right, '^': (left: any, right: any) => left ^ right, - in: () => UnknownValue, - instanceof: () => UnknownValue, '|': (left: any, right: any) => left | right + // We use the fallback for cases where we return something unknown + // in: () => UnknownValue, + // instanceof: () => UnknownValue, }; export default class BinaryExpression extends NodeBase implements DeoptimizableEntity { @@ -60,10 +61,10 @@ export default class BinaryExpression extends NodeBase implements DeoptimizableE ): LiteralValueOrUnknown { if (path.length > 0) return UnknownValue; const leftValue = this.left.getLiteralValueAtPath(EMPTY_PATH, recursionTracker, origin); - if (leftValue === UnknownValue) return UnknownValue; + if (typeof leftValue === 'symbol') return UnknownValue; const rightValue = this.right.getLiteralValueAtPath(EMPTY_PATH, recursionTracker, origin); - if (rightValue === UnknownValue) return UnknownValue; + if (typeof rightValue === 'symbol') return UnknownValue; const operatorFn = binaryOperators[this.operator]; if (!operatorFn) return UnknownValue; diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index 593146bf9f9..be1a578ca47 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -6,13 +6,11 @@ import { type NodeRenderOptions, type RenderOptions } from '../../utils/renderHelpers'; -import type { CallOptions } from '../CallOptions'; import type { DeoptimizableEntity } from '../DeoptimizableEntity'; import type { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { EVENT_CALLED, type NodeEvent } from '../NodeEvents'; +import { EVENT_CALLED } from '../NodeEvents'; import { EMPTY_PATH, - type ObjectPath, type PathTracker, SHARED_RECURSION_TRACKER, UNKNOWN_PATH @@ -22,29 +20,15 @@ import MemberExpression from './MemberExpression'; import type * as NodeType from './NodeType'; import type SpreadElement from './SpreadElement'; import type Super from './Super'; -import { - type ExpressionEntity, - type LiteralValueOrUnknown, - UNKNOWN_EXPRESSION, - UnknownValue -} from './shared/Expression'; -import { - type ExpressionNode, - INCLUDE_PARAMETERS, - type IncludeChildren, - NodeBase -} from './shared/Node'; +import CallExpressionBase from './shared/CallExpressionBase'; +import { type ExpressionEntity, UNKNOWN_EXPRESSION } from './shared/Expression'; +import { type ExpressionNode, INCLUDE_PARAMETERS, type IncludeChildren } from './shared/Node'; -export default class CallExpression extends NodeBase implements DeoptimizableEntity { +export default class CallExpression extends CallExpressionBase implements DeoptimizableEntity { declare arguments: (ExpressionNode | SpreadElement)[]; declare callee: ExpressionNode | Super; declare optional: boolean; declare type: NodeType.tCallExpression; - protected deoptimized = false; - private declare callOptions: CallOptions; - private readonly deoptimizableDependentExpressions: DeoptimizableEntity[] = []; - private readonly expressionsToBeDeoptimized = new Set(); - private returnExpression: ExpressionEntity | null = null; bind(): void { super.bind(); @@ -82,104 +66,6 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt }; } - deoptimizeCache(): void { - if (this.returnExpression !== UNKNOWN_EXPRESSION) { - 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): void { - if ( - path.length === 0 || - this.context.deoptimizationTracker.trackEntityAtPathAndGetIfTracked(path, this) - ) { - return; - } - const returnExpression = this.getReturnExpression(); - if (returnExpression !== UNKNOWN_EXPRESSION) { - returnExpression.deoptimizePath(path); - } - } - - deoptimizeThisOnEventAtPath( - event: NodeEvent, - path: ObjectPath, - thisParameter: ExpressionEntity, - recursionTracker: PathTracker - ): void { - 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, - origin: DeoptimizableEntity - ): LiteralValueOrUnknown { - const returnExpression = this.getReturnExpression(recursionTracker); - if (returnExpression === UNKNOWN_EXPRESSION) { - return UnknownValue; - } - 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; - } - return recursionTracker.withTrackedEntityAtPath( - path, - returnExpression, - () => { - this.deoptimizableDependentExpressions.push(origin); - return returnExpression.getReturnExpressionWhenCalledAtPath( - path, - callOptions, - recursionTracker, - origin - ); - }, - UNKNOWN_EXPRESSION - ); - } - hasEffects(context: HasEffectsContext): boolean { try { for (const argument of this.arguments) { @@ -199,33 +85,6 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return ( - !context.accessed.trackEntityAtPathAndGetIfTracked(path, this) && - this.getReturnExpression().hasEffectsWhenAccessedAtPath(path, context) - ); - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return ( - !context.assigned.trackEntityAtPathAndGetIfTracked(path, this) && - this.getReturnExpression().hasEffectsWhenAssignedAtPath(path, context) - ); - } - - hasEffectsWhenCalledAtPath( - path: ObjectPath, - callOptions: CallOptions, - context: HasEffectsContext - ): boolean { - return ( - !( - callOptions.withNew ? context.instantiated : context.called - ).trackEntityAtPathAndGetIfTracked(path, callOptions, this) && - this.getReturnExpression().hasEffectsWhenCalledAtPath(path, callOptions, context) - ); - } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { if (!this.deoptimized) this.applyDeoptimizations(); if (includeChildrenRecursively) { @@ -239,7 +98,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } } else { this.included = true; - this.callee.include(context, false); + this.callee.include(context, false, { includeWithoutParameterDefaults: true }); } this.callee.includeCallArguments(context, this.arguments); const returnExpression = this.getReturnExpression(); @@ -307,7 +166,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt this.context.requestTreeshakingPass(); } - private getReturnExpression( + protected getReturnExpression( recursionTracker: PathTracker = SHARED_RECURSION_TRACKER ): ExpressionEntity { if (this.returnExpression === null) { diff --git a/src/ast/nodes/ConditionalExpression.ts b/src/ast/nodes/ConditionalExpression.ts index afdc04d8d12..0ff6cc1fcbb 100644 --- a/src/ast/nodes/ConditionalExpression.ts +++ b/src/ast/nodes/ConditionalExpression.ts @@ -48,7 +48,7 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz deoptimizePath(path: ObjectPath): void { const usedBranch = this.getUsedBranch(); - if (usedBranch === null) { + if (!usedBranch) { this.consequent.deoptimizePath(path); this.alternate.deoptimizePath(path); } else { @@ -72,7 +72,7 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz origin: DeoptimizableEntity ): LiteralValueOrUnknown { const usedBranch = this.getUsedBranch(); - if (usedBranch === null) return UnknownValue; + if (!usedBranch) return UnknownValue; this.expressionsToBeDeoptimized.push(origin); return usedBranch.getLiteralValueAtPath(path, recursionTracker, origin); } @@ -84,7 +84,7 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz origin: DeoptimizableEntity ): ExpressionEntity { const usedBranch = this.getUsedBranch(); - if (usedBranch === null) + if (!usedBranch) return new MultiExpression([ this.consequent.getReturnExpressionWhenCalledAtPath( path, @@ -111,7 +111,7 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz hasEffects(context: HasEffectsContext): boolean { if (this.test.hasEffects(context)) return true; const usedBranch = this.getUsedBranch(); - if (usedBranch === null) { + if (!usedBranch) { return this.consequent.hasEffects(context) || this.alternate.hasEffects(context); } return usedBranch.hasEffects(context); @@ -119,7 +119,7 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { const usedBranch = this.getUsedBranch(); - if (usedBranch === null) { + if (!usedBranch) { return ( this.consequent.hasEffectsWhenAccessedAtPath(path, context) || this.alternate.hasEffectsWhenAccessedAtPath(path, context) @@ -130,7 +130,7 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { const usedBranch = this.getUsedBranch(); - if (usedBranch === null) { + if (!usedBranch) { return ( this.consequent.hasEffectsWhenAssignedAtPath(path, context) || this.alternate.hasEffectsWhenAssignedAtPath(path, context) @@ -145,7 +145,7 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz context: HasEffectsContext ): boolean { const usedBranch = this.getUsedBranch(); - if (usedBranch === null) { + if (!usedBranch) { return ( this.consequent.hasEffectsWhenCalledAtPath(path, callOptions, context) || this.alternate.hasEffectsWhenCalledAtPath(path, callOptions, context) @@ -168,10 +168,10 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz includeCallArguments( context: InclusionContext, - args: readonly (ExpressionNode | SpreadElement)[] + args: readonly (ExpressionEntity | SpreadElement)[] ): void { const usedBranch = this.getUsedBranch(); - if (usedBranch === null) { + if (!usedBranch) { this.consequent.includeCallArguments(context, args); this.alternate.includeCallArguments(context, args); } else { @@ -225,7 +225,7 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz } this.isBranchResolutionAnalysed = true; const testValue = this.test.getLiteralValueAtPath(EMPTY_PATH, SHARED_RECURSION_TRACKER, this); - return testValue === UnknownValue + return typeof testValue === 'symbol' ? null : (this.usedBranch = testValue ? this.consequent : this.alternate); } diff --git a/src/ast/nodes/DoWhileStatement.ts b/src/ast/nodes/DoWhileStatement.ts index 6218fa03da2..4779555f8ea 100644 --- a/src/ast/nodes/DoWhileStatement.ts +++ b/src/ast/nodes/DoWhileStatement.ts @@ -31,7 +31,7 @@ export default class DoWhileStatement extends StatementBase { this.included = true; this.test.include(context, includeChildrenRecursively); const { brokenFlow } = context; - this.body.includeAsSingleStatement(context, includeChildrenRecursively); + this.body.include(context, includeChildrenRecursively, { asSingleStatement: true }); context.brokenFlow = brokenFlow; } } diff --git a/src/ast/nodes/ExportNamedDeclaration.ts b/src/ast/nodes/ExportNamedDeclaration.ts index 0d09a25e8e2..e170fefd27a 100644 --- a/src/ast/nodes/ExportNamedDeclaration.ts +++ b/src/ast/nodes/ExportNamedDeclaration.ts @@ -18,11 +18,11 @@ export default class ExportNamedDeclaration extends NodeBase { bind(): void { // Do not bind specifiers - if (this.declaration !== null) this.declaration.bind(); + this.declaration?.bind(); } hasEffects(context: HasEffectsContext): boolean { - return this.declaration !== null && this.declaration.hasEffects(context); + return !!this.declaration?.hasEffects(context); } initialise(): void { diff --git a/src/ast/nodes/ForInStatement.ts b/src/ast/nodes/ForInStatement.ts index 038404ae8d6..9669720ba8d 100644 --- a/src/ast/nodes/ForInStatement.ts +++ b/src/ast/nodes/ForInStatement.ts @@ -53,7 +53,7 @@ export default class ForInStatement extends StatementBase { this.left.include(context, includeChildrenRecursively || true); this.right.include(context, includeChildrenRecursively); const { brokenFlow } = context; - this.body.includeAsSingleStatement(context, includeChildrenRecursively); + this.body.include(context, includeChildrenRecursively, { asSingleStatement: true }); context.brokenFlow = brokenFlow; } diff --git a/src/ast/nodes/ForOfStatement.ts b/src/ast/nodes/ForOfStatement.ts index c7ba84d6dbd..4ece368c04d 100644 --- a/src/ast/nodes/ForOfStatement.ts +++ b/src/ast/nodes/ForOfStatement.ts @@ -38,7 +38,7 @@ export default class ForOfStatement extends StatementBase { this.left.include(context, includeChildrenRecursively || true); this.right.include(context, includeChildrenRecursively); const { brokenFlow } = context; - this.body.includeAsSingleStatement(context, includeChildrenRecursively); + this.body.include(context, includeChildrenRecursively, { asSingleStatement: true }); context.brokenFlow = brokenFlow; } diff --git a/src/ast/nodes/ForStatement.ts b/src/ast/nodes/ForStatement.ts index 25916ad9cd3..eb228cd6942 100644 --- a/src/ast/nodes/ForStatement.ts +++ b/src/ast/nodes/ForStatement.ts @@ -25,9 +25,9 @@ export default class ForStatement extends StatementBase { hasEffects(context: HasEffectsContext): boolean { if ( - (this.init && this.init.hasEffects(context)) || - (this.test && this.test.hasEffects(context)) || - (this.update && this.update.hasEffects(context)) + this.init?.hasEffects(context) || + this.test?.hasEffects(context) || + this.update?.hasEffects(context) ) return true; const { @@ -45,18 +45,18 @@ export default class ForStatement extends StatementBase { include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { this.included = true; - if (this.init) this.init.includeAsSingleStatement(context, includeChildrenRecursively); - if (this.test) this.test.include(context, includeChildrenRecursively); + this.init?.include(context, includeChildrenRecursively, { asSingleStatement: true }); + this.test?.include(context, includeChildrenRecursively); const { brokenFlow } = context; - if (this.update) this.update.include(context, includeChildrenRecursively); - this.body.includeAsSingleStatement(context, includeChildrenRecursively); + this.update?.include(context, includeChildrenRecursively); + this.body.include(context, includeChildrenRecursively, { asSingleStatement: true }); context.brokenFlow = brokenFlow; } render(code: MagicString, options: RenderOptions): void { - if (this.init) this.init.render(code, options, NO_SEMICOLON); - if (this.test) this.test.render(code, options, NO_SEMICOLON); - if (this.update) this.update.render(code, options, NO_SEMICOLON); + this.init?.render(code, options, NO_SEMICOLON); + this.test?.render(code, options, NO_SEMICOLON); + this.update?.render(code, options, NO_SEMICOLON); this.body.render(code, options); } } diff --git a/src/ast/nodes/FunctionDeclaration.ts b/src/ast/nodes/FunctionDeclaration.ts index ddf88636455..adcad843f41 100644 --- a/src/ast/nodes/FunctionDeclaration.ts +++ b/src/ast/nodes/FunctionDeclaration.ts @@ -1,12 +1,17 @@ +import { InclusionContext } from '../ExecutionContext'; import type ChildScope from '../scopes/ChildScope'; import Identifier, { type IdentifierWithVariable } from './Identifier'; import type * as NodeType from './NodeType'; import FunctionNode from './shared/FunctionNode'; -import type { GenericEsTreeNode } from './shared/Node'; +import type { GenericEsTreeNode, IncludeChildren } from './shared/Node'; export default class FunctionDeclaration extends FunctionNode { declare type: NodeType.tFunctionDeclaration; + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + super.include(context, includeChildrenRecursively, { includeWithoutParameterDefaults: true }); + } + initialise(): void { super.initialise(); if (this.id !== null) { diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index 89965f79287..1043e5dee46 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -19,7 +19,7 @@ import { type LiteralValueOrUnknown, UNKNOWN_EXPRESSION } from './shared/Expression'; -import { type ExpressionNode, NodeBase } from './shared/Node'; +import { NodeBase } from './shared/Node'; import type { PatternNode } from './shared/Pattern'; export type IdentifierWithVariable = Identifier & { variable: Variable }; @@ -43,13 +43,13 @@ export default class Identifier extends NodeBase implements PatternNode { variables: Variable[], exportNamesByVariable: ReadonlyMap ): void { - if (this.variable !== null && exportNamesByVariable.has(this.variable)) { - variables.push(this.variable); + if (exportNamesByVariable.has(this.variable!)) { + variables.push(this.variable!); } } bind(): void { - if (this.variable === null && isReference(this, this.parent as NodeWithFieldDefinition)) { + if (!this.variable && isReference(this, this.parent as NodeWithFieldDefinition)) { this.variable = this.scope.findVariable(this.name); this.variable.addReference(this); } @@ -108,7 +108,7 @@ export default class Identifier extends NodeBase implements PatternNode { recursionTracker: PathTracker, origin: DeoptimizableEntity ): LiteralValueOrUnknown { - return this.getVariableRespectingTDZ().getLiteralValueAtPath(path, recursionTracker, origin); + return this.getVariableRespectingTDZ()!.getLiteralValueAtPath(path, recursionTracker, origin); } getReturnExpressionWhenCalledAtPath( @@ -117,7 +117,7 @@ export default class Identifier extends NodeBase implements PatternNode { recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { - return this.getVariableRespectingTDZ().getReturnExpressionWhenCalledAtPath( + return this.getVariableRespectingTDZ()!.getReturnExpressionWhenCalledAtPath( path, callOptions, recursionTracker, @@ -140,18 +140,14 @@ export default class Identifier extends NodeBase implements PatternNode { hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { return ( this.variable !== null && - this.getVariableRespectingTDZ().hasEffectsWhenAccessedAtPath(path, context) + this.getVariableRespectingTDZ()!.hasEffectsWhenAccessedAtPath(path, context) ); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { return ( - !this.variable || - (path.length > 0 - ? this.getVariableRespectingTDZ() - : this.variable - ).hasEffectsWhenAssignedAtPath(path, context) - ); + path.length > 0 ? this.getVariableRespectingTDZ() : this.variable + )!.hasEffectsWhenAssignedAtPath(path, context); } hasEffectsWhenCalledAtPath( @@ -159,10 +155,7 @@ export default class Identifier extends NodeBase implements PatternNode { callOptions: CallOptions, context: HasEffectsContext ): boolean { - return ( - !this.variable || - this.getVariableRespectingTDZ().hasEffectsWhenCalledAtPath(path, callOptions, context) - ); + return this.getVariableRespectingTDZ()!.hasEffectsWhenCalledAtPath(path, callOptions, context); } include(): void { @@ -177,9 +170,9 @@ export default class Identifier extends NodeBase implements PatternNode { includeCallArguments( context: InclusionContext, - args: readonly (ExpressionNode | SpreadElement)[] + args: readonly (ExpressionEntity | SpreadElement)[] ): void { - this.getVariableRespectingTDZ().includeCallArguments(context, args); + this.variable!.includeCallArguments(context, args); } isPossibleTDZ(): boolean { @@ -250,7 +243,7 @@ export default class Identifier extends NodeBase implements PatternNode { protected applyDeoptimizations(): void { this.deoptimized = true; - if (this.variable !== null && this.variable instanceof LocalVariable) { + if (this.variable instanceof LocalVariable) { this.variable.consolidateInitializers(); this.context.requestTreeshakingPass(); } @@ -266,11 +259,11 @@ export default class Identifier extends NodeBase implements PatternNode { ); } - private getVariableRespectingTDZ(): ExpressionEntity { + private getVariableRespectingTDZ(): ExpressionEntity | null { if (this.isPossibleTDZ()) { return UNKNOWN_EXPRESSION; } - return this.variable!; + return this.variable; } } diff --git a/src/ast/nodes/IfStatement.ts b/src/ast/nodes/IfStatement.ts index b7ee1385be3..f32fcf102ab 100644 --- a/src/ast/nodes/IfStatement.ts +++ b/src/ast/nodes/IfStatement.ts @@ -41,7 +41,7 @@ export default class IfStatement extends StatementBase implements DeoptimizableE return true; } const testValue = this.getTestValue(); - if (testValue === UnknownValue) { + if (typeof testValue === 'symbol') { const { brokenFlow } = context; if (this.consequent.hasEffects(context)) return true; const consequentBrokenFlow = context.brokenFlow; @@ -52,9 +52,7 @@ export default class IfStatement extends StatementBase implements DeoptimizableE context.brokenFlow < consequentBrokenFlow ? context.brokenFlow : consequentBrokenFlow; return false; } - return testValue - ? this.consequent.hasEffects(context) - : this.alternate !== null && this.alternate.hasEffects(context); + return testValue ? this.consequent.hasEffects(context) : !!this.alternate?.hasEffects(context); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { @@ -63,7 +61,7 @@ export default class IfStatement extends StatementBase implements DeoptimizableE this.includeRecursively(includeChildrenRecursively, context); } else { const testValue = this.getTestValue(); - if (testValue === UnknownValue) { + if (typeof testValue === 'symbol') { this.includeUnknownTest(context); } else { this.includeKnownTest(context, testValue); @@ -103,14 +101,14 @@ export default class IfStatement extends StatementBase implements DeoptimizableE } else { code.remove(this.start, this.consequent.start); } - if (this.consequent.included && (noTreeshake || testValue === UnknownValue || testValue)) { + if (this.consequent.included && (noTreeshake || typeof testValue === 'symbol' || testValue)) { this.consequent.render(code, options); } else { code.overwrite(this.consequent.start, this.consequent.end, includesIfElse ? ';' : ''); hoistedDeclarations.push(...this.consequentScope.hoistedDeclarations); } if (this.alternate) { - if (this.alternate.included && (noTreeshake || testValue === UnknownValue || !testValue)) { + if (this.alternate.included && (noTreeshake || typeof testValue === 'symbol' || !testValue)) { if (includesIfElse) { if (code.original.charCodeAt(this.alternate.start - 1) === 101) { code.prependLeft(this.alternate.start, ' '); @@ -147,10 +145,10 @@ export default class IfStatement extends StatementBase implements DeoptimizableE this.test.include(context, false); } if (testValue && this.consequent.shouldBeIncluded(context)) { - this.consequent.includeAsSingleStatement(context, false); + this.consequent.include(context, false, { asSingleStatement: true }); } - if (this.alternate !== null && !testValue && this.alternate.shouldBeIncluded(context)) { - this.alternate.includeAsSingleStatement(context, false); + if (!testValue && this.alternate?.shouldBeIncluded(context)) { + this.alternate.include(context, false, { asSingleStatement: true }); } } @@ -160,9 +158,7 @@ export default class IfStatement extends StatementBase implements DeoptimizableE ) { this.test.include(context, includeChildrenRecursively); this.consequent.include(context, includeChildrenRecursively); - if (this.alternate !== null) { - this.alternate.include(context, includeChildrenRecursively); - } + this.alternate?.include(context, includeChildrenRecursively); } private includeUnknownTest(context: InclusionContext) { @@ -170,12 +166,12 @@ export default class IfStatement extends StatementBase implements DeoptimizableE const { brokenFlow } = context; let consequentBrokenFlow = BROKEN_FLOW_NONE; if (this.consequent.shouldBeIncluded(context)) { - this.consequent.includeAsSingleStatement(context, false); + this.consequent.include(context, false, { asSingleStatement: true }); consequentBrokenFlow = context.brokenFlow; context.brokenFlow = brokenFlow; } - if (this.alternate !== null && this.alternate.shouldBeIncluded(context)) { - this.alternate.includeAsSingleStatement(context, false); + if (this.alternate?.shouldBeIncluded(context)) { + this.alternate.include(context, false, { asSingleStatement: true }); context.brokenFlow = context.brokenFlow < consequentBrokenFlow ? context.brokenFlow : consequentBrokenFlow; } diff --git a/src/ast/nodes/LogicalExpression.ts b/src/ast/nodes/LogicalExpression.ts index 571d28a75e2..24cc3594039 100644 --- a/src/ast/nodes/LogicalExpression.ts +++ b/src/ast/nodes/LogicalExpression.ts @@ -54,7 +54,7 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable deoptimizePath(path: ObjectPath): void { const usedBranch = this.getUsedBranch(); - if (usedBranch === null) { + if (!usedBranch) { this.left.deoptimizePath(path); this.right.deoptimizePath(path); } else { @@ -78,7 +78,7 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable origin: DeoptimizableEntity ): LiteralValueOrUnknown { const usedBranch = this.getUsedBranch(); - if (usedBranch === null) return UnknownValue; + if (!usedBranch) return UnknownValue; this.expressionsToBeDeoptimized.push(origin); return usedBranch.getLiteralValueAtPath(path, recursionTracker, origin); } @@ -90,7 +90,7 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable origin: DeoptimizableEntity ): ExpressionEntity { const usedBranch = this.getUsedBranch(); - if (usedBranch === null) + if (!usedBranch) return new MultiExpression([ this.left.getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin), this.right.getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin) @@ -116,7 +116,7 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { const usedBranch = this.getUsedBranch(); - if (usedBranch === null) { + if (!usedBranch) { return ( this.left.hasEffectsWhenAccessedAtPath(path, context) || this.right.hasEffectsWhenAccessedAtPath(path, context) @@ -127,7 +127,7 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { const usedBranch = this.getUsedBranch(); - if (usedBranch === null) { + if (!usedBranch) { return ( this.left.hasEffectsWhenAssignedAtPath(path, context) || this.right.hasEffectsWhenAssignedAtPath(path, context) @@ -142,7 +142,7 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable context: HasEffectsContext ): boolean { const usedBranch = this.getUsedBranch(); - if (usedBranch === null) { + if (!usedBranch) { return ( this.left.hasEffectsWhenCalledAtPath(path, callOptions, context) || this.right.hasEffectsWhenCalledAtPath(path, callOptions, context) @@ -157,7 +157,7 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable if ( includeChildrenRecursively || (usedBranch === this.right && this.left.shouldBeIncluded(context)) || - usedBranch === null + !usedBranch ) { this.left.include(context, includeChildrenRecursively); this.right.include(context, includeChildrenRecursively); @@ -211,7 +211,7 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable if (!this.isBranchResolutionAnalysed) { this.isBranchResolutionAnalysed = true; const leftValue = this.left.getLiteralValueAtPath(EMPTY_PATH, SHARED_RECURSION_TRACKER, this); - if (leftValue === UnknownValue) { + if (typeof leftValue === 'symbol') { return null; } else { this.usedBranch = diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index 2d91fa649d5..20b957e5658 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -291,7 +291,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE includeCallArguments( context: InclusionContext, - args: readonly (ExpressionNode | SpreadElement)[] + args: readonly (ExpressionEntity | SpreadElement)[] ): void { if (this.variable) { this.variable.includeCallArguments(context, args); @@ -385,7 +385,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE if (this.propertyKey === null) { this.propertyKey = UnknownKey; const value = this.property.getLiteralValueAtPath(EMPTY_PATH, SHARED_RECURSION_TRACKER, this); - return (this.propertyKey = value === UnknownValue ? UnknownKey : String(value)); + return (this.propertyKey = typeof value === 'symbol' ? UnknownKey : String(value)); } return this.propertyKey; } diff --git a/src/ast/nodes/NewExpression.ts b/src/ast/nodes/NewExpression.ts index abef82ffae0..871c82fb5f3 100644 --- a/src/ast/nodes/NewExpression.ts +++ b/src/ast/nodes/NewExpression.ts @@ -1,9 +1,10 @@ import type { NormalizedTreeshakingOptions } from '../../rollup/types'; import type { CallOptions } from '../CallOptions'; import type { HasEffectsContext } from '../ExecutionContext'; +import { InclusionContext } from '../ExecutionContext'; import { EMPTY_PATH, type ObjectPath, UNKNOWN_PATH } from '../utils/PathTracker'; import type * as NodeType from './NodeType'; -import { type ExpressionNode, NodeBase } from './shared/Node'; +import { type ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; export default class NewExpression extends NodeBase { declare arguments: ExpressionNode[]; @@ -13,25 +14,39 @@ export default class NewExpression extends NodeBase { private declare callOptions: CallOptions; hasEffects(context: HasEffectsContext): boolean { - if (!this.deoptimized) this.applyDeoptimizations(); - for (const argument of this.arguments) { - if (argument.hasEffects(context)) return true; + try { + for (const argument of this.arguments) { + if (argument.hasEffects(context)) return true; + } + if ( + (this.context.options.treeshake as NormalizedTreeshakingOptions).annotations && + this.annotations + ) + return false; + return ( + this.callee.hasEffects(context) || + this.callee.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.callOptions, context) + ); + } finally { + if (!this.deoptimized) this.applyDeoptimizations(); } - if ( - (this.context.options.treeshake as NormalizedTreeshakingOptions).annotations && - this.annotations - ) - return false; - return ( - this.callee.hasEffects(context) || - this.callee.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.callOptions, context) - ); } hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean { return path.length > 0; } + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { + if (!this.deoptimized) this.applyDeoptimizations(); + if (includeChildrenRecursively) { + super.include(context, includeChildrenRecursively); + } else { + this.included = true; + this.callee.include(context, false); + } + this.callee.includeCallArguments(context, this.arguments); + } + initialise(): void { this.callOptions = { args: this.arguments, diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index 61ff336a6c1..a0746190271 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -17,11 +17,7 @@ import Literal from './Literal'; import * as NodeType from './NodeType'; import type Property from './Property'; import SpreadElement from './SpreadElement'; -import { - type ExpressionEntity, - type LiteralValueOrUnknown, - UnknownValue -} from './shared/Expression'; +import { type ExpressionEntity, type LiteralValueOrUnknown } from './shared/Expression'; import { NodeBase } from './shared/Node'; import { ObjectEntity, type ObjectProperty } from './shared/ObjectEntity'; import { OBJECT_PROTOTYPE } from './shared/ObjectPrototype'; @@ -124,7 +120,7 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE SHARED_RECURSION_TRACKER, this ); - if (keyValue === UnknownValue) { + if (typeof keyValue === 'symbol') { properties.push({ key: UnknownKey, kind: property.kind, property }); continue; } else { diff --git a/src/ast/nodes/PropertyDefinition.ts b/src/ast/nodes/PropertyDefinition.ts index 36183b9872a..caafa2f6a0d 100644 --- a/src/ast/nodes/PropertyDefinition.ts +++ b/src/ast/nodes/PropertyDefinition.ts @@ -55,10 +55,7 @@ export default class PropertyDefinition extends NodeBase { } hasEffects(context: HasEffectsContext): boolean { - return ( - this.key.hasEffects(context) || - (this.static && this.value !== null && this.value.hasEffects(context)) - ); + return this.key.hasEffects(context) || (this.static && !!this.value?.hasEffects(context)); } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { diff --git a/src/ast/nodes/ReturnStatement.ts b/src/ast/nodes/ReturnStatement.ts index acfdd4d066b..eaa99bab74e 100644 --- a/src/ast/nodes/ReturnStatement.ts +++ b/src/ast/nodes/ReturnStatement.ts @@ -14,20 +14,14 @@ export default class ReturnStatement extends StatementBase { declare type: NodeType.tReturnStatement; hasEffects(context: HasEffectsContext): boolean { - if ( - !context.ignore.returnYield || - (this.argument !== null && this.argument.hasEffects(context)) - ) - return true; + if (!context.ignore.returnYield || this.argument?.hasEffects(context)) return true; context.brokenFlow = BROKEN_FLOW_ERROR_RETURN_LABEL; return false; } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { this.included = true; - if (this.argument) { - this.argument.include(context, includeChildrenRecursively); - } + this.argument?.include(context, includeChildrenRecursively); context.brokenFlow = BROKEN_FLOW_ERROR_RETURN_LABEL; } diff --git a/src/ast/nodes/Super.ts b/src/ast/nodes/Super.ts index f598cbff0b1..d04da03870f 100644 --- a/src/ast/nodes/Super.ts +++ b/src/ast/nodes/Super.ts @@ -1,20 +1,32 @@ +import { NodeEvent } from '../NodeEvents'; import type { ObjectPath } from '../utils/PathTracker'; -import type ThisVariable from '../variables/ThisVariable'; +import { PathTracker } from '../utils/PathTracker'; +import Variable from '../variables/Variable'; import type * as NodeType from './NodeType'; +import { ExpressionEntity } from './shared/Expression'; import { NodeBase } from './shared/Node'; export default class Super extends NodeBase { declare type: NodeType.tSuper; - declare variable: ThisVariable; + declare variable: Variable; bind(): void { - this.variable = this.scope.findVariable('this') as ThisVariable; + this.variable = this.scope.findVariable('this'); } deoptimizePath(path: ObjectPath): void { this.variable.deoptimizePath(path); } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.variable.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } + include(): void { if (!this.included) { this.included = true; diff --git a/src/ast/nodes/SwitchCase.ts b/src/ast/nodes/SwitchCase.ts index fad874c41d3..7fd0cfe1501 100644 --- a/src/ast/nodes/SwitchCase.ts +++ b/src/ast/nodes/SwitchCase.ts @@ -21,7 +21,7 @@ export default class SwitchCase extends NodeBase { declare type: NodeType.tSwitchCase; hasEffects(context: HasEffectsContext): boolean { - if (this.test && this.test.hasEffects(context)) return true; + if (this.test?.hasEffects(context)) return true; for (const node of this.consequent) { if (context.brokenFlow) break; if (node.hasEffects(context)) return true; @@ -31,7 +31,7 @@ export default class SwitchCase extends NodeBase { include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { this.included = true; - if (this.test) this.test.include(context, includeChildrenRecursively); + this.test?.include(context, includeChildrenRecursively); for (const node of this.consequent) { if (includeChildrenRecursively || node.shouldBeIncluded(context)) node.include(context, includeChildrenRecursively); diff --git a/src/ast/nodes/TaggedTemplateExpression.ts b/src/ast/nodes/TaggedTemplateExpression.ts index 3bb300252e8..3a661c29848 100644 --- a/src/ast/nodes/TaggedTemplateExpression.ts +++ b/src/ast/nodes/TaggedTemplateExpression.ts @@ -1,20 +1,27 @@ import type MagicString from 'magic-string'; import { type RenderOptions } from '../../utils/renderHelpers'; -import { type CallOptions, NO_ARGS } from '../CallOptions'; import type { HasEffectsContext } from '../ExecutionContext'; -import { EMPTY_PATH } from '../utils/PathTracker'; -import type Identifier from './Identifier'; +import { InclusionContext } from '../ExecutionContext'; +import { EVENT_CALLED } from '../NodeEvents'; +import { + EMPTY_PATH, + PathTracker, + SHARED_RECURSION_TRACKER, + UNKNOWN_PATH +} from '../utils/PathTracker'; +import Identifier from './Identifier'; +import MemberExpression from './MemberExpression'; import * as NodeType from './NodeType'; import type TemplateLiteral from './TemplateLiteral'; -import { type ExpressionNode, NodeBase } from './shared/Node'; +import CallExpressionBase from './shared/CallExpressionBase'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './shared/Expression'; +import { type ExpressionNode, IncludeChildren } from './shared/Node'; -export default class TaggedTemplateExpression extends NodeBase { +export default class TaggedTemplateExpression extends CallExpressionBase { declare quasi: TemplateLiteral; declare tag: ExpressionNode; declare type: NodeType.tTaggedTemplateExpression; - private declare callOptions: CallOptions; - bind(): void { super.bind(); if (this.tag.type === NodeType.Identifier) { @@ -34,16 +41,40 @@ export default class TaggedTemplateExpression extends NodeBase { } hasEffects(context: HasEffectsContext): boolean { - return ( - super.hasEffects(context) || - this.tag.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.callOptions, context) - ); + try { + for (const argument of this.quasi.expressions) { + if (argument.hasEffects(context)) return true; + } + return ( + this.tag.hasEffects(context) || + this.tag.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.callOptions, context) + ); + } finally { + if (!this.deoptimized) this.applyDeoptimizations(); + } + } + + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { + if (!this.deoptimized) this.applyDeoptimizations(); + if (includeChildrenRecursively) { + super.include(context, includeChildrenRecursively); + } else { + this.included = true; + this.tag.include(context, includeChildrenRecursively); + this.quasi.include(context, includeChildrenRecursively); + } + this.tag.includeCallArguments(context, this.callOptions.args); + const returnExpression = this.getReturnExpression(); + if (!returnExpression.included) { + returnExpression.include(context, false); + } } initialise(): void { this.callOptions = { - args: NO_ARGS, - thisParam: null, + args: [UNKNOWN_EXPRESSION, ...this.quasi.expressions], + thisParam: + this.tag instanceof MemberExpression && !this.tag.variable ? this.tag.object : null, withNew: false }; } @@ -52,4 +83,37 @@ export default class TaggedTemplateExpression extends NodeBase { this.tag.render(code, options, { isCalleeOfRenderedParent: true }); this.quasi.render(code, options); } + + protected applyDeoptimizations(): void { + this.deoptimized = true; + const { thisParam } = this.callOptions; + if (thisParam) { + this.tag.deoptimizeThisOnEventAtPath( + EVENT_CALLED, + EMPTY_PATH, + thisParam, + SHARED_RECURSION_TRACKER + ); + } + for (const argument of this.quasi.expressions) { + // This will make sure all properties of parameters behave as "unknown" + argument.deoptimizePath(UNKNOWN_PATH); + } + this.context.requestTreeshakingPass(); + } + + protected getReturnExpression( + recursionTracker: PathTracker = SHARED_RECURSION_TRACKER + ): ExpressionEntity { + if (this.returnExpression === null) { + this.returnExpression = UNKNOWN_EXPRESSION; + return (this.returnExpression = this.tag.getReturnExpressionWhenCalledAtPath( + EMPTY_PATH, + this.callOptions, + recursionTracker, + this + )); + } + return this.returnExpression; + } } diff --git a/src/ast/nodes/TryStatement.ts b/src/ast/nodes/TryStatement.ts index c34a5741d34..7c2fffcbfb7 100644 --- a/src/ast/nodes/TryStatement.ts +++ b/src/ast/nodes/TryStatement.ts @@ -18,8 +18,7 @@ export default class TryStatement extends StatementBase { return ( ((this.context.options.treeshake as NormalizedTreeshakingOptions).tryCatchDeoptimization ? this.block.body.length > 0 - : this.block.hasEffects(context)) || - (this.finalizer !== null && this.finalizer.hasEffects(context)) + : this.block.hasEffects(context)) || !!this.finalizer?.hasEffects(context) ); } @@ -47,8 +46,6 @@ export default class TryStatement extends StatementBase { this.handler.include(context, includeChildrenRecursively); context.brokenFlow = brokenFlow; } - if (this.finalizer !== null) { - this.finalizer.include(context, includeChildrenRecursively); - } + this.finalizer?.include(context, includeChildrenRecursively); } } diff --git a/src/ast/nodes/UnaryExpression.ts b/src/ast/nodes/UnaryExpression.ts index abe63aa8534..6a7ef31bb2d 100644 --- a/src/ast/nodes/UnaryExpression.ts +++ b/src/ast/nodes/UnaryExpression.ts @@ -33,7 +33,7 @@ export default class UnaryExpression extends NodeBase { ): LiteralValueOrUnknown { if (path.length > 0) return UnknownValue; const argumentValue = this.argument.getLiteralValueAtPath(EMPTY_PATH, recursionTracker, origin); - if (argumentValue === UnknownValue) return UnknownValue; + if (typeof argumentValue === 'symbol') return UnknownValue; return unaryOperators[this.operator](argumentValue); } diff --git a/src/ast/nodes/VariableDeclaration.ts b/src/ast/nodes/VariableDeclaration.ts index 5c56d8b7b5e..d4cffb0958c 100644 --- a/src/ast/nodes/VariableDeclaration.ts +++ b/src/ast/nodes/VariableDeclaration.ts @@ -18,6 +18,7 @@ import type Variable from '../variables/Variable'; import Identifier, { type IdentifierWithVariable } from './Identifier'; import * as NodeType from './NodeType'; import type VariableDeclarator from './VariableDeclarator'; +import { InclusionOptions } from './shared/Expression'; import { type IncludeChildren, NodeBase } from './shared/Node'; function areAllDeclarationsIncludedAndNotExported( @@ -52,22 +53,16 @@ export default class VariableDeclaration extends NodeBase { return false; } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { - this.included = true; - for (const declarator of this.declarations) { - if (includeChildrenRecursively || declarator.shouldBeIncluded(context)) - declarator.include(context, includeChildrenRecursively); - } - } - - includeAsSingleStatement( + include( context: InclusionContext, - includeChildrenRecursively: IncludeChildren + includeChildrenRecursively: IncludeChildren, + { asSingleStatement }: InclusionOptions = BLANK ): void { this.included = true; for (const declarator of this.declarations) { - if (includeChildrenRecursively || declarator.shouldBeIncluded(context)) { + if (includeChildrenRecursively || declarator.shouldBeIncluded(context)) declarator.include(context, includeChildrenRecursively); + if (asSingleStatement) { declarator.id.include(context, includeChildrenRecursively); } } @@ -97,7 +92,7 @@ export default class VariableDeclaration extends NodeBase { code.appendLeft(this.end, ';'); } } else { - this.renderReplacedDeclarations(code, options, nodeRenderOptions); + this.renderReplacedDeclarations(code, options); } } @@ -108,15 +103,12 @@ export default class VariableDeclaration extends NodeBase { actualContentEnd: number, renderedContentEnd: number, systemPatternExports: readonly Variable[], - options: RenderOptions, - isNoStatement: boolean | undefined + options: RenderOptions ): void { if (code.original.charCodeAt(this.end - 1) === 59 /*";"*/) { code.remove(this.end - 1, this.end); } - if (!isNoStatement) { - separatorString += ';'; - } + separatorString += ';'; if (lastSeparatorPos !== null) { if ( code.original.charCodeAt(actualContentEnd - 1) === 10 /*"\n"*/ && @@ -145,11 +137,7 @@ export default class VariableDeclaration extends NodeBase { } } - private renderReplacedDeclarations( - code: MagicString, - options: RenderOptions, - { isNoStatement }: NodeRenderOptions - ): void { + private renderReplacedDeclarations(code: MagicString, options: RenderOptions): void { const separatedNodes = getCommaSeparatedNodesWithBoundaries( this.declarations, code, @@ -231,8 +219,7 @@ export default class VariableDeclaration extends NodeBase { actualContentEnd!, renderedContentEnd, aggregatedSystemExports, - options, - isNoStatement + options ); } } diff --git a/src/ast/nodes/VariableDeclarator.ts b/src/ast/nodes/VariableDeclarator.ts index a6499ef4b45..e423b4a84f4 100644 --- a/src/ast/nodes/VariableDeclarator.ts +++ b/src/ast/nodes/VariableDeclarator.ts @@ -28,16 +28,16 @@ export default class VariableDeclarator extends NodeBase { } hasEffects(context: HasEffectsContext): boolean { - const initEffect = this.init !== null && this.init.hasEffects(context); + const initEffect = this.init?.hasEffects(context); this.id.markDeclarationReached(); return initEffect || this.id.hasEffects(context); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { this.included = true; - if (this.init) { - this.init.include(context, includeChildrenRecursively); - } + this.init?.include(context, includeChildrenRecursively, { + includeWithoutParameterDefaults: true + }); this.id.markDeclarationReached(); if (includeChildrenRecursively || this.id.shouldBeIncluded(context)) { this.id.include(context, includeChildrenRecursively); diff --git a/src/ast/nodes/WhileStatement.ts b/src/ast/nodes/WhileStatement.ts index d3bb193d888..95f69e2029d 100644 --- a/src/ast/nodes/WhileStatement.ts +++ b/src/ast/nodes/WhileStatement.ts @@ -31,7 +31,7 @@ export default class WhileStatement extends StatementBase { this.included = true; this.test.include(context, includeChildrenRecursively); const { brokenFlow } = context; - this.body.includeAsSingleStatement(context, includeChildrenRecursively); + this.body.include(context, includeChildrenRecursively, { asSingleStatement: true }); context.brokenFlow = brokenFlow; } } diff --git a/src/ast/nodes/YieldExpression.ts b/src/ast/nodes/YieldExpression.ts index 0d9600f1aee..60f675f3b65 100644 --- a/src/ast/nodes/YieldExpression.ts +++ b/src/ast/nodes/YieldExpression.ts @@ -1,7 +1,6 @@ import type MagicString from 'magic-string'; import type { RenderOptions } from '../../utils/renderHelpers'; import type { HasEffectsContext } from '../ExecutionContext'; -import { UNKNOWN_PATH } from '../utils/PathTracker'; import type * as NodeType from './NodeType'; import { type ExpressionNode, NodeBase } from './shared/Node'; @@ -13,9 +12,7 @@ export default class YieldExpression extends NodeBase { hasEffects(context: HasEffectsContext): boolean { if (!this.deoptimized) this.applyDeoptimizations(); - return ( - !context.ignore.returnYield || (this.argument !== null && this.argument.hasEffects(context)) - ); + return !context.ignore.returnYield || !!this.argument?.hasEffects(context); } render(code: MagicString, options: RenderOptions): void { @@ -26,13 +23,4 @@ export default class YieldExpression extends NodeBase { } } } - - protected applyDeoptimizations(): void { - this.deoptimized = true; - const { argument } = this; - if (argument) { - argument.deoptimizePath(UNKNOWN_PATH); - this.context.requestTreeshakingPass(); - } - } } diff --git a/src/ast/nodes/shared/CallExpressionBase.ts b/src/ast/nodes/shared/CallExpressionBase.ts new file mode 100644 index 00000000000..4f9c187b23d --- /dev/null +++ b/src/ast/nodes/shared/CallExpressionBase.ts @@ -0,0 +1,147 @@ +import type { CallOptions } from '../../CallOptions'; +import type { DeoptimizableEntity } from '../../DeoptimizableEntity'; +import type { HasEffectsContext } from '../../ExecutionContext'; +import { type NodeEvent } from '../../NodeEvents'; +import { type ObjectPath, type PathTracker, UNKNOWN_PATH } from '../../utils/PathTracker'; +import { + type ExpressionEntity, + type LiteralValueOrUnknown, + UNKNOWN_EXPRESSION, + UnknownValue +} from './Expression'; +import { NodeBase } from './Node'; + +export default abstract class CallExpressionBase extends NodeBase implements DeoptimizableEntity { + protected declare callOptions: CallOptions; + protected deoptimized = false; + protected returnExpression: ExpressionEntity | null = null; + private readonly deoptimizableDependentExpressions: DeoptimizableEntity[] = []; + private readonly expressionsToBeDeoptimized = new Set(); + + deoptimizeCache(): void { + if (this.returnExpression !== UNKNOWN_EXPRESSION) { + 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): void { + if ( + path.length === 0 || + this.context.deoptimizationTracker.trackEntityAtPathAndGetIfTracked(path, this) + ) { + return; + } + const returnExpression = this.getReturnExpression(); + if (returnExpression !== UNKNOWN_EXPRESSION) { + returnExpression.deoptimizePath(path); + } + } + + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ): void { + 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, + origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + const returnExpression = this.getReturnExpression(recursionTracker); + if (returnExpression === UNKNOWN_EXPRESSION) { + return UnknownValue; + } + 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; + } + return recursionTracker.withTrackedEntityAtPath( + path, + returnExpression, + () => { + this.deoptimizableDependentExpressions.push(origin); + return returnExpression.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); + }, + UNKNOWN_EXPRESSION + ); + } + + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + return ( + !context.accessed.trackEntityAtPathAndGetIfTracked(path, this) && + this.getReturnExpression().hasEffectsWhenAccessedAtPath(path, context) + ); + } + + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + return ( + !context.assigned.trackEntityAtPathAndGetIfTracked(path, this) && + this.getReturnExpression().hasEffectsWhenAssignedAtPath(path, context) + ); + } + + hasEffectsWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + context: HasEffectsContext + ): boolean { + return ( + !( + callOptions.withNew ? context.instantiated : context.called + ).trackEntityAtPathAndGetIfTracked(path, callOptions, this) && + this.getReturnExpression().hasEffectsWhenCalledAtPath(path, callOptions, context) + ); + } + + protected abstract getReturnExpression(recursionTracker?: PathTracker): ExpressionEntity; +} diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index f8823593b89..53bd8429cff 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -9,13 +9,14 @@ import { type ObjectPath, type PathTracker, SHARED_RECURSION_TRACKER, + UNKNOWN_PATH, UnknownKey } from '../../utils/PathTracker'; import type ClassBody from '../ClassBody'; import Identifier from '../Identifier'; import type Literal from '../Literal'; import MethodDefinition from '../MethodDefinition'; -import { type ExpressionEntity, type LiteralValueOrUnknown, UnknownValue } from './Expression'; +import { type ExpressionEntity, type LiteralValueOrUnknown } from './Expression'; import { type ExpressionNode, type IncludeChildren, NodeBase } from './Node'; import { ObjectEntity, type ObjectProperty } from './ObjectEntity'; import { ObjectMember } from './ObjectMember'; @@ -25,6 +26,7 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { declare body: ClassBody; declare id: Identifier | null; declare superClass: ExpressionNode | null; + protected deoptimized = false; private declare classConstructor: MethodDefinition | null; private objectEntity: ObjectEntity | null = null; @@ -38,6 +40,12 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { deoptimizePath(path: ObjectPath): void { this.getObjectEntity().deoptimizePath(path); + if (path.length === 1 && path[0] === UnknownKey) { + // A reassignment of UNKNOWN_PATH is considered equivalent to having lost track + // which means the constructor needs to be reassigned + this.classConstructor?.deoptimizePath(UNKNOWN_PATH); + this.superClass?.deoptimizePath(UNKNOWN_PATH); + } } deoptimizeThisOnEventAtPath( @@ -77,6 +85,7 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { } hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); const initEffect = this.superClass?.hasEffects(context) || this.body.hasEffects(context); this.id?.markDeclarationReached(); return initEffect || super.hasEffects(context); @@ -100,8 +109,8 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { !callOptions.withNew || (this.classConstructor !== null ? this.classConstructor.hasEffectsWhenCalledAtPath(EMPTY_PATH, callOptions, context) - : this.superClass !== null && - this.superClass.hasEffectsWhenCalledAtPath(path, callOptions, context)) + : this.superClass?.hasEffectsWhenCalledAtPath(path, callOptions, context)) || + false ); } else { return this.getObjectEntity().hasEffectsWhenCalledAtPath(path, callOptions, context); @@ -109,6 +118,7 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { + if (!this.deoptimized) this.applyDeoptimizations(); this.included = true; this.superClass?.include(context, includeChildrenRecursively); this.body.include(context, includeChildrenRecursively); @@ -129,6 +139,22 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { this.classConstructor = null; } + protected applyDeoptimizations(): void { + this.deoptimized = true; + for (const definition of this.body.body) { + if ( + !( + definition.static || + (definition instanceof MethodDefinition && definition.kind === 'constructor') + ) + ) { + // Calls to methods are not tracked, ensure that the return value is deoptimized + definition.deoptimizePath(UNKNOWN_PATH); + } + } + this.context.requestTreeshakingPass(); + } + private getObjectEntity(): ObjectEntity { if (this.objectEntity !== null) { return this.objectEntity; @@ -148,7 +174,7 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { SHARED_RECURSION_TRACKER, this ); - if (keyValue === UnknownValue) { + if (typeof keyValue === 'symbol') { properties.push({ key: UnknownKey, kind, property: definition }); continue; } else { diff --git a/src/ast/nodes/shared/Expression.ts b/src/ast/nodes/shared/Expression.ts index 60764af727c..72c20d5e27e 100644 --- a/src/ast/nodes/shared/Expression.ts +++ b/src/ast/nodes/shared/Expression.ts @@ -6,11 +6,20 @@ 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'; +import { IncludeChildren } from './Node'; export const UnknownValue = Symbol('Unknown Value'); +export const UnknownTruthyValue = Symbol('Unknown Truthy Value'); -export type LiteralValueOrUnknown = LiteralValue | typeof UnknownValue; +export type LiteralValueOrUnknown = LiteralValue | typeof UnknownValue | typeof UnknownTruthyValue; + +export interface InclusionOptions { + /** + * Include the id of a declarator even if unused to ensure it is a valid statement. + */ + asSingleStatement?: boolean; + includeWithoutParameterDefaults?: boolean; +} export class ExpressionEntity implements WritableEntity { included = false; @@ -64,18 +73,26 @@ export class ExpressionEntity implements WritableEntity { return true; } - include(_context: InclusionContext, _includeChildrenRecursively: IncludeChildren): void { + include( + _context: InclusionContext, + _includeChildrenRecursively: IncludeChildren, + _options?: InclusionOptions + ): void { this.included = true; } includeCallArguments( context: InclusionContext, - args: readonly (ExpressionNode | SpreadElement)[] + args: readonly (ExpressionEntity | SpreadElement)[] ): void { for (const arg of args) { arg.include(context, false); } } + + shouldBeIncluded(_context: InclusionContext): boolean { + return true; + } } export const UNKNOWN_EXPRESSION: ExpressionEntity = diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index 38b86e8c92a..071db59e72e 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -1,4 +1,5 @@ import type { NormalizedTreeshakingOptions } from '../../../rollup/types'; +import { BLANK } from '../../../utils/blank'; import { type CallOptions, NO_ARGS } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { @@ -8,12 +9,27 @@ import { } from '../../ExecutionContext'; import { NodeEvent } from '../../NodeEvents'; import ReturnValueScope from '../../scopes/ReturnValueScope'; -import { type ObjectPath, PathTracker, UNKNOWN_PATH, UnknownKey } from '../../utils/PathTracker'; +import { + EMPTY_PATH, + type ObjectPath, + PathTracker, + SHARED_RECURSION_TRACKER, + UNKNOWN_PATH, + UnknownKey +} from '../../utils/PathTracker'; +import LocalVariable from '../../variables/LocalVariable'; +import AssignmentPattern from '../AssignmentPattern'; import BlockStatement from '../BlockStatement'; import * as NodeType from '../NodeType'; import RestElement from '../RestElement'; import type SpreadElement from '../SpreadElement'; -import { type ExpressionEntity, LiteralValueOrUnknown, UNKNOWN_EXPRESSION } from './Expression'; +import { + type ExpressionEntity, + InclusionOptions, + LiteralValueOrUnknown, + UNKNOWN_EXPRESSION, + UnknownValue +} from './Expression'; import { type ExpressionNode, type GenericEsTreeNode, @@ -23,7 +39,7 @@ import { import { ObjectEntity } from './ObjectEntity'; import type { PatternNode } from './Pattern'; -export default abstract class FunctionBase extends NodeBase { +export default abstract class FunctionBase extends NodeBase implements DeoptimizableEntity { declare async: boolean; declare body: BlockStatement | ExpressionNode; declare params: readonly PatternNode[]; @@ -31,12 +47,19 @@ export default abstract class FunctionBase extends NodeBase { declare scope: ReturnValueScope; protected objectEntity: ObjectEntity | null = null; private deoptimizedReturn = false; + private forceIncludeParameters = false; + private declare parameterVariables: LocalVariable[][]; + + deoptimizeCache() { + this.forceIncludeParameters = true; + } deoptimizePath(path: ObjectPath): void { this.getObjectEntity().deoptimizePath(path); if (path.length === 1 && path[0] === UnknownKey) { // A reassignment of UNKNOWN_PATH is considered equivalent to having lost track // which means the return expression needs to be reassigned + this.forceIncludeParameters = true; this.scope.getReturnExpression().deoptimizePath(UNKNOWN_PATH); } } @@ -123,30 +146,88 @@ export default abstract class FunctionBase extends NodeBase { return true; } } - for (const param of this.params) { - if (param.hasEffects(context)) return true; + for (let position = 0; position < this.params.length; position++) { + const parameter = this.params[position]; + if (parameter instanceof AssignmentPattern) { + if (parameter.left.hasEffects(context)) { + return true; + } + const argumentValue = callOptions.args[position]?.getLiteralValueAtPath( + EMPTY_PATH, + SHARED_RECURSION_TRACKER, + this + ); + if ( + (argumentValue === undefined || argumentValue === UnknownValue) && + parameter.right.hasEffects(context) + ) { + return true; + } + } else if (parameter.hasEffects(context)) { + return true; + } } return false; } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { + include( + context: InclusionContext, + includeChildrenRecursively: IncludeChildren, + { includeWithoutParameterDefaults }: InclusionOptions = BLANK + ): void { this.included = true; const { brokenFlow } = context; context.brokenFlow = BROKEN_FLOW_NONE; this.body.include(context, includeChildrenRecursively); context.brokenFlow = brokenFlow; + if ( + !includeWithoutParameterDefaults || + includeChildrenRecursively || + this.forceIncludeParameters + ) { + for (const param of this.params) { + param.include(context, includeChildrenRecursively); + } + } } includeCallArguments( context: InclusionContext, - args: readonly (ExpressionNode | SpreadElement)[] + args: readonly (ExpressionEntity | SpreadElement)[] ): void { + for (let position = 0; position < this.params.length; position++) { + const parameter = this.params[position]; + if (parameter instanceof AssignmentPattern) { + if (parameter.left.shouldBeIncluded(context)) { + parameter.left.include(context, false); + } + const argumentValue = args[position]?.getLiteralValueAtPath( + EMPTY_PATH, + SHARED_RECURSION_TRACKER, + this + ); + // If argumentValue === UnknownTruthyValue, then we do not need to + // include the default + if ( + (argumentValue === undefined || argumentValue === UnknownValue) && + (this.parameterVariables[position].some(variable => variable.included) || + parameter.right.shouldBeIncluded(context)) + ) { + parameter.right.include(context, false); + } + } else if (parameter.shouldBeIncluded(context)) { + parameter.include(context, false); + } + } this.scope.includeCallArguments(context, args); } initialise(): void { + this.parameterVariables = this.params.map(param => + param.declare('parameter', UNKNOWN_EXPRESSION) + ); this.scope.addParameterVariables( - this.params.map(param => param.declare('parameter', UNKNOWN_EXPRESSION)), + this.parameterVariables, this.params[this.params.length - 1] instanceof RestElement ); if (this.body instanceof BlockStatement) { diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index c11a007cbc2..8a88d29de6e 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -1,11 +1,12 @@ +import { BLANK } from '../../../utils/blank'; import { type CallOptions } from '../../CallOptions'; import { type HasEffectsContext, type InclusionContext } from '../../ExecutionContext'; import { EVENT_CALLED, type NodeEvent } from '../../NodeEvents'; import FunctionScope from '../../scopes/FunctionScope'; import { type ObjectPath, PathTracker } from '../../utils/PathTracker'; import BlockStatement from '../BlockStatement'; -import Identifier, { type IdentifierWithVariable } from '../Identifier'; -import { type ExpressionEntity, UNKNOWN_EXPRESSION } from './Expression'; +import { type IdentifierWithVariable } from '../Identifier'; +import { type ExpressionEntity, InclusionOptions, UNKNOWN_EXPRESSION } from './Expression'; import FunctionBase from './FunctionBase'; import { type IncludeChildren } from './Node'; import { ObjectEntity } from './ObjectEntity'; @@ -38,7 +39,7 @@ export default class FunctionNode extends FunctionBase { } hasEffects(): boolean { - return this.id !== null && this.id.hasEffects(); + return !!this.id?.hasEffects(); } hasEffectsWhenCalledAtPath( @@ -72,15 +73,16 @@ export default class FunctionNode extends FunctionBase { return false; } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { - super.include(context, includeChildrenRecursively); - if (this.id) this.id.include(); - const hasArguments = this.scope.argumentsVariable.included; - for (const param of this.params) { - if (!(param instanceof Identifier) || hasArguments) { - param.include(context, includeChildrenRecursively); - } - } + include( + context: InclusionContext, + includeChildrenRecursively: IncludeChildren, + { includeWithoutParameterDefaults }: InclusionOptions = BLANK + ): void { + this.id?.include(); + super.include(context, includeChildrenRecursively, { + includeWithoutParameterDefaults: + includeWithoutParameterDefaults && !this.scope.argumentsVariable.included + }); } initialise(): void { diff --git a/src/ast/nodes/shared/MethodTypes.ts b/src/ast/nodes/shared/MethodTypes.ts index 78c061d5d18..f2cb1ea95a5 100644 --- a/src/ast/nodes/shared/MethodTypes.ts +++ b/src/ast/nodes/shared/MethodTypes.ts @@ -1,5 +1,5 @@ import { type CallOptions, NO_ARGS } from '../../CallOptions'; -import type { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; +import type { HasEffectsContext } from '../../ExecutionContext'; import { EVENT_CALLED, type NodeEvent } from '../../NodeEvents'; import { EMPTY_PATH, type ObjectPath, UNKNOWN_INTEGER_PATH } from '../../utils/PathTracker'; import { @@ -7,9 +7,7 @@ import { UNKNOWN_LITERAL_NUMBER, UNKNOWN_LITERAL_STRING } from '../../values'; -import type SpreadElement from '../SpreadElement'; import { ExpressionEntity, UNKNOWN_EXPRESSION } from './Expression'; -import type { ExpressionNode } from './Node'; type MethodDescription = { callsArgs: number[] | null; @@ -95,15 +93,6 @@ export class Method extends ExpressionEntity { } return false; } - - includeCallArguments( - context: InclusionContext, - args: readonly (ExpressionNode | SpreadElement)[] - ): void { - for (const arg of args) { - arg.include(context, false); - } - } } export const METHOD_RETURNS_BOOLEAN = [ diff --git a/src/ast/nodes/shared/Node.ts b/src/ast/nodes/shared/Node.ts index 656c4ef1c1f..6e9e4b97c7b 100644 --- a/src/ast/nodes/shared/Node.ts +++ b/src/ast/nodes/shared/Node.ts @@ -12,9 +12,10 @@ import { } from '../../ExecutionContext'; import { getAndCreateKeys, keys } from '../../keys'; import type ChildScope from '../../scopes/ChildScope'; +import { UNKNOWN_PATH } from '../../utils/PathTracker'; import type Variable from '../../variables/Variable'; import * as NodeType from '../NodeType'; -import { ExpressionEntity } from './Expression'; +import { ExpressionEntity, InclusionOptions } from './Expression'; export interface GenericEsTreeNode extends acorn.Node { [key: string]: any; @@ -60,15 +61,10 @@ export interface Node extends Entity { * if they are necessary for this node (e.g. a function body) or if they have effects. * Necessary variables need to be included as well. */ - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void; - - /** - * Alternative version of include to override the default behaviour of - * declarations to not include the id by default if the declarator has an effect. - */ - includeAsSingleStatement( + include( context: InclusionContext, - includeChildrenRecursively: IncludeChildren + includeChildrenRecursively: IncludeChildren, + options?: InclusionOptions ): void; render(code: MagicString, options: RenderOptions, nodeRenderOptions?: NodeRenderOptions): void; @@ -134,7 +130,7 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { if (value === null) continue; if (Array.isArray(value)) { for (const child of value) { - if (child !== null) child.bind(); + child?.bind(); } } else { value.bind(); @@ -156,14 +152,18 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { if (value === null) continue; if (Array.isArray(value)) { for (const child of value) { - if (child !== null && child.hasEffects(context)) return true; + if (child?.hasEffects(context)) return true; } } else if (value.hasEffects(context)) return true; } return false; } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { + include( + context: InclusionContext, + includeChildrenRecursively: IncludeChildren, + _options?: InclusionOptions + ): void { if (this.deoptimized === false) this.applyDeoptimizations(); this.included = true; for (const key of this.keys) { @@ -171,7 +171,7 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { if (value === null) continue; if (Array.isArray(value)) { for (const child of value) { - if (child !== null) child.include(context, includeChildrenRecursively); + child?.include(context, includeChildrenRecursively); } } else { value.include(context, includeChildrenRecursively); @@ -179,13 +179,6 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { } } - includeAsSingleStatement( - context: InclusionContext, - includeChildrenRecursively: IncludeChildren - ): void { - this.include(context, includeChildrenRecursively); - } - /** * Override to perform special initialisation steps after the scope is initialised */ @@ -235,7 +228,7 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { if (value === null) continue; if (Array.isArray(value)) { for (const child of value) { - if (child !== null) child.render(code, options); + child?.render(code, options); } } else { value.render(code, options); @@ -247,14 +240,35 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { return this.included || (!context.brokenFlow && this.hasEffects(createHasEffectsContext())); } - protected applyDeoptimizations(): void {} + /** + * Just deoptimize everything by default so that when e.g. we do not track + * something properly, it is deoptimized. + * @protected + */ + protected applyDeoptimizations(): void { + this.deoptimized = true; + for (const key of this.keys) { + const value = (this as GenericEsTreeNode)[key]; + if (value === null) continue; + if (Array.isArray(value)) { + for (const child of value) { + child?.deoptimizePath(UNKNOWN_PATH); + } + } else { + value.deoptimizePath(UNKNOWN_PATH); + } + } + this.context.requestTreeshakingPass(); + } } export { NodeBase as StatementBase }; -export function locateNode(node: Node): Location { - const location = locate(node.context.code, node.start, { offsetLine: 1 }); - (location as any).file = node.context.fileName; +export function locateNode(node: Node): Location & { file: string } { + const location = locate(node.context.code, node.start, { offsetLine: 1 }) as Location & { + file: string; + }; + location.file = node.context.fileName; location.toString = () => JSON.stringify(location); return location; diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts index 1aff8dcd598..c1eb584b80a 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -16,6 +16,7 @@ import { ExpressionEntity, LiteralValueOrUnknown, UNKNOWN_EXPRESSION, + UnknownTruthyValue, UnknownValue } from './Expression'; @@ -225,7 +226,7 @@ export class ObjectEntity extends ExpressionEntity { origin: DeoptimizableEntity ): LiteralValueOrUnknown { if (path.length === 0) { - return UnknownValue; + return UnknownTruthyValue; } const key = path[0]; const expressionAtPath = this.getMemberExpressionAndTrackDeopt(key, origin); diff --git a/src/ast/nodes/shared/ObjectMember.ts b/src/ast/nodes/shared/ObjectMember.ts index ac6871eb369..eded9c87631 100644 --- a/src/ast/nodes/shared/ObjectMember.ts +++ b/src/ast/nodes/shared/ObjectMember.ts @@ -51,7 +51,6 @@ export class ObjectMember extends ExpressionEntity { } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return false; return this.object.hasEffectsWhenAccessedAtPath([this.key, ...path], context); } diff --git a/src/ast/scopes/ClassBodyScope.ts b/src/ast/scopes/ClassBodyScope.ts index 029053102a3..c908f5a85a1 100644 --- a/src/ast/scopes/ClassBodyScope.ts +++ b/src/ast/scopes/ClassBodyScope.ts @@ -1,5 +1,5 @@ import type { AstContext } from '../../Module'; -import type { ExpressionEntity } from '../nodes/shared/Expression'; +import ClassNode from '../nodes/shared/ClassNode'; import LocalVariable from '../variables/LocalVariable'; import ThisVariable from '../variables/ThisVariable'; import ChildScope from './ChildScope'; @@ -9,7 +9,7 @@ export default class ClassBodyScope extends ChildScope { instanceScope: ChildScope; thisVariable: LocalVariable; - constructor(parent: Scope, classNode: ExpressionEntity, context: AstContext) { + constructor(parent: Scope, classNode: ClassNode, context: AstContext) { super(parent); this.variables.set( 'this', diff --git a/src/ast/scopes/FunctionScope.ts b/src/ast/scopes/FunctionScope.ts index 79daa9ccb96..fcc2a872bd6 100644 --- a/src/ast/scopes/FunctionScope.ts +++ b/src/ast/scopes/FunctionScope.ts @@ -1,7 +1,7 @@ import type { AstContext } from '../../Module'; import type { InclusionContext } from '../ExecutionContext'; import type SpreadElement from '../nodes/SpreadElement'; -import type { ExpressionNode } from '../nodes/shared/Node'; +import { ExpressionEntity } from '../nodes/shared/Expression'; import ArgumentsVariable from '../variables/ArgumentsVariable'; import ThisVariable from '../variables/ThisVariable'; import type ChildScope from './ChildScope'; @@ -23,7 +23,7 @@ export default class FunctionScope extends ReturnValueScope { includeCallArguments( context: InclusionContext, - args: readonly (ExpressionNode | SpreadElement)[] + args: readonly (ExpressionEntity | SpreadElement)[] ): void { super.includeCallArguments(context, args); if (this.argumentsVariable.included) { diff --git a/src/ast/scopes/ParameterScope.ts b/src/ast/scopes/ParameterScope.ts index b31d50238aa..4d69418aaae 100644 --- a/src/ast/scopes/ParameterScope.ts +++ b/src/ast/scopes/ParameterScope.ts @@ -2,8 +2,7 @@ import type { AstContext } from '../../Module'; import type { InclusionContext } from '../ExecutionContext'; import type Identifier from '../nodes/Identifier'; import SpreadElement from '../nodes/SpreadElement'; -import { UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; -import type { ExpressionNode } from '../nodes/shared/Node'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; import LocalVariable from '../variables/LocalVariable'; import ChildScope from './ChildScope'; import type Scope from './Scope'; @@ -49,7 +48,7 @@ export default class ParameterScope extends ChildScope { includeCallArguments( context: InclusionContext, - args: readonly (ExpressionNode | SpreadElement)[] + args: readonly (ExpressionEntity | SpreadElement)[] ): void { let calledFromTryStatement = false; let argIncluded = false; diff --git a/src/ast/values.ts b/src/ast/values.ts index ceda252a22b..1cf037140db 100644 --- a/src/ast/values.ts +++ b/src/ast/values.ts @@ -1,7 +1,7 @@ import { type CallOptions, NO_ARGS } from './CallOptions'; import type { HasEffectsContext } from './ExecutionContext'; import type { LiteralValue } from './nodes/Literal'; -import { ExpressionEntity, UNKNOWN_EXPRESSION, UnknownValue } from './nodes/shared/Expression'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './nodes/shared/Expression'; import { EMPTY_PATH, type ObjectPath, @@ -145,9 +145,9 @@ const stringReplace: RawMemberDescription = { const arg1 = callOptions.args[1]; return ( callOptions.args.length < 2 || - (arg1.getLiteralValueAtPath(EMPTY_PATH, SHARED_RECURSION_TRACKER, { + (typeof arg1.getLiteralValueAtPath(EMPTY_PATH, SHARED_RECURSION_TRACKER, { deoptimizeCache() {} - }) === UnknownValue && + }) === 'symbol' && arg1.hasEffectsWhenCalledAtPath( EMPTY_PATH, { diff --git a/src/ast/variables/GlobalVariable.ts b/src/ast/variables/GlobalVariable.ts index 7d6928c5f9c..e79ecd421e5 100644 --- a/src/ast/variables/GlobalVariable.ts +++ b/src/ast/variables/GlobalVariable.ts @@ -1,7 +1,14 @@ import { CallOptions } from '../CallOptions'; +import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; +import { + LiteralValueOrUnknown, + UnknownTruthyValue, + UnknownValue +} from '../nodes/shared/Expression'; import { getGlobalAtPath } from '../nodes/shared/knownGlobals'; import type { ObjectPath } from '../utils/PathTracker'; +import { PathTracker } from '../utils/PathTracker'; import Variable from './Variable'; export default class GlobalVariable extends Variable { @@ -9,12 +16,20 @@ export default class GlobalVariable extends Variable { // been reassigned isReassigned = true; + getLiteralValueAtPath( + path: ObjectPath, + _recursionTracker: PathTracker, + _origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + return getGlobalAtPath([this.name, ...path]) ? UnknownTruthyValue : UnknownValue; + } + hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean { if (path.length === 0) { // Technically, "undefined" is a global variable of sorts - return this.name !== 'undefined' && getGlobalAtPath([this.name]) === null; + return this.name !== 'undefined' && !getGlobalAtPath([this.name]); } - return getGlobalAtPath([this.name, ...path].slice(0, -1)) === null; + return !getGlobalAtPath([this.name, ...path].slice(0, -1)); } hasEffectsWhenCalledAtPath( @@ -23,6 +38,6 @@ export default class GlobalVariable extends Variable { context: HasEffectsContext ): boolean { const globalAtPath = getGlobalAtPath([this.name, ...path]); - return globalAtPath === null || globalAtPath.hasEffectsWhenCalled(callOptions, context); + return !globalAtPath || globalAtPath.hasEffectsWhenCalled(callOptions, context); } } diff --git a/src/ast/variables/LocalVariable.ts b/src/ast/variables/LocalVariable.ts index a90613a2825..e585ea775a3 100644 --- a/src/ast/variables/LocalVariable.ts +++ b/src/ast/variables/LocalVariable.ts @@ -1,5 +1,4 @@ -import type Module from '../../Module'; -import type { AstContext } from '../../Module'; +import Module, { AstContext } from '../../Module'; import type { CallOptions } from '../CallOptions'; import type { DeoptimizableEntity } from '../DeoptimizableEntity'; import { createInclusionContext, HasEffectsContext, InclusionContext } from '../ExecutionContext'; @@ -14,7 +13,7 @@ import { UNKNOWN_EXPRESSION, UnknownValue } from '../nodes/shared/Expression'; -import type { ExpressionNode, Node } from '../nodes/shared/Node'; +import type { Node } from '../nodes/shared/Node'; import { type ObjectPath, type PathTracker, UNKNOWN_PATH } from '../utils/PathTracker'; import Variable from './Variable'; @@ -192,7 +191,7 @@ export default class LocalVariable extends Variable { includeCallArguments( context: InclusionContext, - args: readonly (ExpressionNode | SpreadElement)[] + args: readonly (ExpressionEntity | SpreadElement)[] ): void { if (this.isReassigned || (this.init && context.includedCallArguments.has(this.init))) { for (const arg of args) { diff --git a/test/form/samples/body-less-for-loops/_expected/es.js b/test/form/samples/body-less-for-loops/_expected.js similarity index 100% rename from test/form/samples/body-less-for-loops/_expected/es.js rename to test/form/samples/body-less-for-loops/_expected.js diff --git a/test/form/samples/body-less-for-loops/_expected/amd.js b/test/form/samples/body-less-for-loops/_expected/amd.js deleted file mode 100644 index 62a1a7223af..00000000000 --- a/test/form/samples/body-less-for-loops/_expected/amd.js +++ /dev/null @@ -1,16 +0,0 @@ -define((function () { 'use strict'; - - for ( let i = 0; i < 10; i += 1 ) console.log( i ); - for ( const letter of array ) console.log( letter ); - for ( const index in array ) console.log( index ); - - let i; - for ( i = 0; i < 10; i += 1 ) console.log( i ); - - let letter; - for ( letter of array ) console.log( letter ); - - let index; - for ( index in array ) console.log( index ); - -})); diff --git a/test/form/samples/body-less-for-loops/_expected/cjs.js b/test/form/samples/body-less-for-loops/_expected/cjs.js deleted file mode 100644 index c230de3843d..00000000000 --- a/test/form/samples/body-less-for-loops/_expected/cjs.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -for ( let i = 0; i < 10; i += 1 ) console.log( i ); -for ( const letter of array ) console.log( letter ); -for ( const index in array ) console.log( index ); - -let i; -for ( i = 0; i < 10; i += 1 ) console.log( i ); - -let letter; -for ( letter of array ) console.log( letter ); - -let index; -for ( index in array ) console.log( index ); diff --git a/test/form/samples/body-less-for-loops/_expected/iife.js b/test/form/samples/body-less-for-loops/_expected/iife.js deleted file mode 100644 index 69787edce4c..00000000000 --- a/test/form/samples/body-less-for-loops/_expected/iife.js +++ /dev/null @@ -1,17 +0,0 @@ -(function () { - 'use strict'; - - for ( let i = 0; i < 10; i += 1 ) console.log( i ); - for ( const letter of array ) console.log( letter ); - for ( const index in array ) console.log( index ); - - let i; - for ( i = 0; i < 10; i += 1 ) console.log( i ); - - let letter; - for ( letter of array ) console.log( letter ); - - let index; - for ( index in array ) console.log( index ); - -})(); diff --git a/test/form/samples/body-less-for-loops/_expected/system.js b/test/form/samples/body-less-for-loops/_expected/system.js deleted file mode 100644 index 93a5465ef92..00000000000 --- a/test/form/samples/body-less-for-loops/_expected/system.js +++ /dev/null @@ -1,21 +0,0 @@ -System.register([], (function () { - 'use strict'; - return { - execute: (function () { - - for ( let i = 0; i < 10; i += 1 ) console.log( i ); - for ( const letter of array ) console.log( letter ); - for ( const index in array ) console.log( index ); - - let i; - for ( i = 0; i < 10; i += 1 ) console.log( i ); - - let letter; - for ( letter of array ) console.log( letter ); - - let index; - for ( index in array ) console.log( index ); - - }) - }; -})); diff --git a/test/form/samples/body-less-for-loops/_expected/umd.js b/test/form/samples/body-less-for-loops/_expected/umd.js deleted file mode 100644 index ea7ab3fab33..00000000000 --- a/test/form/samples/body-less-for-loops/_expected/umd.js +++ /dev/null @@ -1,19 +0,0 @@ -(function (factory) { - typeof define === 'function' && define.amd ? define(factory) : - factory(); -})((function () { 'use strict'; - - for ( let i = 0; i < 10; i += 1 ) console.log( i ); - for ( const letter of array ) console.log( letter ); - for ( const index in array ) console.log( index ); - - let i; - for ( i = 0; i < 10; i += 1 ) console.log( i ); - - let letter; - for ( letter of array ) console.log( letter ); - - let index; - for ( index in array ) console.log( index ); - -})); diff --git a/test/form/samples/function-call-parameters/_expected/es.js b/test/form/samples/function-call-parameters/_expected.js similarity index 100% rename from test/form/samples/function-call-parameters/_expected/es.js rename to test/form/samples/function-call-parameters/_expected.js diff --git a/test/form/samples/function-call-parameters/_expected/amd.js b/test/form/samples/function-call-parameters/_expected/amd.js deleted file mode 100644 index a6108eadc05..00000000000 --- a/test/form/samples/function-call-parameters/_expected/amd.js +++ /dev/null @@ -1,46 +0,0 @@ -define((function () { 'use strict'; - - // parameters are associated correctly - const retained1 = function ( func, obj ) { return func( obj ); }; - retained1( obj => obj(), () => () => console.log( 'effect' ) )(); - - const retained2 = function ( func, obj ) { return func( obj ); }; - retained2( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; - - // parameters and arguments have the same values - function retained3 ( x ) { - x.foo.bar = 1; - } - - retained3( {} ); - - function retained4 ( x ) { - arguments[ 0 ].foo.bar = 1; - } - - retained4( {} ); - - // assigning to an argument will change the corresponding parameter - function retained5 ( x ) { - arguments[ 0 ] = {}; - x.foo.bar = 1; - } - - retained5( { foo: {} } ); - - // assigning to a parameter will change the corresponding argument - function retained6 ( x ) { - x = {}; - arguments[ 0 ].foo.bar = 1; - } - - retained6( { foo: {} } ); - - // the number of arguments does not depend on the number of parameters - function retained7 ( x ) { - arguments[ 1 ].foo.bar = 1; - } - - retained7( {}, {} ); - -})); diff --git a/test/form/samples/function-call-parameters/_expected/cjs.js b/test/form/samples/function-call-parameters/_expected/cjs.js deleted file mode 100644 index 69e2e8f7186..00000000000 --- a/test/form/samples/function-call-parameters/_expected/cjs.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -// parameters are associated correctly -const retained1 = function ( func, obj ) { return func( obj ); }; -retained1( obj => obj(), () => () => console.log( 'effect' ) )(); - -const retained2 = function ( func, obj ) { return func( obj ); }; -retained2( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; - -// parameters and arguments have the same values -function retained3 ( x ) { - x.foo.bar = 1; -} - -retained3( {} ); - -function retained4 ( x ) { - arguments[ 0 ].foo.bar = 1; -} - -retained4( {} ); - -// assigning to an argument will change the corresponding parameter -function retained5 ( x ) { - arguments[ 0 ] = {}; - x.foo.bar = 1; -} - -retained5( { foo: {} } ); - -// assigning to a parameter will change the corresponding argument -function retained6 ( x ) { - x = {}; - arguments[ 0 ].foo.bar = 1; -} - -retained6( { foo: {} } ); - -// the number of arguments does not depend on the number of parameters -function retained7 ( x ) { - arguments[ 1 ].foo.bar = 1; -} - -retained7( {}, {} ); diff --git a/test/form/samples/function-call-parameters/_expected/iife.js b/test/form/samples/function-call-parameters/_expected/iife.js deleted file mode 100644 index 599667b4f90..00000000000 --- a/test/form/samples/function-call-parameters/_expected/iife.js +++ /dev/null @@ -1,47 +0,0 @@ -(function () { - 'use strict'; - - // parameters are associated correctly - const retained1 = function ( func, obj ) { return func( obj ); }; - retained1( obj => obj(), () => () => console.log( 'effect' ) )(); - - const retained2 = function ( func, obj ) { return func( obj ); }; - retained2( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; - - // parameters and arguments have the same values - function retained3 ( x ) { - x.foo.bar = 1; - } - - retained3( {} ); - - function retained4 ( x ) { - arguments[ 0 ].foo.bar = 1; - } - - retained4( {} ); - - // assigning to an argument will change the corresponding parameter - function retained5 ( x ) { - arguments[ 0 ] = {}; - x.foo.bar = 1; - } - - retained5( { foo: {} } ); - - // assigning to a parameter will change the corresponding argument - function retained6 ( x ) { - x = {}; - arguments[ 0 ].foo.bar = 1; - } - - retained6( { foo: {} } ); - - // the number of arguments does not depend on the number of parameters - function retained7 ( x ) { - arguments[ 1 ].foo.bar = 1; - } - - retained7( {}, {} ); - -})(); diff --git a/test/form/samples/function-call-parameters/_expected/system.js b/test/form/samples/function-call-parameters/_expected/system.js deleted file mode 100644 index 377df4947fe..00000000000 --- a/test/form/samples/function-call-parameters/_expected/system.js +++ /dev/null @@ -1,51 +0,0 @@ -System.register([], (function () { - 'use strict'; - return { - execute: (function () { - - // parameters are associated correctly - const retained1 = function ( func, obj ) { return func( obj ); }; - retained1( obj => obj(), () => () => console.log( 'effect' ) )(); - - const retained2 = function ( func, obj ) { return func( obj ); }; - retained2( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; - - // parameters and arguments have the same values - function retained3 ( x ) { - x.foo.bar = 1; - } - - retained3( {} ); - - function retained4 ( x ) { - arguments[ 0 ].foo.bar = 1; - } - - retained4( {} ); - - // assigning to an argument will change the corresponding parameter - function retained5 ( x ) { - arguments[ 0 ] = {}; - x.foo.bar = 1; - } - - retained5( { foo: {} } ); - - // assigning to a parameter will change the corresponding argument - function retained6 ( x ) { - x = {}; - arguments[ 0 ].foo.bar = 1; - } - - retained6( { foo: {} } ); - - // the number of arguments does not depend on the number of parameters - function retained7 ( x ) { - arguments[ 1 ].foo.bar = 1; - } - - retained7( {}, {} ); - - }) - }; -})); diff --git a/test/form/samples/function-call-parameters/_expected/umd.js b/test/form/samples/function-call-parameters/_expected/umd.js deleted file mode 100644 index a00ea3f3a2a..00000000000 --- a/test/form/samples/function-call-parameters/_expected/umd.js +++ /dev/null @@ -1,49 +0,0 @@ -(function (factory) { - typeof define === 'function' && define.amd ? define(factory) : - factory(); -})((function () { 'use strict'; - - // parameters are associated correctly - const retained1 = function ( func, obj ) { return func( obj ); }; - retained1( obj => obj(), () => () => console.log( 'effect' ) )(); - - const retained2 = function ( func, obj ) { return func( obj ); }; - retained2( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; - - // parameters and arguments have the same values - function retained3 ( x ) { - x.foo.bar = 1; - } - - retained3( {} ); - - function retained4 ( x ) { - arguments[ 0 ].foo.bar = 1; - } - - retained4( {} ); - - // assigning to an argument will change the corresponding parameter - function retained5 ( x ) { - arguments[ 0 ] = {}; - x.foo.bar = 1; - } - - retained5( { foo: {} } ); - - // assigning to a parameter will change the corresponding argument - function retained6 ( x ) { - x = {}; - arguments[ 0 ].foo.bar = 1; - } - - retained6( { foo: {} } ); - - // the number of arguments does not depend on the number of parameters - function retained7 ( x ) { - arguments[ 1 ].foo.bar = 1; - } - - retained7( {}, {} ); - -})); diff --git a/test/form/samples/parameter-defaults/default-parameter-side-effects/_config.js b/test/form/samples/parameter-defaults/default-parameter-side-effects/_config.js new file mode 100644 index 00000000000..7a232d2ace8 --- /dev/null +++ b/test/form/samples/parameter-defaults/default-parameter-side-effects/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles side effects of default parameters' +}; diff --git a/test/form/samples/parameter-defaults/default-parameter-side-effects/_expected.js b/test/form/samples/parameter-defaults/default-parameter-side-effects/_expected.js new file mode 100644 index 00000000000..d5e814ca54a --- /dev/null +++ b/test/form/samples/parameter-defaults/default-parameter-side-effects/_expected.js @@ -0,0 +1,17 @@ +const a = (p = 'retained') => console.log(p); +a(); + +const b = (p) => console.log(p); +b('value'); + +const c = (p = console.log('retained because of side effect')) => {}; +c(); + +const d = (p) => console.log('effect'); +d(); + +const e = (p) => console.log('effect'); +e(); + +const f = ({ x = console.log('retained') }) => {}; +f('value'); diff --git a/test/form/samples/parameter-defaults/default-parameter-side-effects/main.js b/test/form/samples/parameter-defaults/default-parameter-side-effects/main.js new file mode 100644 index 00000000000..bc551eb69bc --- /dev/null +++ b/test/form/samples/parameter-defaults/default-parameter-side-effects/main.js @@ -0,0 +1,20 @@ +const a = (p = 'retained') => console.log(p); +a(); + +const b = (p = console.log('removed')) => console.log(p); +b('value'); + +const c = (p = console.log('retained because of side effect')) => {}; +c(); + +const d = (p = 'removed because no side effect') => console.log('effect'); +d(); + +const e = (p = console.log('removed')) => console.log('effect'); +e('value'); + +const f = ({ x = console.log('retained') } = {}) => {}; +f('value'); + +const removed = (p = console.log('ignored')) => {}; +removed('value'); diff --git a/test/form/samples/parameter-defaults/functions/_config.js b/test/form/samples/parameter-defaults/functions/_config.js new file mode 100644 index 00000000000..0faee52a9f7 --- /dev/null +++ b/test/form/samples/parameter-defaults/functions/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'supports tree-shaking for unused default parameter values for functions' +}; diff --git a/test/form/samples/parameter-defaults/functions/_expected.js b/test/form/samples/parameter-defaults/functions/_expected.js new file mode 100644 index 00000000000..945aec4a8bc --- /dev/null +++ b/test/form/samples/parameter-defaults/functions/_expected.js @@ -0,0 +1,21 @@ +var isUndefined; + +function funDecl(a = 'retained', b = 'retained', c, d) { + console.log(a, b, c); +} + +funDecl(isUndefined, 'b', 'c'); +funDecl('a', globalThis.unknown, 'c'); + +const funExp = function (a = 'retained', b = 'retained', c, d) { + console.log(a, b, c); +}; + +funExp(isUndefined, 'b', 'c'); +funExp('a', globalThis.unknown, 'c'); + +const arrow = (a = 'retained', b = 'retained', c, d) => + console.log(a, b, c); + +arrow(isUndefined, 'b', 'c'); +arrow('a', globalThis.unknown, 'c'); diff --git a/test/form/samples/parameter-defaults/functions/main.js b/test/form/samples/parameter-defaults/functions/main.js new file mode 100644 index 00000000000..048cf4dbae2 --- /dev/null +++ b/test/form/samples/parameter-defaults/functions/main.js @@ -0,0 +1,21 @@ +var isUndefined; + +function funDecl(a = 'retained', b = 'retained', c = 'removed', d = 'removed') { + console.log(a, b, c); +} + +funDecl(isUndefined, 'b', 'c'); +funDecl('a', globalThis.unknown, 'c'); + +const funExp = function (a = 'retained', b = 'retained', c = 'removed', d = 'removed') { + console.log(a, b, c); +}; + +funExp(isUndefined, 'b', 'c'); +funExp('a', globalThis.unknown, 'c'); + +const arrow = (a = 'retained', b = 'retained', c = 'removed', d = 'removed') => + console.log(a, b, c); + +arrow(isUndefined, 'b', 'c'); +arrow('a', globalThis.unknown, 'c'); diff --git a/test/form/samples/parameter-defaults/non-literal-arguments/_config.js b/test/form/samples/parameter-defaults/non-literal-arguments/_config.js new file mode 100644 index 00000000000..341af06cb2d --- /dev/null +++ b/test/form/samples/parameter-defaults/non-literal-arguments/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'recognizes non-literal arguments as not undefined' +}; diff --git a/test/form/samples/parameter-defaults/non-literal-arguments/_expected.js b/test/form/samples/parameter-defaults/non-literal-arguments/_expected.js new file mode 100644 index 00000000000..3ac927db486 --- /dev/null +++ b/test/form/samples/parameter-defaults/non-literal-arguments/_expected.js @@ -0,0 +1,11 @@ +function test(a) { + console.log(a); +} + +test({}); +test([]); +test(() => {}); +test(function () {}); +function a(){} +test(a); +test(Symbol); diff --git a/test/form/samples/parameter-defaults/non-literal-arguments/main.js b/test/form/samples/parameter-defaults/non-literal-arguments/main.js new file mode 100644 index 00000000000..6f3421429ae --- /dev/null +++ b/test/form/samples/parameter-defaults/non-literal-arguments/main.js @@ -0,0 +1,11 @@ +function test(a = 'removed') { + console.log(a); +} + +test({}); +test([]); +test(() => {}); +test(function () {}); +function a(){} +test(a); +test(Symbol); diff --git a/test/form/samples/side-effects-parameter-defaults/_config.js b/test/form/samples/parameter-defaults/side-effects/_config.js similarity index 100% rename from test/form/samples/side-effects-parameter-defaults/_config.js rename to test/form/samples/parameter-defaults/side-effects/_config.js diff --git a/test/form/samples/side-effects-parameter-defaults/_expected/amd.js b/test/form/samples/parameter-defaults/side-effects/_expected/amd.js similarity index 100% rename from test/form/samples/side-effects-parameter-defaults/_expected/amd.js rename to test/form/samples/parameter-defaults/side-effects/_expected/amd.js diff --git a/test/form/samples/side-effects-parameter-defaults/_expected/cjs.js b/test/form/samples/parameter-defaults/side-effects/_expected/cjs.js similarity index 100% rename from test/form/samples/side-effects-parameter-defaults/_expected/cjs.js rename to test/form/samples/parameter-defaults/side-effects/_expected/cjs.js diff --git a/test/form/samples/side-effects-parameter-defaults/_expected/es.js b/test/form/samples/parameter-defaults/side-effects/_expected/es.js similarity index 100% rename from test/form/samples/side-effects-parameter-defaults/_expected/es.js rename to test/form/samples/parameter-defaults/side-effects/_expected/es.js diff --git a/test/form/samples/side-effects-parameter-defaults/_expected/iife.js b/test/form/samples/parameter-defaults/side-effects/_expected/iife.js similarity index 100% rename from test/form/samples/side-effects-parameter-defaults/_expected/iife.js rename to test/form/samples/parameter-defaults/side-effects/_expected/iife.js diff --git a/test/form/samples/side-effects-parameter-defaults/_expected/system.js b/test/form/samples/parameter-defaults/side-effects/_expected/system.js similarity index 100% rename from test/form/samples/side-effects-parameter-defaults/_expected/system.js rename to test/form/samples/parameter-defaults/side-effects/_expected/system.js diff --git a/test/form/samples/side-effects-parameter-defaults/_expected/umd.js b/test/form/samples/parameter-defaults/side-effects/_expected/umd.js similarity index 100% rename from test/form/samples/side-effects-parameter-defaults/_expected/umd.js rename to test/form/samples/parameter-defaults/side-effects/_expected/umd.js diff --git a/test/form/samples/side-effects-parameter-defaults/main.js b/test/form/samples/parameter-defaults/side-effects/main.js similarity index 100% rename from test/form/samples/side-effects-parameter-defaults/main.js rename to test/form/samples/parameter-defaults/side-effects/main.js diff --git a/test/form/samples/parameter-defaults/super-class-methods/_config.js b/test/form/samples/parameter-defaults/super-class-methods/_config.js new file mode 100644 index 00000000000..6dbe1e96dd5 --- /dev/null +++ b/test/form/samples/parameter-defaults/super-class-methods/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'supports tree-shaking for unused default parameter values on super classes' +}; diff --git a/test/form/samples/parameter-defaults/super-class-methods/_expected.js b/test/form/samples/parameter-defaults/super-class-methods/_expected.js new file mode 100644 index 00000000000..167fdc9faff --- /dev/null +++ b/test/form/samples/parameter-defaults/super-class-methods/_expected.js @@ -0,0 +1,17 @@ +class TestSuper { + constructor(a = 'retained') { + console.log(a); + } + + static staticMethod(a = 'retained') { + console.log(a); + } + + static staticProp = (a = 'retained') => console.log(a); +} + +class Test extends TestSuper {} + +new Test().method(); +Test.staticMethod(); +Test.staticProp(); diff --git a/test/form/samples/parameter-defaults/super-class-methods/main.js b/test/form/samples/parameter-defaults/super-class-methods/main.js new file mode 100644 index 00000000000..167fdc9faff --- /dev/null +++ b/test/form/samples/parameter-defaults/super-class-methods/main.js @@ -0,0 +1,17 @@ +class TestSuper { + constructor(a = 'retained') { + console.log(a); + } + + static staticMethod(a = 'retained') { + console.log(a); + } + + static staticProp = (a = 'retained') => console.log(a); +} + +class Test extends TestSuper {} + +new Test().method(); +Test.staticMethod(); +Test.staticProp(); diff --git a/test/form/samples/parameter-defaults/termplate-tags/_config.js b/test/form/samples/parameter-defaults/termplate-tags/_config.js new file mode 100644 index 00000000000..6c444966620 --- /dev/null +++ b/test/form/samples/parameter-defaults/termplate-tags/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'supports tree-shaking for unused default parameter values on template tags' +}; diff --git a/test/form/samples/parameter-defaults/termplate-tags/_expected.js b/test/form/samples/parameter-defaults/termplate-tags/_expected.js new file mode 100644 index 00000000000..39bc336e378 --- /dev/null +++ b/test/form/samples/parameter-defaults/termplate-tags/_expected.js @@ -0,0 +1,6 @@ +const templateTag = (_, a = 'retained', b = 'retained', c, d) => { + console.log(a, b, c); +}; + +templateTag`${isUndefined}${'b'}${'c'}`; +templateTag`${'a'}${globalThis.unknown}${'c'}`; diff --git a/test/form/samples/parameter-defaults/termplate-tags/main.js b/test/form/samples/parameter-defaults/termplate-tags/main.js new file mode 100644 index 00000000000..bed212d6f45 --- /dev/null +++ b/test/form/samples/parameter-defaults/termplate-tags/main.js @@ -0,0 +1,6 @@ +const templateTag = (_, a = 'retained', b = 'retained', c = 'removed', d = 'removed') => { + console.log(a, b, c); +}; + +templateTag`${isUndefined}${'b'}${'c'}`; +templateTag`${'a'}${globalThis.unknown}${'c'}`; diff --git a/test/form/samples/side-effect-e/_expected/es.js b/test/form/samples/side-effect-e/_expected.js similarity index 78% rename from test/form/samples/side-effect-e/_expected/es.js rename to test/form/samples/side-effect-e/_expected.js index 3067a16c289..94df6848308 100644 --- a/test/form/samples/side-effect-e/_expected/es.js +++ b/test/form/samples/side-effect-e/_expected.js @@ -10,7 +10,3 @@ function foo () { } foo(); - -var main = 42; - -export { main as default }; diff --git a/test/form/samples/side-effect-e/_expected/amd.js b/test/form/samples/side-effect-e/_expected/amd.js deleted file mode 100644 index 9f4ef975e84..00000000000 --- a/test/form/samples/side-effect-e/_expected/amd.js +++ /dev/null @@ -1,20 +0,0 @@ -define((function () { 'use strict'; - - function foo () { - var Object = { - keys: function () { - console.log( 'side-effect' ); - } - }; - - var obj = { foo: 1, bar: 2 }; - Object.keys( obj ); - } - - foo(); - - var main = 42; - - return main; - -})); diff --git a/test/form/samples/side-effect-e/_expected/cjs.js b/test/form/samples/side-effect-e/_expected/cjs.js deleted file mode 100644 index 249b9936f03..00000000000 --- a/test/form/samples/side-effect-e/_expected/cjs.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -function foo () { - var Object = { - keys: function () { - console.log( 'side-effect' ); - } - }; - - var obj = { foo: 1, bar: 2 }; - Object.keys( obj ); -} - -foo(); - -var main = 42; - -module.exports = main; diff --git a/test/form/samples/side-effect-e/_expected/iife.js b/test/form/samples/side-effect-e/_expected/iife.js deleted file mode 100644 index de3c6013977..00000000000 --- a/test/form/samples/side-effect-e/_expected/iife.js +++ /dev/null @@ -1,21 +0,0 @@ -var myBundle = (function () { - 'use strict'; - - function foo () { - var Object = { - keys: function () { - console.log( 'side-effect' ); - } - }; - - var obj = { foo: 1, bar: 2 }; - Object.keys( obj ); - } - - foo(); - - var main = 42; - - return main; - -})(); diff --git a/test/form/samples/side-effect-e/_expected/system.js b/test/form/samples/side-effect-e/_expected/system.js deleted file mode 100644 index baef598d676..00000000000 --- a/test/form/samples/side-effect-e/_expected/system.js +++ /dev/null @@ -1,23 +0,0 @@ -System.register('myBundle', [], (function (exports) { - 'use strict'; - return { - execute: (function () { - - function foo () { - var Object = { - keys: function () { - console.log( 'side-effect' ); - } - }; - - var obj = { foo: 1, bar: 2 }; - Object.keys( obj ); - } - - foo(); - - var main = exports('default', 42); - - }) - }; -})); diff --git a/test/form/samples/side-effect-e/_expected/umd.js b/test/form/samples/side-effect-e/_expected/umd.js deleted file mode 100644 index 0ef9db79ea7..00000000000 --- a/test/form/samples/side-effect-e/_expected/umd.js +++ /dev/null @@ -1,24 +0,0 @@ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.myBundle = factory()); -})(this, (function () { 'use strict'; - - function foo () { - var Object = { - keys: function () { - console.log( 'side-effect' ); - } - }; - - var obj = { foo: 1, bar: 2 }; - Object.keys( obj ); - } - - foo(); - - var main = 42; - - return main; - -})); diff --git a/test/form/samples/side-effect-e/main.js b/test/form/samples/side-effect-e/main.js index b96dfdf5d71..1ac85f8de9e 100644 --- a/test/form/samples/side-effect-e/main.js +++ b/test/form/samples/side-effect-e/main.js @@ -10,5 +10,3 @@ function foo () { } foo(); - -export default 42; 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 index 936fab7c5a4..b6203e675ca 100644 --- a/test/form/samples/super-classes/super-class-prototype-access/_expected.js +++ b/test/form/samples/super-classes/super-class-prototype-access/_expected.js @@ -4,5 +4,4 @@ class SuperAccess { } 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 index fcf38d4ead6..240e4365a28 100644 --- a/test/form/samples/super-classes/super-class-prototype-access/main.js +++ b/test/form/samples/super-classes/super-class-prototype-access/main.js @@ -6,6 +6,4 @@ 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/main.js b/test/form/samples/super-classes/super-class-prototype-assignment/main.js index d47b557daf5..06ef7c3d692 100644 --- a/test/form/samples/super-classes/super-class-prototype-assignment/main.js +++ b/test/form/samples/super-classes/super-class-prototype-assignment/main.js @@ -1,10 +1,8 @@ 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 { 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 index 478810e2b22..9f4197f314d 100644 --- a/test/form/samples/super-classes/super-class-prototype-calls/_expected.js +++ b/test/form/samples/super-classes/super-class-prototype-calls/_expected.js @@ -4,9 +4,7 @@ class SuperValues { effect(used) { console.log('effect', used); }, - isTrue() { - return true; - } + }; } effect(used) { @@ -18,6 +16,5 @@ class SuperValues { } 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 index 43c62658ce6..2a66d55579a 100644 --- a/test/form/samples/super-classes/super-class-prototype-calls/main.js +++ b/test/form/samples/super-classes/super-class-prototype-calls/main.js @@ -4,9 +4,7 @@ class SuperValues { effect(used) { console.log('effect', used); }, - isTrue() { - return true; - } + }; } effect(used) { @@ -19,7 +17,5 @@ class SuperValues { 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/_expected.js b/test/form/samples/super-classes/super-class-prototype-values/_expected.js index 60cdfd88e49..4002b3a03aa 100644 --- a/test/form/samples/super-classes/super-class-prototype-values/_expected.js +++ b/test/form/samples/super-classes/super-class-prototype-values/_expected.js @@ -1,5 +1,4 @@ console.log('retained'); -console.log('retained'); const prop = { isTrue: true }; class SuperDeopt { 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 index 1fce6f66d3d..12003a915de 100644 --- a/test/form/samples/super-classes/super-class-prototype-values/main.js +++ b/test/form/samples/super-classes/super-class-prototype-values/main.js @@ -9,8 +9,6 @@ class SuperValues { 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'); const prop = { isTrue: true }; class SuperDeopt { diff --git a/test/form/samples/this-in-class-body/_expected.js b/test/form/samples/this-in-class-body/_expected.js index ff7c88e7839..ffc2e565be4 100644 --- a/test/form/samples/this-in-class-body/_expected.js +++ b/test/form/samples/this-in-class-body/_expected.js @@ -1,8 +1,8 @@ class Used { - static flag = false + static flag = false; static mutate = () => { this.flag = true; - } + }; } Used.mutate(); @@ -10,23 +10,23 @@ if (Used.flag) console.log('retained'); else console.log('unimportant'); class InstanceMutation { - static flag = false - flag = false + static flag = false; + flag = false; mutate = () => { this.flag = true; - } + }; } -(new InstanceMutation).mutate(); +new InstanceMutation().mutate(); console.log('retained'); class UsedSuper { - static flag = false + static flag = false; } -class UsedWithSuper extends UsedSuper{ +class UsedWithSuper extends UsedSuper { static mutate = () => { super.flag = true; - } + }; } UsedWithSuper.mutate(); diff --git a/test/form/samples/this-in-class-body/main.js b/test/form/samples/this-in-class-body/main.js index 6fa4fc5ed65..a02119ea207 100644 --- a/test/form/samples/this-in-class-body/main.js +++ b/test/form/samples/this-in-class-body/main.js @@ -1,16 +1,16 @@ class Unused { - static flag = false + static flag = false; static mutate = () => { this.flag = true; - } + }; } Unused.mutate(); class Used { - static flag = false + static flag = false; static mutate = () => { this.flag = true; - } + }; } Used.mutate(); @@ -18,24 +18,24 @@ if (Used.flag) console.log('retained'); else console.log('unimportant'); class InstanceMutation { - static flag = false - flag = false + static flag = false; + flag = false; mutate = () => { this.flag = true; - } + }; } -(new InstanceMutation).mutate(); +new InstanceMutation().mutate(); if (InstanceMutation.flag) console.log('removed'); else console.log('retained'); class UsedSuper { - static flag = false + static flag = false; } -class UsedWithSuper extends UsedSuper{ +class UsedWithSuper extends UsedSuper { static mutate = () => { super.flag = true; - } + }; } UsedWithSuper.mutate(); diff --git a/test/form/samples/treeshake-excess-arguments/function-arguments/_expected.js b/test/form/samples/treeshake-excess-arguments/function-arguments/_expected.js index 6c216679275..2a7e9951bc2 100644 --- a/test/form/samples/treeshake-excess-arguments/function-arguments/_expected.js +++ b/test/form/samples/treeshake-excess-arguments/function-arguments/_expected.js @@ -1,37 +1,31 @@ function noParams() { console.log(); } + function someUsedParams(p1, p2, p3) { console.log(p1, p3); } - noParams(); - const needed21 = 1; const needed22 = 2; const needed23 = 3; - someUsedParams(needed21, needed22, needed23); const noParamsExp = function() { console.log(); }; + const someUsedParamsExp = function(p1, p2, p3) { console.log(p1, p3); }; - noParamsExp(); - const needed41 = 1; const needed42 = 2; const needed43 = 3; - someUsedParamsExp(needed41, needed42, needed43); - (function() { console.log(); }()); - const needed61 = 1; const needed62 = 2; const needed63 = 3; diff --git a/test/form/samples/treeshake-excess-arguments/function-arguments/main.js b/test/form/samples/treeshake-excess-arguments/function-arguments/main.js index a7d8a634f68..b0492cca08b 100644 --- a/test/form/samples/treeshake-excess-arguments/function-arguments/main.js +++ b/test/form/samples/treeshake-excess-arguments/function-arguments/main.js @@ -1,47 +1,44 @@ function noParams() { console.log(); } + function someUsedParams(p1, p2, p3) { console.log(p1, p3); } -const unneeded1 = 1; +const unneeded1 = 1; noParams(unneeded1); -const unneeded2 = 1; - +const unneeded21 = 1; +const unneeded22 = 1; const needed21 = 1; const needed22 = 2; const needed23 = 3; - -someUsedParams(needed21, needed22, needed23, unneeded2); +someUsedParams(needed21, needed22, needed23, unneeded21, unneeded22); const noParamsExp = function() { console.log(); }; + const someUsedParamsExp = function(p1, p2, p3) { console.log(p1, p3); }; -const unneeded3 = 1; +const unneeded3 = 1; noParamsExp(unneeded3); const unneeded4 = 1; - const needed41 = 1; const needed42 = 2; const needed43 = 3; - someUsedParamsExp(needed41, needed42, needed43, unneeded4); const unneeded5 = 1; - (function() { console.log(); }(unneeded5)); const unneeded6 = 1; - const needed61 = 1; const needed62 = 2; const needed63 = 3; diff --git a/test/form/samples/treeshake-excess-arguments/unused-parameters/_expected.js b/test/form/samples/treeshake-excess-arguments/unused-parameters/_expected.js index 256b223b12a..40313093cf8 100644 --- a/test/form/samples/treeshake-excess-arguments/unused-parameters/_expected.js +++ b/test/form/samples/treeshake-excess-arguments/unused-parameters/_expected.js @@ -1,11 +1,9 @@ -function test(a, b = globalThis.unknown(), c) {} -test(1, 2); +function test(a, b, c) {} function noEffect() {} test(1, 2, noEffect(), globalThis.unknown()); -const testArr = (a, b = globalThis.unknown(), c) => {}; -testArr(1, 2); +const testArr = (a, b, c) => {}; function noEffectArr() {} testArr(1, 2, noEffectArr(), globalThis.unknown()); diff --git a/test/function/samples/class-method-mutation/_config.js b/test/function/samples/class-method-mutation/_config.js new file mode 100644 index 00000000000..fcf070b8b11 --- /dev/null +++ b/test/function/samples/class-method-mutation/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks mutations of class methods' +}; diff --git a/test/function/samples/class-method-mutation/main.js b/test/function/samples/class-method-mutation/main.js new file mode 100644 index 00000000000..0d089acdd3e --- /dev/null +++ b/test/function/samples/class-method-mutation/main.js @@ -0,0 +1,14 @@ +let effect = false; + +class Foo { + method() {} +} + +const foo = new Foo(); +Object.defineProperty(foo.method, 'effect', { + get() { + effect = true; + } +}); + +Foo.prototype.method.effect; diff --git a/test/function/samples/class-method-mutations/_config.js b/test/function/samples/class-method-mutations/_config.js new file mode 100644 index 00000000000..73a6de81b35 --- /dev/null +++ b/test/function/samples/class-method-mutations/_config.js @@ -0,0 +1,11 @@ +const assert = require('assert'); + +module.exports = { + description: 'includes variable mutations in class methods if tree-shaking is disabled', + options: { + treeshake: false + }, + async exports({ promise }) { + assert.strictEqual(await promise, 'ok'); + } +}; diff --git a/test/function/samples/class-method-mutations/_expected.js b/test/function/samples/class-method-mutations/_expected.js new file mode 100644 index 00000000000..c387edcca09 --- /dev/null +++ b/test/function/samples/class-method-mutations/_expected.js @@ -0,0 +1,6 @@ +var a = () => { + console.log('props'); +}; + +a(); +a(); diff --git a/test/function/samples/class-method-mutations/main.js b/test/function/samples/class-method-mutations/main.js new file mode 100644 index 00000000000..4a72b001572 --- /dev/null +++ b/test/function/samples/class-method-mutations/main.js @@ -0,0 +1,11 @@ +class LookupService { + updateLookupById() { + return new Promise((resolve) => { + let result; + result = 'ok'; + resolve(result); + }); + } +} + +export const promise = new LookupService().updateLookupById(); \ No newline at end of file diff --git a/test/function/samples/class-method-returns/_config.js b/test/function/samples/class-method-returns/_config.js new file mode 100644 index 00000000000..8f20eb5fa6b --- /dev/null +++ b/test/function/samples/class-method-returns/_config.js @@ -0,0 +1,4 @@ +module.exports = { + description: 'deoptimizes return values of class methods', + minNodeVersion: 12 +}; diff --git a/test/function/samples/class-method-returns/main.js b/test/function/samples/class-method-returns/main.js new file mode 100644 index 00000000000..ce1fc1aa74f --- /dev/null +++ b/test/function/samples/class-method-returns/main.js @@ -0,0 +1,29 @@ +const a = { mutated: false }; +const b = { mutated: false }; +const c = { mutated: false }; +const d = { mutated: false }; + +class Foo { + static staticProp = () => a; + static staticMethod() { + return b; + } + prop = () => c; + method() { + return d; + } +} + +Foo.staticProp().mutated = true; +assert.ok(a.mutated ? true : false); + +Foo.staticMethod().mutated = true; +assert.ok(b.mutated ? true : false); + +const foo = new Foo(); + +foo.prop().mutated = true; +assert.ok(c.mutated ? true : false); + +foo.method().mutated = true; +assert.ok(d.mutated ? true : false); diff --git a/test/function/samples/parameter-defaults/exported/_config.js b/test/function/samples/parameter-defaults/exported/_config.js new file mode 100644 index 00000000000..e2b24f315cd --- /dev/null +++ b/test/function/samples/parameter-defaults/exported/_config.js @@ -0,0 +1,10 @@ +const assert = require('assert'); + +module.exports = { + description: 'includes default parameters for exported functions', + exports({ funDecl, funExp, arrow }) { + assert.strictEqual(funDecl(), 'defaultValue', 'function declaration'); + assert.strictEqual(funExp(), 'defaultValue', 'function expression'); + assert.strictEqual(arrow(), 'defaultValue', 'arrow function'); + } +}; diff --git a/test/function/samples/parameter-defaults/exported/main.js b/test/function/samples/parameter-defaults/exported/main.js new file mode 100644 index 00000000000..c40f173c19b --- /dev/null +++ b/test/function/samples/parameter-defaults/exported/main.js @@ -0,0 +1,9 @@ +export function funDecl(a = 'defaultValue') { + return a; +} + +export const funExp = function (a = 'defaultValue') { + return a; +}; + +export const arrow = (a = 'defaultValue') => a; diff --git a/test/function/samples/parameter-defaults/function-return/_config.js b/test/function/samples/parameter-defaults/function-return/_config.js new file mode 100644 index 00000000000..d9edae38255 --- /dev/null +++ b/test/function/samples/parameter-defaults/function-return/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks parameter defaults for returned functions' +}; diff --git a/test/function/samples/parameter-defaults/function-return/main.js b/test/function/samples/parameter-defaults/function-return/main.js new file mode 100644 index 00000000000..33187381725 --- /dev/null +++ b/test/function/samples/parameter-defaults/function-return/main.js @@ -0,0 +1,14 @@ +function test1() { + return function (a = 'fallback') { + return a; + }; +} + +assert.strictEqual(test1()(), 'fallback'); + +const test2 = + () => + (_, a = 'fallback') => + a; + +assert.strictEqual(test2````, 'fallback'); diff --git a/test/function/samples/parameter-defaults/logical-expressions/_config.js b/test/function/samples/parameter-defaults/logical-expressions/_config.js new file mode 100644 index 00000000000..69d69853719 --- /dev/null +++ b/test/function/samples/parameter-defaults/logical-expressions/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'includes default parameters in logical expressios' +}; diff --git a/test/function/samples/parameter-defaults/logical-expressions/main.js b/test/function/samples/parameter-defaults/logical-expressions/main.js new file mode 100644 index 00000000000..9557d0b7f87 --- /dev/null +++ b/test/function/samples/parameter-defaults/logical-expressions/main.js @@ -0,0 +1,2 @@ +const foo = ((a = 'fallback') => a) || false; +assert.strictEqual(foo(), 'fallback'); diff --git a/test/function/samples/parameter-defaults/module-side-effects/_config.js b/test/function/samples/parameter-defaults/module-side-effects/_config.js new file mode 100644 index 00000000000..39e073d8c7d --- /dev/null +++ b/test/function/samples/parameter-defaults/module-side-effects/_config.js @@ -0,0 +1,7 @@ +module.exports = { + description: + 'does not tree-shake necessary parameter defaults when modulesSideEffects are disabled', + options: { + treeshake: { moduleSideEffects: false } + } +}; diff --git a/test/function/samples/parameter-defaults/module-side-effects/main.js b/test/function/samples/parameter-defaults/module-side-effects/main.js new file mode 100644 index 00000000000..61838adaa51 --- /dev/null +++ b/test/function/samples/parameter-defaults/module-side-effects/main.js @@ -0,0 +1,3 @@ +import { foo } from './other'; + +assert.strictEqual(foo(), 'fallback'); diff --git a/test/function/samples/parameter-defaults/module-side-effects/other.js b/test/function/samples/parameter-defaults/module-side-effects/other.js new file mode 100644 index 00000000000..2bded0cd154 --- /dev/null +++ b/test/function/samples/parameter-defaults/module-side-effects/other.js @@ -0,0 +1 @@ +export const foo = (a = 'fallback') => a; diff --git a/test/function/samples/parameter-defaults/super-classes/_config.js b/test/function/samples/parameter-defaults/super-classes/_config.js new file mode 100644 index 00000000000..290793fd4de --- /dev/null +++ b/test/function/samples/parameter-defaults/super-classes/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'keeps parameter defaults for super classes' +}; diff --git a/test/function/samples/parameter-defaults/super-classes/main.js b/test/function/samples/parameter-defaults/super-classes/main.js new file mode 100644 index 00000000000..c83087dbe6b --- /dev/null +++ b/test/function/samples/parameter-defaults/super-classes/main.js @@ -0,0 +1,35 @@ +class A { + constructor(a = 'superConstructorDefault') { + this.a = a; + } + + static staticMethod(b = 'superStaticDefault') { + return b; + } + + method(c = 'superMethodDefault') { + return c; + } +} + +class B extends A { + constructor(a = 'constructorDefault') { + assert.strictEqual(a, 'constructorDefault'); + super(); + } + + static staticMethod(b = 'staticDefault') { + assert.strictEqual(b, 'staticDefault'); + return super.staticMethod(); + } + + method(c = 'methodDefault') { + assert.strictEqual(c, 'methodDefault'); + return super.method(); + } +} + +assert.strictEqual(B.staticMethod(), 'superStaticDefault'); +const b = new B(); +assert.strictEqual(b.a, 'superConstructorDefault'); +assert.strictEqual(b.method(), 'superMethodDefault'); diff --git a/test/function/samples/parameter-defaults/tagged-templates/_config.js b/test/function/samples/parameter-defaults/tagged-templates/_config.js new file mode 100644 index 00000000000..6e2f4a704b6 --- /dev/null +++ b/test/function/samples/parameter-defaults/tagged-templates/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'includes necessary default parameters for tagged template literals' +}; diff --git a/test/function/samples/parameter-defaults/tagged-templates/main.js b/test/function/samples/parameter-defaults/tagged-templates/main.js new file mode 100644 index 00000000000..3c0a5f8bd8c --- /dev/null +++ b/test/function/samples/parameter-defaults/tagged-templates/main.js @@ -0,0 +1,6 @@ +const templateTag = ([, a = 'quasiFallback'], b = 'expressionFallback') => { + assert.strictEqual(a, 'quasiFallback'); + assert.strictEqual(b, 'expressionFallback'); +}; + +templateTag``; diff --git a/test/function/samples/parameter-side-effects/_config.js b/test/function/samples/parameter-side-effects/_config.js new file mode 100644 index 00000000000..a2379c5f5c8 --- /dev/null +++ b/test/function/samples/parameter-side-effects/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'retains calls with parameter side effects' +}; diff --git a/test/function/samples/parameter-side-effects/main.js b/test/function/samples/parameter-side-effects/main.js new file mode 100644 index 00000000000..1ab1f23b307 --- /dev/null +++ b/test/function/samples/parameter-side-effects/main.js @@ -0,0 +1,12 @@ +let effect = false; + +function getPatternValueWithEffect() { + effect = true; + return 'value'; +} + +function test({ [getPatternValueWithEffect()]: value }) {} + +test({ value: 'foo' }); + +assert.ok(effect); diff --git a/test/function/samples/tagged-template-deoptimize/_config.js b/test/function/samples/tagged-template-deoptimize/_config.js new file mode 100644 index 00000000000..2f550cae185 --- /dev/null +++ b/test/function/samples/tagged-template-deoptimize/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'correctly deoptimizes tagged template expressions' +}; diff --git a/test/function/samples/tagged-template-deoptimize/_expected.js b/test/function/samples/tagged-template-deoptimize/_expected.js new file mode 100644 index 00000000000..c387edcca09 --- /dev/null +++ b/test/function/samples/tagged-template-deoptimize/_expected.js @@ -0,0 +1,6 @@ +var a = () => { + console.log('props'); +}; + +a(); +a(); diff --git a/test/function/samples/tagged-template-deoptimize/main.js b/test/function/samples/tagged-template-deoptimize/main.js new file mode 100644 index 00000000000..b564677dd8f --- /dev/null +++ b/test/function/samples/tagged-template-deoptimize/main.js @@ -0,0 +1,16 @@ +const tagReturn = 'return'; + +const param = { modified: false }; + +const obj = { + modified: false, + tag(_, param) { + this.modified = true; + param.modified = true; + return tagReturn; + } +}; + +assert.strictEqual(obj.tag`${param}`, 'return'); +assert.ok(obj.modified ? true : false, 'obj'); +assert.ok(param.modified ? true : false, 'param');