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..1a7994f8827 100644 --- a/src/ast/nodes/ArrayExpression.ts +++ b/src/ast/nodes/ArrayExpression.ts @@ -1,8 +1,14 @@ import type { CallOptions } from '../CallOptions'; import type { DeoptimizableEntity } from '../DeoptimizableEntity'; import type { HasEffectsContext } from '../ExecutionContext'; +import { InclusionContext } 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 +20,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 { @@ -56,7 +63,7 @@ export default class ArrayExpression extends NodeBase { ); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean | undefined { return this.getObjectEntity().hasEffectsWhenAccessedAtPath(path, context); } @@ -72,6 +79,29 @@ export default class ArrayExpression extends NodeBase { return this.getObjectEntity().hasEffectsWhenCalledAtPath(path, callOptions, context); } + includeArgumentsWhenCalledAtPath( + path: ObjectPath, + context: InclusionContext, + args: readonly (ExpressionEntity | SpreadElement)[] + ) { + this.getObjectEntity().includeArgumentsWhenCalledAtPath(path, context, args); + } + + 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 +112,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/AssignmentExpression.ts b/src/ast/nodes/AssignmentExpression.ts index b139d1ffe8f..1a9bcaeb417 100644 --- a/src/ast/nodes/AssignmentExpression.ts +++ b/src/ast/nodes/AssignmentExpression.ts @@ -54,7 +54,7 @@ export default class AssignmentExpression extends NodeBase { ); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean | undefined { return path.length > 0 && this.right.hasEffectsWhenAccessedAtPath(path, context); } diff --git a/src/ast/nodes/AssignmentPattern.ts b/src/ast/nodes/AssignmentPattern.ts index 21459c772a6..0d2e1907ec2 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,12 @@ export default class AssignmentPattern extends NodeBase implements PatternNode { return path.length > 0 || this.left.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context); } + 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 +52,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..0b7a8598a5d 100644 --- a/src/ast/nodes/BinaryExpression.ts +++ b/src/ast/nodes/BinaryExpression.ts @@ -60,10 +60,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; @@ -71,7 +71,7 @@ export default class BinaryExpression extends NodeBase implements DeoptimizableE return operatorFn(leftValue, rightValue); } - hasEffects(context: HasEffectsContext): boolean { + hasEffects(context: HasEffectsContext): boolean | undefined { // support some implicit type coercion runtime errors if ( this.operator === '+' && diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index 593146bf9f9..6a9e2966561 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) { @@ -241,7 +100,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt this.included = true; this.callee.include(context, false); } - this.callee.includeCallArguments(context, this.arguments); + this.callee.includeArgumentsWhenCalledAtPath(EMPTY_PATH, context, this.arguments); const returnExpression = this.getReturnExpression(); if (!returnExpression.included) { returnExpression.include(context, false); @@ -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..edef01a9095 100644 --- a/src/ast/nodes/ConditionalExpression.ts +++ b/src/ast/nodes/ConditionalExpression.ts @@ -108,7 +108,7 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz ); } - hasEffects(context: HasEffectsContext): boolean { + hasEffects(context: HasEffectsContext): boolean | undefined { if (this.test.hasEffects(context)) return true; const usedBranch = this.getUsedBranch(); if (usedBranch === null) { @@ -117,7 +117,7 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz return usedBranch.hasEffects(context); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean | undefined { const usedBranch = this.getUsedBranch(); if (usedBranch === null) { return ( @@ -166,16 +166,17 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz } } - includeCallArguments( + includeArgumentsWhenCalledAtPath( + path: ObjectPath, context: InclusionContext, - args: readonly (ExpressionNode | SpreadElement)[] + args: readonly (ExpressionEntity | SpreadElement)[] ): void { const usedBranch = this.getUsedBranch(); if (usedBranch === null) { - this.consequent.includeCallArguments(context, args); - this.alternate.includeCallArguments(context, args); + this.consequent.includeArgumentsWhenCalledAtPath(path, context, args); + this.alternate.includeArgumentsWhenCalledAtPath(path, context, args); } else { - usedBranch.includeCallArguments(context, args); + usedBranch.includeArgumentsWhenCalledAtPath(path, context, args); } } @@ -225,7 +226,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..2accca3bba3 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); + hasEffects(context: HasEffectsContext): boolean | undefined { + return this.declaration?.hasEffects(context); } initialise(): void { diff --git a/src/ast/nodes/ExpressionStatement.ts b/src/ast/nodes/ExpressionStatement.ts index f5c141c84d2..5171d5f19a0 100644 --- a/src/ast/nodes/ExpressionStatement.ts +++ b/src/ast/nodes/ExpressionStatement.ts @@ -30,7 +30,7 @@ export default class ExpressionStatement extends StatementBase { if (this.included) this.insertSemicolon(code); } - shouldBeIncluded(context: InclusionContext): boolean { + shouldBeIncluded(context: InclusionContext): boolean | undefined { if (this.directive && this.directive !== 'use strict') return this.parent.type !== NodeType.Program; 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/Identifier.ts b/src/ast/nodes/Identifier.ts index 89965f79287..cdf4c21d173 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, @@ -137,21 +137,14 @@ export default class Identifier extends NodeBase implements PatternNode { ); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return ( - this.variable !== null && - this.getVariableRespectingTDZ().hasEffectsWhenAccessedAtPath(path, context) - ); + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean | undefined { + return 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 +152,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 { @@ -175,11 +165,12 @@ export default class Identifier extends NodeBase implements PatternNode { } } - includeCallArguments( + includeArgumentsWhenCalledAtPath( + path: ObjectPath, context: InclusionContext, - args: readonly (ExpressionNode | SpreadElement)[] + args: readonly (ExpressionEntity | SpreadElement)[] ): void { - this.getVariableRespectingTDZ().includeCallArguments(context, args); + this.variable!.includeArgumentsWhenCalledAtPath(path, context, args); } isPossibleTDZ(): boolean { @@ -250,7 +241,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 +257,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..bfc9999ecab 100644 --- a/src/ast/nodes/IfStatement.ts +++ b/src/ast/nodes/IfStatement.ts @@ -36,12 +36,12 @@ export default class IfStatement extends StatementBase implements DeoptimizableE this.testValue = UnknownValue; } - hasEffects(context: HasEffectsContext): boolean { + hasEffects(context: HasEffectsContext): boolean | undefined { if (this.test.hasEffects(context)) { 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..cc9ccca0150 100644 --- a/src/ast/nodes/LogicalExpression.ts +++ b/src/ast/nodes/LogicalExpression.ts @@ -104,7 +104,7 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable ); } - hasEffects(context: HasEffectsContext): boolean { + hasEffects(context: HasEffectsContext): boolean | undefined { if (this.left.hasEffects(context)) { return true; } @@ -114,7 +114,7 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable return false; } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean | undefined { const usedBranch = this.getUsedBranch(); if (usedBranch === null) { return ( @@ -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..c8f6a965e30 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -211,7 +211,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE return UNKNOWN_EXPRESSION; } - hasEffects(context: HasEffectsContext): boolean { + hasEffects(context: HasEffectsContext): boolean | undefined { if (!this.deoptimized) this.applyDeoptimizations(); const { propertyReadSideEffects } = this.context.options .treeshake as NormalizedTreeshakingOptions; @@ -230,7 +230,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE ); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean | undefined { if (this.variable !== null) { return this.variable.hasEffectsWhenAccessedAtPath(path, context); } @@ -289,14 +289,17 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE this.property.include(context, includeChildrenRecursively); } - includeCallArguments( + includeArgumentsWhenCalledAtPath( + path: ObjectPath, context: InclusionContext, - args: readonly (ExpressionNode | SpreadElement)[] + args: readonly (ExpressionEntity | SpreadElement)[] ): void { if (this.variable) { - this.variable.includeCallArguments(context, args); - } else { - super.includeCallArguments(context, args); + this.variable.includeArgumentsWhenCalledAtPath(path, context, args); + } else if (this.replacement) { + super.includeArgumentsWhenCalledAtPath(path, context, args); + } else if (path.length < MAX_PATH_DEPTH) { + this.object.includeArgumentsWhenCalledAtPath([this.getPropertyKey(), ...path], context, args); } } @@ -385,7 +388,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..4c8c0a6c584 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,35 @@ 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(); + this.included = true; + this.callee.include(context, includeChildrenRecursively); + this.callee.includeArgumentsWhenCalledAtPath(EMPTY_PATH, 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..acb84035fee 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -3,7 +3,7 @@ import { BLANK } from '../../utils/blank'; import type { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import type { CallOptions } from '../CallOptions'; import type { DeoptimizableEntity } from '../DeoptimizableEntity'; -import type { HasEffectsContext } from '../ExecutionContext'; +import type { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import type { NodeEvent } from '../NodeEvents'; import { EMPTY_PATH, @@ -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'; @@ -75,7 +71,7 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE ); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean | undefined { return this.getObjectEntity().hasEffectsWhenAccessedAtPath(path, context); } @@ -91,6 +87,14 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE return this.getObjectEntity().hasEffectsWhenCalledAtPath(path, callOptions, context); } + includeArgumentsWhenCalledAtPath( + path: ObjectPath, + context: InclusionContext, + args: readonly (ExpressionEntity | SpreadElement)[] + ) { + this.getObjectEntity().includeArgumentsWhenCalledAtPath(path, context, args); + } + render( code: MagicString, options: RenderOptions, @@ -124,7 +128,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/Property.ts b/src/ast/nodes/Property.ts index c55fac6e834..1695c4a3409 100644 --- a/src/ast/nodes/Property.ts +++ b/src/ast/nodes/Property.ts @@ -24,7 +24,7 @@ export default class Property extends MethodBase implements PatternNode { return (this.value as PatternNode).declare(kind, UNKNOWN_EXPRESSION); } - hasEffects(context: HasEffectsContext): boolean { + hasEffects(context: HasEffectsContext): boolean | undefined { if (!this.deoptimized) this.applyDeoptimizations(); const propertyReadSideEffects = (this.context.options.treeshake as NormalizedTreeshakingOptions) .propertyReadSideEffects; diff --git a/src/ast/nodes/PropertyDefinition.ts b/src/ast/nodes/PropertyDefinition.ts index 36183b9872a..6c212c67760 100644 --- a/src/ast/nodes/PropertyDefinition.ts +++ b/src/ast/nodes/PropertyDefinition.ts @@ -1,10 +1,11 @@ import type { CallOptions } from '../CallOptions'; import type { DeoptimizableEntity } from '../DeoptimizableEntity'; -import type { HasEffectsContext } from '../ExecutionContext'; +import type { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import type { NodeEvent } from '../NodeEvents'; import type { ObjectPath, PathTracker } from '../utils/PathTracker'; import type * as NodeType from './NodeType'; import type PrivateIdentifier from './PrivateIdentifier'; +import SpreadElement from './SpreadElement'; import { type ExpressionEntity, type LiteralValueOrUnknown, @@ -54,14 +55,11 @@ export default class PropertyDefinition extends NodeBase { : UNKNOWN_EXPRESSION; } - hasEffects(context: HasEffectsContext): boolean { - return ( - this.key.hasEffects(context) || - (this.static && this.value !== null && this.value.hasEffects(context)) - ); + hasEffects(context: HasEffectsContext): boolean | undefined { + return this.key.hasEffects(context) || (this.static && this.value?.hasEffects(context)); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean | undefined { return !this.value || this.value.hasEffectsWhenAccessedAtPath(path, context); } @@ -76,4 +74,12 @@ export default class PropertyDefinition extends NodeBase { ): boolean { return !this.value || this.value.hasEffectsWhenCalledAtPath(path, callOptions, context); } + + includeArgumentsWhenCalledAtPath( + path: ObjectPath, + context: InclusionContext, + args: readonly (ExpressionEntity | SpreadElement)[] + ) { + this.value?.includeArgumentsWhenCalledAtPath(path, context, args); + } } 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/SequenceExpression.ts b/src/ast/nodes/SequenceExpression.ts index 1eaca85ad4b..51031e632ff 100644 --- a/src/ast/nodes/SequenceExpression.ts +++ b/src/ast/nodes/SequenceExpression.ts @@ -58,7 +58,7 @@ export default class SequenceExpression extends NodeBase { return false; } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean | undefined { return ( path.length > 0 && this.expressions[this.expressions.length - 1].hasEffectsWhenAccessedAtPath(path, context) diff --git a/src/ast/nodes/SpreadElement.ts b/src/ast/nodes/SpreadElement.ts index 95145d9c6fd..7e0f38e3872 100644 --- a/src/ast/nodes/SpreadElement.ts +++ b/src/ast/nodes/SpreadElement.ts @@ -27,7 +27,7 @@ export default class SpreadElement extends NodeBase { } } - hasEffects(context: HasEffectsContext): boolean { + hasEffects(context: HasEffectsContext): boolean | undefined { if (!this.deoptimized) this.applyDeoptimizations(); const { propertyReadSideEffects } = this.context.options .treeshake as NormalizedTreeshakingOptions; diff --git a/src/ast/nodes/Super.ts b/src/ast/nodes/Super.ts index f598cbff0b1..b7a2d35e733 100644 --- a/src/ast/nodes/Super.ts +++ b/src/ast/nodes/Super.ts @@ -1,19 +1,30 @@ +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) { 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..140f1899e0a 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,36 @@ 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(); + this.included = true; + this.tag.include(context, includeChildrenRecursively); + this.quasi.include(context, includeChildrenRecursively); + this.tag.includeArgumentsWhenCalledAtPath(EMPTY_PATH, 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 +79,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..ecfc3dd0012 100644 --- a/src/ast/nodes/TryStatement.ts +++ b/src/ast/nodes/TryStatement.ts @@ -19,7 +19,8 @@ export default class TryStatement extends StatementBase { ((this.context.options.treeshake as NormalizedTreeshakingOptions).tryCatchDeoptimization ? this.block.body.length > 0 : this.block.hasEffects(context)) || - (this.finalizer !== null && this.finalizer.hasEffects(context)) + this.finalizer?.hasEffects(context) || + false ); } @@ -47,8 +48,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..32b5c850a33 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); } } diff --git a/src/ast/nodes/VariableDeclarator.ts b/src/ast/nodes/VariableDeclarator.ts index a6499ef4b45..7b67948fcd6 100644 --- a/src/ast/nodes/VariableDeclarator.ts +++ b/src/ast/nodes/VariableDeclarator.ts @@ -27,17 +27,15 @@ export default class VariableDeclarator extends NodeBase { this.id.deoptimizePath(path); } - hasEffects(context: HasEffectsContext): boolean { - const initEffect = this.init !== null && this.init.hasEffects(context); + hasEffects(context: HasEffectsContext): boolean | undefined { + 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); 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..8739cac039f 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'; @@ -11,11 +10,9 @@ export default class YieldExpression extends NodeBase { declare type: NodeType.tYieldExpression; protected deoptimized = false; - hasEffects(context: HasEffectsContext): boolean { + hasEffects(context: HasEffectsContext): boolean | undefined { 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..24ac09556ac --- /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 | undefined { + 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..bf7d827956b 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -9,13 +9,15 @@ 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 SpreadElement from '../SpreadElement'; +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 +27,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 +41,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( @@ -76,13 +85,14 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { ); } - hasEffects(context: HasEffectsContext): boolean { + hasEffects(context: HasEffectsContext): boolean | undefined { + if (!this.deoptimized) this.applyDeoptimizations(); const initEffect = this.superClass?.hasEffects(context) || this.body.hasEffects(context); this.id?.markDeclarationReached(); return initEffect || super.hasEffects(context); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean | undefined { return this.getObjectEntity().hasEffectsWhenAccessedAtPath(path, context); } @@ -100,8 +110,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 +119,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); @@ -118,6 +129,22 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { } } + includeArgumentsWhenCalledAtPath( + path: ObjectPath, + context: InclusionContext, + args: readonly (ExpressionEntity | SpreadElement)[] + ): void { + if (path.length === 0) { + if (this.classConstructor) { + this.classConstructor.includeArgumentsWhenCalledAtPath(path, context, args); + } else { + this.superClass?.includeArgumentsWhenCalledAtPath(path, context, args); + } + } else { + this.getObjectEntity().includeArgumentsWhenCalledAtPath(path, context, args); + } + } + initialise(): void { this.id?.declare('class', this); for (const method of this.body.body) { @@ -129,6 +156,23 @@ 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 parameter defaults are + // included and the return value is deoptimized + definition.deoptimizePath(UNKNOWN_PATH); + } + } + this.context.requestTreeshakingPass(); + } + private getObjectEntity(): ObjectEntity { if (this.objectEntity !== null) { return this.objectEntity; @@ -148,7 +192,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..a484267e67c 100644 --- a/src/ast/nodes/shared/Expression.ts +++ b/src/ast/nodes/shared/Expression.ts @@ -6,11 +6,19 @@ 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; +} export class ExpressionEntity implements WritableEntity { included = false; @@ -48,7 +56,10 @@ export class ExpressionEntity implements WritableEntity { return UNKNOWN_EXPRESSION; } - hasEffectsWhenAccessedAtPath(_path: ObjectPath, _context: HasEffectsContext): boolean { + hasEffectsWhenAccessedAtPath( + _path: ObjectPath, + _context: HasEffectsContext + ): boolean | undefined { return true; } @@ -64,18 +75,27 @@ 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( + includeArgumentsWhenCalledAtPath( + _path: ObjectPath, context: InclusionContext, - args: readonly (ExpressionNode | SpreadElement)[] + args: readonly (ExpressionEntity | SpreadElement)[] ): void { for (const arg of args) { arg.include(context, false); } } + + shouldBeIncluded(_context: InclusionContext): boolean | undefined { + 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..b2d4944362d 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -8,12 +8,26 @@ 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, + LiteralValueOrUnknown, + UNKNOWN_EXPRESSION, + UnknownValue +} from './Expression'; import { type ExpressionNode, type GenericEsTreeNode, @@ -23,20 +37,28 @@ 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[]; declare preventChildBlockScope: true; declare scope: ReturnValueScope; + // By default, parameters are included via includeArgumentsWhenCalledAtPath + protected alwaysIncludeParameters = false; protected objectEntity: ObjectEntity | null = null; private deoptimizedReturn = false; + private declare parameterVariables: LocalVariable[][]; + + deoptimizeCache() { + this.alwaysIncludeParameters = 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.alwaysIncludeParameters = true; this.scope.getReturnExpression().deoptimizePath(UNKNOWN_PATH); } } @@ -90,7 +112,7 @@ export default abstract class FunctionBase extends NodeBase { return this.scope.getReturnExpression(); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean | undefined { return this.getObjectEntity().hasEffectsWhenAccessedAtPath(path, context); } @@ -135,18 +157,55 @@ export default abstract class FunctionBase extends NodeBase { context.brokenFlow = BROKEN_FLOW_NONE; this.body.include(context, includeChildrenRecursively); context.brokenFlow = brokenFlow; + if (includeChildrenRecursively || this.alwaysIncludeParameters) { + for (const param of this.params) { + param.include(context, includeChildrenRecursively); + } + } } - includeCallArguments( + includeArgumentsWhenCalledAtPath( + path: ObjectPath, context: InclusionContext, - args: readonly (ExpressionNode | SpreadElement)[] + args: readonly (ExpressionEntity | SpreadElement)[] ): void { - this.scope.includeCallArguments(context, args); + if (path.length === 0) { + 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); + } else { + this.getObjectEntity().includeArgumentsWhenCalledAtPath(path, 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..c54360949eb 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -4,7 +4,7 @@ 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 IdentifierWithVariable } from '../Identifier'; import { type ExpressionEntity, UNKNOWN_EXPRESSION } from './Expression'; import FunctionBase from './FunctionBase'; import { type IncludeChildren } from './Node'; @@ -37,8 +37,8 @@ export default class FunctionNode extends FunctionBase { } } - hasEffects(): boolean { - return this.id !== null && this.id.hasEffects(); + hasEffects(): boolean | undefined { + return this.id?.hasEffects(); } hasEffectsWhenCalledAtPath( @@ -73,14 +73,12 @@ export default class FunctionNode extends FunctionBase { } 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); - } + // This ensures that super.include will also include all parameters + if (this.scope.argumentsVariable.included) { + this.alwaysIncludeParameters = true; } + this.id?.include(); + super.include(context, includeChildrenRecursively); } initialise(): void { diff --git a/src/ast/nodes/shared/MethodBase.ts b/src/ast/nodes/shared/MethodBase.ts index 8ad74d66d13..93d756d094c 100644 --- a/src/ast/nodes/shared/MethodBase.ts +++ b/src/ast/nodes/shared/MethodBase.ts @@ -1,6 +1,6 @@ import { type CallOptions, NO_ARGS } from '../../CallOptions'; import type { DeoptimizableEntity } from '../../DeoptimizableEntity'; -import type { HasEffectsContext } from '../../ExecutionContext'; +import type { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; import { EVENT_ACCESSED, EVENT_ASSIGNED, EVENT_CALLED, type NodeEvent } from '../../NodeEvents'; import { EMPTY_PATH, @@ -9,6 +9,7 @@ import { SHARED_RECURSION_TRACKER } from '../../utils/PathTracker'; import type PrivateIdentifier from '../PrivateIdentifier'; +import SpreadElement from '../SpreadElement'; import { type ExpressionEntity, type LiteralValueOrUnknown, @@ -90,11 +91,11 @@ export default class MethodBase extends NodeBase implements DeoptimizableEntity ); } - hasEffects(context: HasEffectsContext): boolean { + hasEffects(context: HasEffectsContext): boolean | undefined { return this.key.hasEffects(context); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean | undefined { if (this.kind === 'get' && path.length === 0) { return this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context); } @@ -116,6 +117,14 @@ export default class MethodBase extends NodeBase implements DeoptimizableEntity return this.getAccessedValue().hasEffectsWhenCalledAtPath(path, callOptions, context); } + includeArgumentsWhenCalledAtPath( + path: ObjectPath, + context: InclusionContext, + args: readonly (ExpressionEntity | SpreadElement)[] + ): void { + this.getAccessedValue().includeArgumentsWhenCalledAtPath(path, context, args); + } + protected getAccessedValue(): ExpressionEntity { if (this.accessedValue === null) { if (this.kind === 'get') { diff --git a/src/ast/nodes/shared/MethodTypes.ts b/src/ast/nodes/shared/MethodTypes.ts index 78c061d5d18..1d10526fdf9 100644 --- a/src/ast/nodes/shared/MethodTypes.ts +++ b/src/ast/nodes/shared/MethodTypes.ts @@ -9,7 +9,6 @@ import { } from '../../values'; import type SpreadElement from '../SpreadElement'; import { ExpressionEntity, UNKNOWN_EXPRESSION } from './Expression'; -import type { ExpressionNode } from './Node'; type MethodDescription = { callsArgs: number[] | null; @@ -96,9 +95,10 @@ export class Method extends ExpressionEntity { return false; } - includeCallArguments( + includeArgumentsWhenCalledAtPath( + _path: ObjectPath, context: InclusionContext, - args: readonly (ExpressionNode | SpreadElement)[] + args: readonly (ExpressionEntity | SpreadElement)[] ): void { for (const arg of args) { arg.include(context, false); diff --git a/src/ast/nodes/shared/Node.ts b/src/ast/nodes/shared/Node.ts index 656c4ef1c1f..7746099dfd9 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; @@ -53,22 +54,17 @@ export interface Node extends Entity { * which only have an effect if their surrounding loop or switch statement is included. * The options pass on information like this about the current execution path. */ - hasEffects(context: HasEffectsContext): boolean; + hasEffects(context: HasEffectsContext): boolean | undefined; /** * Includes the node in the bundle. If the flag is not set, children are usually included * 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; @@ -79,7 +75,7 @@ export interface Node extends Entity { * visits as the inclusion of additional variables may require the inclusion of more child * nodes in e.g. block statements. */ - shouldBeIncluded(context: InclusionContext): boolean; + shouldBeIncluded(context: InclusionContext): boolean | undefined; } export type StatementNode = Node; @@ -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(); @@ -149,21 +145,25 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { this.scope = parentScope; } - hasEffects(context: HasEffectsContext): boolean { + hasEffects(context: HasEffectsContext): boolean | undefined { if (this.deoptimized === false) this.applyDeoptimizations(); 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) { - 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); @@ -243,18 +236,39 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { } } - shouldBeIncluded(context: InclusionContext): boolean { + shouldBeIncluded(context: InclusionContext): boolean | undefined { 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..475a7ad936b 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -1,6 +1,6 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; -import { HasEffectsContext } from '../../ExecutionContext'; +import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; import { EVENT_ACCESSED, EVENT_CALLED, NodeEvent } from '../../NodeEvents'; import { ObjectPath, @@ -12,10 +12,12 @@ import { UnknownKey, UnknownNonAccessorKey } from '../../utils/PathTracker'; +import SpreadElement from '../SpreadElement'; import { ExpressionEntity, LiteralValueOrUnknown, UNKNOWN_EXPRESSION, + UnknownTruthyValue, UnknownValue } from './Expression'; @@ -225,7 +227,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); @@ -271,7 +273,7 @@ export class ObjectEntity extends ExpressionEntity { return UNKNOWN_EXPRESSION; } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean | undefined { const [key, ...subPath] = path; if (path.length > 1) { if (typeof key !== 'string') { @@ -378,6 +380,21 @@ export class ObjectEntity extends ExpressionEntity { return true; } + includeArgumentsWhenCalledAtPath( + path: ObjectPath, + context: InclusionContext, + args: readonly (ExpressionEntity | SpreadElement)[] + ) { + const key = path[0]; + const expressionAtPath = this.getMemberExpression(key); + if (expressionAtPath) { + return expressionAtPath.includeArgumentsWhenCalledAtPath(path.slice(1), context, args); + } + if (this.prototypeExpression) { + return this.prototypeExpression.includeArgumentsWhenCalledAtPath(path, context, args); + } + } + private buildPropertyMaps(properties: readonly ObjectProperty[]): void { const { allProperties, diff --git a/src/ast/nodes/shared/ObjectMember.ts b/src/ast/nodes/shared/ObjectMember.ts index ac6871eb369..055192ba8b5 100644 --- a/src/ast/nodes/shared/ObjectMember.ts +++ b/src/ast/nodes/shared/ObjectMember.ts @@ -50,8 +50,7 @@ export class ObjectMember extends ExpressionEntity { ); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return false; + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean | undefined { return this.object.hasEffectsWhenAccessedAtPath([this.key, ...path], context); } 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..11a6b70d154 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'; @@ -190,9 +189,10 @@ export default class LocalVariable extends Variable { } } - includeCallArguments( + includeArgumentsWhenCalledAtPath( + path: ObjectPath, 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) { @@ -200,7 +200,7 @@ export default class LocalVariable extends Variable { } } else if (this.init) { context.includedCallArguments.add(this.init); - this.init.includeCallArguments(context, args); + this.init.includeArgumentsWhenCalledAtPath(path, context, args); context.includedCallArguments.delete(this.init); } } diff --git a/test/form/samples/side-effect-e/_expected/es.js b/test/form/samples/side-effect-e/_expected.js similarity index 52% rename from test/form/samples/side-effect-e/_expected/es.js rename to test/form/samples/side-effect-e/_expected.js index 3067a16c289..08730030302 100644 --- a/test/form/samples/side-effect-e/_expected/es.js +++ b/test/form/samples/side-effect-e/_expected.js @@ -4,13 +4,7 @@ function foo () { console.log( 'side-effect' ); } }; - - var obj = { foo: 1, bar: 2 }; - Object.keys( obj ); + Object.keys(); } 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/tree-shake-default-parameters/array-elements/_config.js b/test/form/samples/tree-shake-default-parameters/array-elements/_config.js new file mode 100644 index 00000000000..514bd9b092f --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/array-elements/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'supports tree-shaking for unused default parameter values on arrays' +}; diff --git a/test/form/samples/tree-shake-default-parameters/array-elements/_expected.js b/test/form/samples/tree-shake-default-parameters/array-elements/_expected.js new file mode 100644 index 00000000000..6bf23b83290 --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/array-elements/_expected.js @@ -0,0 +1,16 @@ +var isUndefined; + +const test2 = [ + (a = 'retained') => console.log(a) +]; + +const test = [ + (a = 'retained', b = 'retained', c, d) => console.log(a, b, c), + ...test2, + (a = 'retained') => console.log(a) +]; + +test[0](isUndefined, 'b', 'c'); +test[0]('a', globalThis.unknown, 'c'); +test[1](); +test[2](); diff --git a/test/form/samples/tree-shake-default-parameters/array-elements/main.js b/test/form/samples/tree-shake-default-parameters/array-elements/main.js new file mode 100644 index 00000000000..668a44988e5 --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/array-elements/main.js @@ -0,0 +1,16 @@ +var isUndefined; + +const test2 = [ + (a = 'retained') => console.log(a) +]; + +const test = [ + (a = 'retained', b = 'retained', c = 'removed', d = 'removed') => console.log(a, b, c), + ...test2, + (a = 'retained') => console.log(a) +]; + +test[0](isUndefined, 'b', 'c'); +test[0]('a', globalThis.unknown, 'c'); +test[1](); +test[2](); diff --git a/test/form/samples/tree-shake-default-parameters/class-methods/_config.js b/test/form/samples/tree-shake-default-parameters/class-methods/_config.js new file mode 100644 index 00000000000..39978209649 --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/class-methods/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'supports tree-shaking for unused default parameter values on classes' +}; diff --git a/test/form/samples/tree-shake-default-parameters/class-methods/_expected.js b/test/form/samples/tree-shake-default-parameters/class-methods/_expected.js new file mode 100644 index 00000000000..269a1d5ae08 --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/class-methods/_expected.js @@ -0,0 +1,29 @@ +var isUndefined; + +class Test { + constructor(a = 'retained', b = 'retained', c, d) { + console.log(a, b, c); + } + + method(a = 'retained') { + console.log(a); + } + + prop = (a = 'retained') => console.log(a); + + static staticMethod(a = 'retained', b = 'retained', c, d) { + console.log(a, b, c); + } + + static staticProp = (a = 'retained', b = 'retained', c, d) => + console.log(a, b, c); +} + +new Test(isUndefined, 'b', 'c'); +new Test('a', globalThis.unknown, 'c').method(); + +Test.staticMethod(isUndefined, 'b', 'c'); +Test.staticMethod('a', globalThis.unknown, 'c'); + +Test.staticProp(isUndefined, 'b', 'c'); +Test.staticProp('a', globalThis.unknown, 'c'); diff --git a/test/form/samples/tree-shake-default-parameters/class-methods/main.js b/test/form/samples/tree-shake-default-parameters/class-methods/main.js new file mode 100644 index 00000000000..64101ab49df --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/class-methods/main.js @@ -0,0 +1,29 @@ +var isUndefined; + +class Test { + constructor(a = 'retained', b = 'retained', c = 'removed', d = 'removed') { + console.log(a, b, c); + } + + method(a = 'retained') { + console.log(a); + } + + prop = (a = 'retained') => console.log(a); + + static staticMethod(a = 'retained', b = 'retained', c = 'removed', d = 'removed') { + console.log(a, b, c); + } + + static staticProp = (a = 'retained', b = 'retained', c = 'removed', d = 'removed') => + console.log(a, b, c); +} + +new Test(isUndefined, 'b', 'c'); +new Test('a', globalThis.unknown, 'c').method(); + +Test.staticMethod(isUndefined, 'b', 'c'); +Test.staticMethod('a', globalThis.unknown, 'c'); + +Test.staticProp(isUndefined, 'b', 'c'); +Test.staticProp('a', globalThis.unknown, 'c'); diff --git a/test/form/samples/tree-shake-default-parameters/default-parameter-side-effects/_config.js b/test/form/samples/tree-shake-default-parameters/default-parameter-side-effects/_config.js new file mode 100644 index 00000000000..7a232d2ace8 --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/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/tree-shake-default-parameters/default-parameter-side-effects/_expected.js b/test/form/samples/tree-shake-default-parameters/default-parameter-side-effects/_expected.js new file mode 100644 index 00000000000..6eccd926254 --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/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) => {}; +e(); + +const f = ({ x = console.log('retained') }) => {}; +f('value'); diff --git a/test/form/samples/tree-shake-default-parameters/default-parameter-side-effects/main.js b/test/form/samples/tree-shake-default-parameters/default-parameter-side-effects/main.js new file mode 100644 index 00000000000..25501dd5f0a --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/default-parameter-side-effects/main.js @@ -0,0 +1,17 @@ +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')) => {}; +e('value'); + +const f = ({ x = console.log('retained') } = {}) => {}; +f('value'); diff --git a/test/form/samples/tree-shake-default-parameters/functions/_config.js b/test/form/samples/tree-shake-default-parameters/functions/_config.js new file mode 100644 index 00000000000..0faee52a9f7 --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/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/tree-shake-default-parameters/functions/_expected.js b/test/form/samples/tree-shake-default-parameters/functions/_expected.js new file mode 100644 index 00000000000..945aec4a8bc --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/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/tree-shake-default-parameters/functions/main.js b/test/form/samples/tree-shake-default-parameters/functions/main.js new file mode 100644 index 00000000000..048cf4dbae2 --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/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/tree-shake-default-parameters/non-literal-arguments/_config.js b/test/form/samples/tree-shake-default-parameters/non-literal-arguments/_config.js new file mode 100644 index 00000000000..341af06cb2d --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/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/tree-shake-default-parameters/non-literal-arguments/_expected.js b/test/form/samples/tree-shake-default-parameters/non-literal-arguments/_expected.js new file mode 100644 index 00000000000..3ac927db486 --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/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/tree-shake-default-parameters/non-literal-arguments/main.js b/test/form/samples/tree-shake-default-parameters/non-literal-arguments/main.js new file mode 100644 index 00000000000..6f3421429ae --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/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/tree-shake-default-parameters/object-methods/_config.js b/test/form/samples/tree-shake-default-parameters/object-methods/_config.js new file mode 100644 index 00000000000..879fa5d672b --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/object-methods/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'supports tree-shaking for unused default parameter values on objects' +}; diff --git a/test/form/samples/tree-shake-default-parameters/object-methods/_expected.js b/test/form/samples/tree-shake-default-parameters/object-methods/_expected.js new file mode 100644 index 00000000000..e3164086966 --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/object-methods/_expected.js @@ -0,0 +1,10 @@ +var isUndefined; + +const test = { + method(a = 'retained', b = 'retained', c, d) { + console.log(a, b, c); + } +}; + +test.method(isUndefined, 'b', 'c'); +test.method('a', globalThis.unknown, 'c'); diff --git a/test/form/samples/tree-shake-default-parameters/object-methods/main.js b/test/form/samples/tree-shake-default-parameters/object-methods/main.js new file mode 100644 index 00000000000..6a7d812c145 --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/object-methods/main.js @@ -0,0 +1,10 @@ +var isUndefined; + +const test = { + method(a = 'retained', b = 'retained', c = 'removed', d = 'removed') { + console.log(a, b, c); + } +}; + +test.method(isUndefined, 'b', 'c'); +test.method('a', globalThis.unknown, 'c'); diff --git a/test/form/samples/tree-shake-default-parameters/super-class-methods/_config.js b/test/form/samples/tree-shake-default-parameters/super-class-methods/_config.js new file mode 100644 index 00000000000..6dbe1e96dd5 --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/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/tree-shake-default-parameters/super-class-methods/_expected.js b/test/form/samples/tree-shake-default-parameters/super-class-methods/_expected.js new file mode 100644 index 00000000000..3cbf6cdad4e --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/super-class-methods/_expected.js @@ -0,0 +1,25 @@ +var isUndefined; + +class TestSuper { + constructor(a = 'retained', b = 'retained', c, d) { + console.log(a, b, c); + } + + static staticMethod(a = 'retained', b = 'retained', c, d) { + console.log(a, b, c); + } + + static staticProp = (a = 'retained', b = 'retained', c, d) => + console.log(a, b, c); +} + +class Test extends TestSuper {} + +new Test(isUndefined, 'b', 'c'); +new Test('a', globalThis.unknown, 'c').method(); + +Test.staticMethod(isUndefined, 'b', 'c'); +Test.staticMethod('a', globalThis.unknown, 'c'); + +Test.staticProp(isUndefined, 'b', 'c'); +Test.staticProp('a', globalThis.unknown, 'c'); diff --git a/test/form/samples/tree-shake-default-parameters/super-class-methods/main.js b/test/form/samples/tree-shake-default-parameters/super-class-methods/main.js new file mode 100644 index 00000000000..7cb2dacc9a5 --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/super-class-methods/main.js @@ -0,0 +1,25 @@ +var isUndefined; + +class TestSuper { + constructor(a = 'retained', b = 'retained', c = 'removed', d = 'removed') { + console.log(a, b, c); + } + + static staticMethod(a = 'retained', b = 'retained', c = 'removed', d = 'removed') { + console.log(a, b, c); + } + + static staticProp = (a = 'retained', b = 'retained', c = 'removed', d = 'removed') => + console.log(a, b, c); +} + +class Test extends TestSuper {} + +new Test(isUndefined, 'b', 'c'); +new Test('a', globalThis.unknown, 'c').method(); + +Test.staticMethod(isUndefined, 'b', 'c'); +Test.staticMethod('a', globalThis.unknown, 'c'); + +Test.staticProp(isUndefined, 'b', 'c'); +Test.staticProp('a', globalThis.unknown, 'c'); diff --git a/test/form/samples/tree-shake-default-parameters/termplate-tags/_config.js b/test/form/samples/tree-shake-default-parameters/termplate-tags/_config.js new file mode 100644 index 00000000000..6c444966620 --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/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/tree-shake-default-parameters/termplate-tags/_expected.js b/test/form/samples/tree-shake-default-parameters/termplate-tags/_expected.js new file mode 100644 index 00000000000..39bc336e378 --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/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/tree-shake-default-parameters/termplate-tags/main.js b/test/form/samples/tree-shake-default-parameters/termplate-tags/main.js new file mode 100644 index 00000000000..bed212d6f45 --- /dev/null +++ b/test/form/samples/tree-shake-default-parameters/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/treeshake-excess-arguments/unused-parameters/_expected.js b/test/form/samples/treeshake-excess-arguments/unused-parameters/_expected.js index 256b223b12a..c13a9d031cf 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,11 @@ -function test(a, b = globalThis.unknown(), c) {} -test(1, 2); +function test(a, b, c) {} +test(); function noEffect() {} test(1, 2, noEffect(), globalThis.unknown()); -const testArr = (a, b = globalThis.unknown(), c) => {}; -testArr(1, 2); +const testArr = (a, b, c) => {}; +testArr(); 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/default-parameter-tagged-templates/_config.js b/test/function/samples/default-parameter-tagged-templates/_config.js new file mode 100644 index 00000000000..6e2f4a704b6 --- /dev/null +++ b/test/function/samples/default-parameter-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/default-parameter-tagged-templates/main.js b/test/function/samples/default-parameter-tagged-templates/main.js new file mode 100644 index 00000000000..3c0a5f8bd8c --- /dev/null +++ b/test/function/samples/default-parameter-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/default-parameters-exported/_config.js b/test/function/samples/default-parameters-exported/_config.js new file mode 100644 index 00000000000..e2b24f315cd --- /dev/null +++ b/test/function/samples/default-parameters-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/default-parameters-exported/main.js b/test/function/samples/default-parameters-exported/main.js new file mode 100644 index 00000000000..c40f173c19b --- /dev/null +++ b/test/function/samples/default-parameters-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-module-side-effects/_config.js b/test/function/samples/parameter-defaults-module-side-effects/_config.js new file mode 100644 index 00000000000..6c32573208e --- /dev/null +++ b/test/function/samples/parameter-defaults-module-side-effects/_config.js @@ -0,0 +1,9 @@ +const path = require('path'); + +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/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');