From c443303954a291a0143bc3360d6e9019d4620e14 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Tue, 7 Jun 2022 16:37:44 +0200 Subject: [PATCH] Refactor side effect handling for property interactions (#4522) * Rename event -> interaction * Refactor MemberExpression assignment logic to prepare for new interactions * Use objects for interactions * Add additional interaction parameters * Pick "this"-argument from interaction * Use interaction for getReturnExpression * Replace dedicated methods with hasEffectsOnInteractionAtPath * Fix accessor handling for loops and updated expressions * Simplify assignment handling * Change assignment to use args property * Simplify ObjectEntity effect handling * Cache remaining interactions that may be cached * Improve coverage --- src/ast/CallOptions.ts | 10 - src/ast/Entity.ts | 7 +- src/ast/NodeEvents.ts | 5 - src/ast/NodeInteractions.ts | 56 ++++ src/ast/nodes/ArrayExpression.ts | 34 +-- src/ast/nodes/ArrayPattern.ts | 9 +- src/ast/nodes/ArrowFunctionExpression.ts | 30 ++- src/ast/nodes/AssignmentExpression.ts | 83 +++--- src/ast/nodes/AssignmentPattern.ts | 27 +- src/ast/nodes/BinaryExpression.ts | 5 +- src/ast/nodes/CallExpression.ts | 19 +- src/ast/nodes/ConditionalExpression.ts | 56 ++-- src/ast/nodes/ForInStatement.ts | 28 +- src/ast/nodes/ForOfStatement.ts | 17 +- src/ast/nodes/Identifier.ts | 65 +++-- src/ast/nodes/Literal.ts | 33 ++- src/ast/nodes/LogicalExpression.ts | 53 ++-- src/ast/nodes/MemberExpression.ts | 250 ++++++++++-------- src/ast/nodes/MetaProperty.ts | 7 +- src/ast/nodes/NewExpression.ts | 20 +- src/ast/nodes/ObjectExpression.ts | 34 +-- src/ast/nodes/ObjectPattern.ts | 12 +- src/ast/nodes/PropertyDefinition.ts | 29 +- src/ast/nodes/RestElement.ts | 12 +- src/ast/nodes/SequenceExpression.ts | 38 +-- src/ast/nodes/SpreadElement.ts | 20 +- src/ast/nodes/Super.ts | 10 +- src/ast/nodes/TaggedTemplateExpression.ts | 22 +- src/ast/nodes/TemplateLiteral.ts | 19 +- src/ast/nodes/ThisExpression.ts | 31 +-- src/ast/nodes/UnaryExpression.ts | 18 +- src/ast/nodes/UpdateExpression.ts | 30 ++- src/ast/nodes/VariableDeclaration.ts | 2 +- src/ast/nodes/shared/CallExpressionBase.ts | 74 +++--- src/ast/nodes/shared/ClassNode.ts | 46 ++-- src/ast/nodes/shared/Expression.ts | 28 +- src/ast/nodes/shared/FunctionBase.ts | 52 ++-- src/ast/nodes/shared/FunctionNode.ts | 74 +++--- src/ast/nodes/shared/MethodBase.ts | 104 +++++--- src/ast/nodes/shared/MethodTypes.ts | 74 +++--- src/ast/nodes/shared/MultiExpression.ts | 26 +- src/ast/nodes/shared/Node.ts | 88 ++++-- src/ast/nodes/shared/ObjectEntity.ts | 154 ++++------- src/ast/nodes/shared/ObjectMember.ts | 32 +-- src/ast/nodes/shared/ObjectPrototype.ts | 25 +- src/ast/nodes/shared/knownGlobals.ts | 16 +- src/ast/values.ts | 77 +++--- src/ast/variables/ArgumentsVariable.ts | 15 +- src/ast/variables/ExternalVariable.ts | 5 +- src/ast/variables/GlobalVariable.ts | 35 ++- src/ast/variables/LocalVariable.ts | 69 ++--- src/ast/variables/ThisVariable.ts | 66 ++--- src/ast/variables/Variable.ts | 9 +- .../samples/for-in-accessors/_config.js | 3 + .../function/samples/for-in-accessors/main.js | 10 + .../samples/for-of-accessors/_config.js | 3 + .../function/samples/for-of-accessors/main.js | 10 + .../update-expression-accessors/_config.js | 3 + .../update-expression-accessors/main.js | 16 ++ 59 files changed, 1147 insertions(+), 1058 deletions(-) delete mode 100644 src/ast/CallOptions.ts delete mode 100644 src/ast/NodeEvents.ts create mode 100644 src/ast/NodeInteractions.ts create mode 100644 test/function/samples/for-in-accessors/_config.js create mode 100644 test/function/samples/for-in-accessors/main.js create mode 100644 test/function/samples/for-of-accessors/_config.js create mode 100644 test/function/samples/for-of-accessors/main.js create mode 100644 test/function/samples/update-expression-accessors/_config.js create mode 100644 test/function/samples/update-expression-accessors/main.js diff --git a/src/ast/CallOptions.ts b/src/ast/CallOptions.ts deleted file mode 100644 index ff5b2c0601a..00000000000 --- a/src/ast/CallOptions.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type SpreadElement from './nodes/SpreadElement'; -import type { ExpressionEntity } from './nodes/shared/Expression'; - -export const NO_ARGS = []; - -export interface CallOptions { - args: (ExpressionEntity | SpreadElement)[]; - thisParam: ExpressionEntity | null; - withNew: boolean; -} diff --git a/src/ast/Entity.ts b/src/ast/Entity.ts index 7ab643b7008..13f958ba245 100644 --- a/src/ast/Entity.ts +++ b/src/ast/Entity.ts @@ -1,4 +1,5 @@ import type { HasEffectsContext } from './ExecutionContext'; +import { NodeInteractionAssigned } from './NodeInteractions'; import type { ObjectPath } from './utils/PathTracker'; // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -13,5 +14,9 @@ export interface WritableEntity extends Entity { */ deoptimizePath(path: ObjectPath): void; - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean; + hasEffectsOnInteractionAtPath( + path: ObjectPath, + interaction: NodeInteractionAssigned, + context: HasEffectsContext + ): boolean; } diff --git a/src/ast/NodeEvents.ts b/src/ast/NodeEvents.ts deleted file mode 100644 index d0ae880f6f3..00000000000 --- a/src/ast/NodeEvents.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const EVENT_ACCESSED = 0; -export const EVENT_ASSIGNED = 1; -export const EVENT_CALLED = 2; - -export type NodeEvent = typeof EVENT_ACCESSED | typeof EVENT_ASSIGNED | typeof EVENT_CALLED; diff --git a/src/ast/NodeInteractions.ts b/src/ast/NodeInteractions.ts new file mode 100644 index 00000000000..35113cb5b0f --- /dev/null +++ b/src/ast/NodeInteractions.ts @@ -0,0 +1,56 @@ +import SpreadElement from './nodes/SpreadElement'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './nodes/shared/Expression'; + +export const INTERACTION_ACCESSED = 0; +export const INTERACTION_ASSIGNED = 1; +export const INTERACTION_CALLED = 2; + +export interface NodeInteractionAccessed { + thisArg: ExpressionEntity | null; + type: typeof INTERACTION_ACCESSED; +} + +export const NODE_INTERACTION_UNKNOWN_ACCESS: NodeInteractionAccessed = { + thisArg: null, + type: INTERACTION_ACCESSED +}; + +export interface NodeInteractionAssigned { + args: readonly [ExpressionEntity]; + thisArg: ExpressionEntity | null; + type: typeof INTERACTION_ASSIGNED; +} + +export const UNKNOWN_ARG = [UNKNOWN_EXPRESSION] as const; + +export const NODE_INTERACTION_UNKNOWN_ASSIGNMENT: NodeInteractionAssigned = { + args: UNKNOWN_ARG, + thisArg: null, + type: INTERACTION_ASSIGNED +}; + +export interface NodeInteractionCalled { + args: readonly (ExpressionEntity | SpreadElement)[]; + thisArg: ExpressionEntity | null; + type: typeof INTERACTION_CALLED; + withNew: boolean; +} + +export const NO_ARGS = []; + +// While this is technically a call without arguments, we can compare against +// this reference in places where precise values or thisArg would make a +// difference +export const NODE_INTERACTION_UNKNOWN_CALL: NodeInteractionCalled = { + args: NO_ARGS, + thisArg: null, + type: INTERACTION_CALLED, + withNew: false +}; + +export type NodeInteraction = + | NodeInteractionAccessed + | NodeInteractionAssigned + | NodeInteractionCalled; + +export type NodeInteractionWithThisArg = NodeInteraction & { thisArg: ExpressionEntity }; diff --git a/src/ast/nodes/ArrayExpression.ts b/src/ast/nodes/ArrayExpression.ts index 67710bc8f13..b2111b8f92c 100644 --- a/src/ast/nodes/ArrayExpression.ts +++ b/src/ast/nodes/ArrayExpression.ts @@ -1,7 +1,7 @@ -import type { CallOptions } from '../CallOptions'; import type { DeoptimizableEntity } from '../DeoptimizableEntity'; import type { HasEffectsContext } from '../ExecutionContext'; -import type { NodeEvent } from '../NodeEvents'; +import type { NodeInteractionCalled, NodeInteractionWithThisArg } from '../NodeInteractions'; +import { NodeInteraction } from '../NodeInteractions'; import { type ObjectPath, type PathTracker, @@ -25,18 +25,12 @@ export default class ArrayExpression extends NodeBase { this.getObjectEntity().deoptimizePath(path); } - deoptimizeThisOnEventAtPath( - event: NodeEvent, + deoptimizeThisOnInteractionAtPath( + interaction: NodeInteractionWithThisArg, path: ObjectPath, - thisParameter: ExpressionEntity, recursionTracker: PathTracker ): void { - this.getObjectEntity().deoptimizeThisOnEventAtPath( - event, - path, - thisParameter, - recursionTracker - ); + this.getObjectEntity().deoptimizeThisOnInteractionAtPath(interaction, path, recursionTracker); } getLiteralValueAtPath( @@ -49,32 +43,24 @@ export default class ArrayExpression extends NodeBase { getReturnExpressionWhenCalledAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteractionCalled, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { return this.getObjectEntity().getReturnExpressionWhenCalledAtPath( path, - callOptions, + interaction, recursionTracker, origin ); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return this.getObjectEntity().hasEffectsWhenAccessedAtPath(path, context); - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return this.getObjectEntity().hasEffectsWhenAssignedAtPath(path, context); - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - return this.getObjectEntity().hasEffectsWhenCalledAtPath(path, callOptions, context); + return this.getObjectEntity().hasEffectsOnInteractionAtPath(path, interaction, context); } protected applyDeoptimizations(): void { diff --git a/src/ast/nodes/ArrayPattern.ts b/src/ast/nodes/ArrayPattern.ts index aab64901f49..e8e2ccdc229 100644 --- a/src/ast/nodes/ArrayPattern.ts +++ b/src/ast/nodes/ArrayPattern.ts @@ -1,4 +1,5 @@ import type { HasEffectsContext } from '../ExecutionContext'; +import { NodeInteractionAssigned } from '../NodeInteractions'; import { EMPTY_PATH, type ObjectPath } from '../utils/PathTracker'; import type LocalVariable from '../variables/LocalVariable'; import type Variable from '../variables/Variable'; @@ -38,9 +39,13 @@ export default class ArrayPattern extends NodeBase implements PatternNode { } // Patterns are only checked at the emtpy path at the moment - hasEffectsWhenAssignedAtPath(_path: ObjectPath, context: HasEffectsContext): boolean { + hasEffectsOnInteractionAtPath( + _path: ObjectPath, + interaction: NodeInteractionAssigned, + context: HasEffectsContext + ): boolean { for (const element of this.elements) { - if (element?.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context)) return true; + if (element?.hasEffectsOnInteractionAtPath(EMPTY_PATH, interaction, context)) return true; } return false; } diff --git a/src/ast/nodes/ArrowFunctionExpression.ts b/src/ast/nodes/ArrowFunctionExpression.ts index cb294d859e1..75ec03b1ac6 100644 --- a/src/ast/nodes/ArrowFunctionExpression.ts +++ b/src/ast/nodes/ArrowFunctionExpression.ts @@ -1,5 +1,5 @@ -import { type CallOptions } from '../CallOptions'; import { type HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { INTERACTION_CALLED, NodeInteraction } from '../NodeInteractions'; import ReturnValueScope from '../scopes/ReturnValueScope'; import type Scope from '../scopes/Scope'; import { type ObjectPath } from '../utils/PathTracker'; @@ -30,22 +30,24 @@ export default class ArrowFunctionExpression extends FunctionBase { return false; } - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - if (super.hasEffectsWhenCalledAtPath(path, callOptions, context)) return true; - const { ignore, brokenFlow } = context; - context.ignore = { - breaks: false, - continues: false, - labels: new Set(), - returnYield: true - }; - if (this.body.hasEffects(context)) return true; - context.ignore = ignore; - context.brokenFlow = brokenFlow; + if (super.hasEffectsOnInteractionAtPath(path, interaction, context)) return true; + if (interaction.type === INTERACTION_CALLED) { + const { ignore, brokenFlow } = context; + context.ignore = { + breaks: false, + continues: false, + labels: new Set(), + returnYield: true + }; + if (this.body.hasEffects(context)) return true; + context.ignore = ignore; + context.brokenFlow = brokenFlow; + } return false; } diff --git a/src/ast/nodes/AssignmentExpression.ts b/src/ast/nodes/AssignmentExpression.ts index 8912b01cdb3..a985ddc272a 100644 --- a/src/ast/nodes/AssignmentExpression.ts +++ b/src/ast/nodes/AssignmentExpression.ts @@ -17,6 +17,7 @@ import { type HasEffectsContext, type InclusionContext } from '../ExecutionContext'; +import { NodeInteraction } from '../NodeInteractions'; import { EMPTY_PATH, type ObjectPath, UNKNOWN_PATH } from '../utils/PathTracker'; import type Variable from '../variables/Variable'; import Identifier from './Identifier'; @@ -45,33 +46,40 @@ export default class AssignmentExpression extends NodeBase { declare type: NodeType.tAssignmentExpression; hasEffects(context: HasEffectsContext): boolean { - if (!this.deoptimized) this.applyDeoptimizations(); + const { deoptimized, left, right } = this; + if (!deoptimized) this.applyDeoptimizations(); + // MemberExpressions do not access the property before assignments if the + // operator is '='. return ( - this.right.hasEffects(context) || - this.left.hasEffects(context) || - this.left.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context) + right.hasEffects(context) || left.hasEffectsAsAssignmentTarget(context, this.operator !== '=') ); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return path.length > 0 && this.right.hasEffectsWhenAccessedAtPath(path, context); + hasEffectsOnInteractionAtPath( + path: ObjectPath, + interaction: NodeInteraction, + context: HasEffectsContext + ): boolean { + return this.right.hasEffectsOnInteractionAtPath(path, interaction, context); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { - if (!this.deoptimized) this.applyDeoptimizations(); + const { deoptimized, left, right, operator } = this; + if (!deoptimized) this.applyDeoptimizations(); this.included = true; - let hasEffectsContext; if ( includeChildrenRecursively || - this.operator !== '=' || - this.left.included || - ((hasEffectsContext = createHasEffectsContext()), - this.left.hasEffects(hasEffectsContext) || - this.left.hasEffectsWhenAssignedAtPath(EMPTY_PATH, hasEffectsContext)) + operator !== '=' || + left.included || + left.hasEffectsAsAssignmentTarget(createHasEffectsContext(), false) ) { - this.left.include(context, includeChildrenRecursively); + left.includeAsAssignmentTarget(context, includeChildrenRecursively, operator !== '='); } - this.right.include(context, includeChildrenRecursively); + right.include(context, includeChildrenRecursively); + } + + initialise(): void { + this.left.setAssignedValue(this.right); } render( @@ -79,36 +87,37 @@ export default class AssignmentExpression extends NodeBase { options: RenderOptions, { preventASI, renderedParentType, renderedSurroundingElement }: NodeRenderOptions = BLANK ): void { - if (this.left.included) { - this.left.render(code, options); - this.right.render(code, options); + const { left, right, start, end, parent } = this; + if (left.included) { + left.render(code, options); + right.render(code, options); } else { const inclusionStart = findNonWhiteSpace( code.original, - findFirstOccurrenceOutsideComment(code.original, '=', this.left.end) + 1 + findFirstOccurrenceOutsideComment(code.original, '=', left.end) + 1 ); - code.remove(this.start, inclusionStart); + code.remove(start, inclusionStart); if (preventASI) { - removeLineBreaks(code, inclusionStart, this.right.start); + removeLineBreaks(code, inclusionStart, right.start); } - this.right.render(code, options, { - renderedParentType: renderedParentType || this.parent.type, - renderedSurroundingElement: renderedSurroundingElement || this.parent.type + right.render(code, options, { + renderedParentType: renderedParentType || parent.type, + renderedSurroundingElement: renderedSurroundingElement || parent.type }); } if (options.format === 'system') { - if (this.left instanceof Identifier) { - const variable = this.left.variable!; + if (left instanceof Identifier) { + const variable = left.variable!; const exportNames = options.exportNamesByVariable.get(variable); if (exportNames) { if (exportNames.length === 1) { - renderSystemExportExpression(variable, this.start, this.end, code, options); + renderSystemExportExpression(variable, start, end, code, options); } else { renderSystemExportSequenceAfterExpression( variable, - this.start, - this.end, - this.parent.type !== NodeType.ExpressionStatement, + start, + end, + parent.type !== NodeType.ExpressionStatement, code, options ); @@ -117,12 +126,12 @@ export default class AssignmentExpression extends NodeBase { } } else { const systemPatternExports: Variable[] = []; - this.left.addExportedVariables(systemPatternExports, options.exportNamesByVariable); + left.addExportedVariables(systemPatternExports, options.exportNamesByVariable); if (systemPatternExports.length > 0) { renderSystemExportFunction( systemPatternExports, - this.start, - this.end, + start, + end, renderedSurroundingElement === NodeType.ExpressionStatement, code, options @@ -132,13 +141,13 @@ export default class AssignmentExpression extends NodeBase { } } if ( - this.left.included && - this.left instanceof ObjectPattern && + left.included && + left instanceof ObjectPattern && (renderedSurroundingElement === NodeType.ExpressionStatement || renderedSurroundingElement === NodeType.ArrowFunctionExpression) ) { - code.appendRight(this.start, '('); - code.prependLeft(this.end, ')'); + code.appendRight(start, '('); + code.prependLeft(end, ')'); } } diff --git a/src/ast/nodes/AssignmentPattern.ts b/src/ast/nodes/AssignmentPattern.ts index f0238ae0fb7..14ef36a46b0 100644 --- a/src/ast/nodes/AssignmentPattern.ts +++ b/src/ast/nodes/AssignmentPattern.ts @@ -2,13 +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 { NodeInteractionAssigned } from '../NodeInteractions'; 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, IncludeChildren, NodeBase } from './shared/Node'; +import { type ExpressionNode, NodeBase } from './shared/Node'; import type { PatternNode } from './shared/Pattern'; export default class AssignmentPattern extends NodeBase implements PatternNode { @@ -31,15 +31,14 @@ export default class AssignmentPattern extends NodeBase implements PatternNode { path.length === 0 && this.left.deoptimizePath(path); } - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return path.length > 0 || this.left.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context); - } - - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { - if (!this.deoptimized) this.applyDeoptimizations(); - this.included = true; - this.left.include(context, includeChildrenRecursively); - this.right.include(context, includeChildrenRecursively); + hasEffectsOnInteractionAtPath( + path: ObjectPath, + interaction: NodeInteractionAssigned, + context: HasEffectsContext + ): boolean { + return ( + path.length > 0 || this.left.hasEffectsOnInteractionAtPath(EMPTY_PATH, interaction, context) + ); } markDeclarationReached(): void { @@ -52,11 +51,7 @@ export default class AssignmentPattern extends NodeBase implements PatternNode { { isShorthandProperty }: NodeRenderOptions = BLANK ): void { this.left.render(code, options, { isShorthandProperty }); - if (this.right.included) { - this.right.render(code, options); - } else { - code.remove(this.left.end, this.end); - } + this.right.render(code, options); } protected applyDeoptimizations(): void { diff --git a/src/ast/nodes/BinaryExpression.ts b/src/ast/nodes/BinaryExpression.ts index 4f38da0106b..d1167831f79 100644 --- a/src/ast/nodes/BinaryExpression.ts +++ b/src/ast/nodes/BinaryExpression.ts @@ -3,6 +3,7 @@ import { BLANK } from '../../utils/blank'; import type { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import type { DeoptimizableEntity } from '../DeoptimizableEntity'; import type { HasEffectsContext } from '../ExecutionContext'; +import { INTERACTION_ACCESSED, NodeInteraction } from '../NodeInteractions'; import { EMPTY_PATH, type ObjectPath, @@ -83,8 +84,8 @@ export default class BinaryExpression extends NodeBase implements DeoptimizableE return super.hasEffects(context); } - hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean { - return path.length > 1; + hasEffectsOnInteractionAtPath(path: ObjectPath, { type }: NodeInteraction): boolean { + return type !== INTERACTION_ACCESSED || path.length > 1; } render( diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index 2b6f84f234a..414904a8608 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -5,7 +5,7 @@ import { renderCallArguments } from '../../utils/renderCallArguments'; import { type NodeRenderOptions, type RenderOptions } from '../../utils/renderHelpers'; import type { DeoptimizableEntity } from '../DeoptimizableEntity'; import type { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { EVENT_CALLED } from '../NodeEvents'; +import { INTERACTION_CALLED, NodeInteractionWithThisArg } from '../NodeInteractions'; import { EMPTY_PATH, type PathTracker, @@ -53,12 +53,13 @@ export default class CallExpression extends CallExpressionBase implements Deopti ); } } - this.callOptions = { + this.interaction = { args: this.arguments, - thisParam: + thisArg: this.callee instanceof MemberExpression && !this.callee.variable ? this.callee.object : null, + type: INTERACTION_CALLED, withNew: false }; } @@ -75,7 +76,7 @@ export default class CallExpression extends CallExpressionBase implements Deopti return false; return ( this.callee.hasEffects(context) || - this.callee.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.callOptions, context) + this.callee.hasEffectsOnInteractionAtPath(EMPTY_PATH, this.interaction, context) ); } finally { if (!this.deoptimized) this.applyDeoptimizations(); @@ -118,12 +119,10 @@ export default class CallExpression extends CallExpressionBase implements Deopti protected applyDeoptimizations(): void { this.deoptimized = true; - const { thisParam } = this.callOptions; - if (thisParam) { - this.callee.deoptimizeThisOnEventAtPath( - EVENT_CALLED, + if (this.interaction.thisArg) { + this.callee.deoptimizeThisOnInteractionAtPath( + this.interaction as NodeInteractionWithThisArg, EMPTY_PATH, - thisParam, SHARED_RECURSION_TRACKER ); } @@ -141,7 +140,7 @@ export default class CallExpression extends CallExpressionBase implements Deopti this.returnExpression = UNKNOWN_EXPRESSION; return (this.returnExpression = this.callee.getReturnExpressionWhenCalledAtPath( EMPTY_PATH, - this.callOptions, + this.interaction, recursionTracker, this )); diff --git a/src/ast/nodes/ConditionalExpression.ts b/src/ast/nodes/ConditionalExpression.ts index 0ff6cc1fcbb..5673ad60a73 100644 --- a/src/ast/nodes/ConditionalExpression.ts +++ b/src/ast/nodes/ConditionalExpression.ts @@ -8,10 +8,13 @@ import { RenderOptions } from '../../utils/renderHelpers'; import { removeAnnotations } from '../../utils/treeshakeNode'; -import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { NodeEvent } from '../NodeEvents'; +import { + NodeInteraction, + NodeInteractionCalled, + NodeInteractionWithThisArg +} from '../NodeInteractions'; import { EMPTY_PATH, ObjectPath, @@ -56,14 +59,13 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz } } - deoptimizeThisOnEventAtPath( - event: NodeEvent, + deoptimizeThisOnInteractionAtPath( + interaction: NodeInteractionWithThisArg, path: ObjectPath, - thisParameter: ExpressionEntity, recursionTracker: PathTracker ): void { - this.consequent.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); - this.alternate.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + this.consequent.deoptimizeThisOnInteractionAtPath(interaction, path, recursionTracker); + this.alternate.deoptimizeThisOnInteractionAtPath(interaction, path, recursionTracker); } getLiteralValueAtPath( @@ -79,7 +81,7 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz getReturnExpressionWhenCalledAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteractionCalled, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { @@ -88,13 +90,13 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz return new MultiExpression([ this.consequent.getReturnExpressionWhenCalledAtPath( path, - callOptions, + interaction, recursionTracker, origin ), this.alternate.getReturnExpressionWhenCalledAtPath( path, - callOptions, + interaction, recursionTracker, origin ) @@ -102,7 +104,7 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz this.expressionsToBeDeoptimized.push(origin); return usedBranch.getReturnExpressionWhenCalledAtPath( path, - callOptions, + interaction, recursionTracker, origin ); @@ -117,41 +119,19 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz return usedBranch.hasEffects(context); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - const usedBranch = this.getUsedBranch(); - if (!usedBranch) { - return ( - this.consequent.hasEffectsWhenAccessedAtPath(path, context) || - this.alternate.hasEffectsWhenAccessedAtPath(path, context) - ); - } - return usedBranch.hasEffectsWhenAccessedAtPath(path, context); - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - const usedBranch = this.getUsedBranch(); - if (!usedBranch) { - return ( - this.consequent.hasEffectsWhenAssignedAtPath(path, context) || - this.alternate.hasEffectsWhenAssignedAtPath(path, context) - ); - } - return usedBranch.hasEffectsWhenAssignedAtPath(path, context); - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { const usedBranch = this.getUsedBranch(); if (!usedBranch) { return ( - this.consequent.hasEffectsWhenCalledAtPath(path, callOptions, context) || - this.alternate.hasEffectsWhenCalledAtPath(path, callOptions, context) + this.consequent.hasEffectsOnInteractionAtPath(path, interaction, context) || + this.alternate.hasEffectsOnInteractionAtPath(path, interaction, context) ); } - return usedBranch.hasEffectsWhenCalledAtPath(path, callOptions, context); + return usedBranch.hasEffectsOnInteractionAtPath(path, interaction, context); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { diff --git a/src/ast/nodes/ForInStatement.ts b/src/ast/nodes/ForInStatement.ts index 46a91d4595f..8ca62b46738 100644 --- a/src/ast/nodes/ForInStatement.ts +++ b/src/ast/nodes/ForInStatement.ts @@ -4,8 +4,10 @@ import type { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import BlockScope from '../scopes/BlockScope'; import type Scope from '../scopes/Scope'; import { EMPTY_PATH } from '../utils/PathTracker'; +import MemberExpression from './MemberExpression'; import type * as NodeType from './NodeType'; import type VariableDeclaration from './VariableDeclaration'; +import { UNKNOWN_EXPRESSION } from './shared/Expression'; import { type ExpressionNode, type IncludeChildren, @@ -16,7 +18,7 @@ import type { PatternNode } from './shared/Pattern'; export default class ForInStatement extends StatementBase { declare body: StatementNode; - declare left: VariableDeclaration | PatternNode; + declare left: VariableDeclaration | PatternNode | MemberExpression; declare right: ExpressionNode; declare type: NodeType.tForInStatement; @@ -25,14 +27,9 @@ export default class ForInStatement extends StatementBase { } hasEffects(context: HasEffectsContext): boolean { - if (!this.deoptimized) this.applyDeoptimizations(); - if ( - (this.left && - (this.left.hasEffects(context) || - this.left.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context))) || - (this.right && this.right.hasEffects(context)) - ) - return true; + const { deoptimized, left, right } = this; + if (!deoptimized) this.applyDeoptimizations(); + if (left.hasEffectsAsAssignmentTarget(context, false) || right.hasEffects(context)) return true; const { brokenFlow, ignore: { breaks, continues } @@ -47,15 +44,20 @@ export default class ForInStatement extends StatementBase { } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { - if (!this.deoptimized) this.applyDeoptimizations(); + const { body, deoptimized, left, right } = this; + if (!deoptimized) this.applyDeoptimizations(); this.included = true; - this.left.include(context, includeChildrenRecursively || true); - this.right.include(context, includeChildrenRecursively); + left.includeAsAssignmentTarget(context, includeChildrenRecursively || true, false); + right.include(context, includeChildrenRecursively); const { brokenFlow } = context; - this.body.include(context, includeChildrenRecursively, { asSingleStatement: true }); + body.include(context, includeChildrenRecursively, { asSingleStatement: true }); context.brokenFlow = brokenFlow; } + initialise() { + this.left.setAssignedValue(UNKNOWN_EXPRESSION); + } + render(code: MagicString, options: RenderOptions): void { this.left.render(code, options, NO_SEMICOLON); this.right.render(code, options, NO_SEMICOLON); diff --git a/src/ast/nodes/ForOfStatement.ts b/src/ast/nodes/ForOfStatement.ts index 8d35fd4e136..edca00c7d5d 100644 --- a/src/ast/nodes/ForOfStatement.ts +++ b/src/ast/nodes/ForOfStatement.ts @@ -4,8 +4,10 @@ import type { InclusionContext } from '../ExecutionContext'; import BlockScope from '../scopes/BlockScope'; import type Scope from '../scopes/Scope'; import { EMPTY_PATH } from '../utils/PathTracker'; +import MemberExpression from './MemberExpression'; import type * as NodeType from './NodeType'; import type VariableDeclaration from './VariableDeclaration'; +import { UNKNOWN_EXPRESSION } from './shared/Expression'; import { type ExpressionNode, type IncludeChildren, @@ -17,7 +19,7 @@ import type { PatternNode } from './shared/Pattern'; export default class ForOfStatement extends StatementBase { declare await: boolean; declare body: StatementNode; - declare left: VariableDeclaration | PatternNode; + declare left: VariableDeclaration | PatternNode | MemberExpression; declare right: ExpressionNode; declare type: NodeType.tForOfStatement; @@ -32,15 +34,20 @@ export default class ForOfStatement extends StatementBase { } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { - if (!this.deoptimized) this.applyDeoptimizations(); + const { body, deoptimized, left, right } = this; + if (!deoptimized) this.applyDeoptimizations(); this.included = true; - this.left.include(context, includeChildrenRecursively || true); - this.right.include(context, includeChildrenRecursively); + left.includeAsAssignmentTarget(context, includeChildrenRecursively || true, false); + right.include(context, includeChildrenRecursively); const { brokenFlow } = context; - this.body.include(context, includeChildrenRecursively, { asSingleStatement: true }); + body.include(context, includeChildrenRecursively, { asSingleStatement: true }); context.brokenFlow = brokenFlow; } + initialise() { + this.left.setAssignedValue(UNKNOWN_EXPRESSION); + } + render(code: MagicString, options: RenderOptions): void { this.left.render(code, options, NO_SEMICOLON); this.right.render(code, options, NO_SEMICOLON); diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index 03125e097ce..b4ddde25c0f 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -3,10 +3,17 @@ import type MagicString from 'magic-string'; import type { NormalizedTreeshakingOptions } from '../../rollup/types'; 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, InclusionContext } from '../ExecutionContext'; -import type { NodeEvent } from '../NodeEvents'; +import type { NodeInteractionWithThisArg } from '../NodeInteractions'; +import { + INTERACTION_ACCESSED, + INTERACTION_ASSIGNED, + INTERACTION_CALLED, + NODE_INTERACTION_UNKNOWN_ACCESS, + NodeInteraction, + NodeInteractionCalled +} from '../NodeInteractions'; import type FunctionScope from '../scopes/FunctionScope'; import { EMPTY_PATH, type ObjectPath, type PathTracker } from '../utils/PathTracker'; import GlobalVariable from '../variables/GlobalVariable'; @@ -95,13 +102,12 @@ export default class Identifier extends NodeBase implements PatternNode { this.variable?.deoptimizePath(path); } - deoptimizeThisOnEventAtPath( - event: NodeEvent, + deoptimizeThisOnInteractionAtPath( + interaction: NodeInteractionWithThisArg, path: ObjectPath, - thisParameter: ExpressionEntity, recursionTracker: PathTracker ): void { - this.variable!.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + this.variable!.deoptimizeThisOnInteractionAtPath(interaction, path, recursionTracker); } getLiteralValueAtPath( @@ -114,19 +120,19 @@ export default class Identifier extends NodeBase implements PatternNode { getReturnExpressionWhenCalledAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteractionCalled, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { return this.getVariableRespectingTDZ()!.getReturnExpressionWhenCalledAtPath( path, - callOptions, + interaction, recursionTracker, origin ); } - hasEffects(): boolean { + hasEffects(context: HasEffectsContext): boolean { if (!this.deoptimized) this.applyDeoptimizations(); if (this.isPossibleTDZ() && this.variable!.kind !== 'var') { return true; @@ -134,29 +140,36 @@ export default class Identifier extends NodeBase implements PatternNode { return ( (this.context.options.treeshake as NormalizedTreeshakingOptions).unknownGlobalSideEffects && this.variable instanceof GlobalVariable && - this.variable.hasEffectsWhenAccessedAtPath(EMPTY_PATH) - ); - } - - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return ( - this.variable !== null && - this.getVariableRespectingTDZ()!.hasEffectsWhenAccessedAtPath(path, context) + this.variable.hasEffectsOnInteractionAtPath( + EMPTY_PATH, + NODE_INTERACTION_UNKNOWN_ACCESS, + context + ) ); } - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return ( - path.length > 0 ? this.getVariableRespectingTDZ() : this.variable - )!.hasEffectsWhenAssignedAtPath(path, context); - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - return this.getVariableRespectingTDZ()!.hasEffectsWhenCalledAtPath(path, callOptions, context); + switch (interaction.type) { + case INTERACTION_ACCESSED: + return ( + this.variable !== null && + this.getVariableRespectingTDZ()!.hasEffectsOnInteractionAtPath(path, interaction, context) + ); + case INTERACTION_ASSIGNED: + return ( + path.length > 0 ? this.getVariableRespectingTDZ() : this.variable + )!.hasEffectsOnInteractionAtPath(path, interaction, context); + case INTERACTION_CALLED: + return this.getVariableRespectingTDZ()!.hasEffectsOnInteractionAtPath( + path, + interaction, + context + ); + } } include(): void { diff --git a/src/ast/nodes/Literal.ts b/src/ast/nodes/Literal.ts index 192cffa8fe3..5ff42781d69 100644 --- a/src/ast/nodes/Literal.ts +++ b/src/ast/nodes/Literal.ts @@ -1,6 +1,11 @@ import type MagicString from 'magic-string'; -import type { CallOptions } from '../CallOptions'; import type { HasEffectsContext } from '../ExecutionContext'; +import { + INTERACTION_ACCESSED, + INTERACTION_ASSIGNED, + INTERACTION_CALLED, + NodeInteraction +} from '../NodeInteractions'; import type { ObjectPath } from '../utils/PathTracker'; import { getLiteralMembersForValue, @@ -29,7 +34,7 @@ export default class Literal extends Node private declare members: { [key: string]: MemberDescription }; - deoptimizeThisOnEventAtPath(): void {} + deoptimizeThisOnInteractionAtPath(): void {} getLiteralValueAtPath(path: ObjectPath): LiteralValueOrUnknown { if ( @@ -50,22 +55,22 @@ export default class Literal extends Node return getMemberReturnExpressionWhenCalled(this.members, path[0]); } - hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean { - if (this.value === null) { - return path.length > 0; - } - return path.length > 1; - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - if (path.length === 1) { - return hasMemberEffectWhenCalled(this.members, path[0], callOptions, context); + switch (interaction.type) { + case INTERACTION_ACCESSED: + return path.length > (this.value === null ? 0 : 1); + case INTERACTION_ASSIGNED: + return true; + case INTERACTION_CALLED: + return ( + path.length !== 1 || + hasMemberEffectWhenCalled(this.members, path[0], interaction, context) + ); } - return true; } initialise(): void { diff --git a/src/ast/nodes/LogicalExpression.ts b/src/ast/nodes/LogicalExpression.ts index 763ff942b69..d7d496d6dd2 100644 --- a/src/ast/nodes/LogicalExpression.ts +++ b/src/ast/nodes/LogicalExpression.ts @@ -8,10 +8,10 @@ import { type RenderOptions } from '../../utils/renderHelpers'; import { removeAnnotations } from '../../utils/treeshakeNode'; -import type { CallOptions } from '../CallOptions'; import type { DeoptimizableEntity } from '../DeoptimizableEntity'; import type { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import type { NodeEvent } from '../NodeEvents'; +import type { NodeInteractionWithThisArg } from '../NodeInteractions'; +import { NodeInteraction, NodeInteractionCalled } from '../NodeInteractions'; import { EMPTY_PATH, type ObjectPath, @@ -65,14 +65,13 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable } } - deoptimizeThisOnEventAtPath( - event: NodeEvent, + deoptimizeThisOnInteractionAtPath( + interaction: NodeInteractionWithThisArg, path: ObjectPath, - thisParameter: ExpressionEntity, recursionTracker: PathTracker ): void { - this.left.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); - this.right.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + this.left.deoptimizeThisOnInteractionAtPath(interaction, path, recursionTracker); + this.right.deoptimizeThisOnInteractionAtPath(interaction, path, recursionTracker); } getLiteralValueAtPath( @@ -88,20 +87,20 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable getReturnExpressionWhenCalledAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteractionCalled, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { const usedBranch = this.getUsedBranch(); if (!usedBranch) return new MultiExpression([ - this.left.getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin), - this.right.getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin) + this.left.getReturnExpressionWhenCalledAtPath(path, interaction, recursionTracker, origin), + this.right.getReturnExpressionWhenCalledAtPath(path, interaction, recursionTracker, origin) ]); this.expressionsToBeDeoptimized.push(origin); return usedBranch.getReturnExpressionWhenCalledAtPath( path, - callOptions, + interaction, recursionTracker, origin ); @@ -117,41 +116,19 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable return false; } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - const usedBranch = this.getUsedBranch(); - if (!usedBranch) { - return ( - this.left.hasEffectsWhenAccessedAtPath(path, context) || - this.right.hasEffectsWhenAccessedAtPath(path, context) - ); - } - return usedBranch.hasEffectsWhenAccessedAtPath(path, context); - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - const usedBranch = this.getUsedBranch(); - if (!usedBranch) { - return ( - this.left.hasEffectsWhenAssignedAtPath(path, context) || - this.right.hasEffectsWhenAssignedAtPath(path, context) - ); - } - return usedBranch.hasEffectsWhenAssignedAtPath(path, context); - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { const usedBranch = this.getUsedBranch(); if (!usedBranch) { return ( - this.left.hasEffectsWhenCalledAtPath(path, callOptions, context) || - this.right.hasEffectsWhenCalledAtPath(path, callOptions, context) + this.left.hasEffectsOnInteractionAtPath(path, interaction, context) || + this.right.hasEffectsOnInteractionAtPath(path, interaction, context) ); } - return usedBranch.hasEffectsWhenCalledAtPath(path, callOptions, context); + return usedBranch.hasEffectsOnInteractionAtPath(path, interaction, context); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index d98d548fa30..93922eb0e2c 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -1,12 +1,20 @@ import type MagicString from 'magic-string'; +import { AstContext } from '../../Module'; import type { NormalizedTreeshakingOptions } from '../../rollup/types'; import { BLANK } from '../../utils/blank'; import relativeId from '../../utils/relativeId'; import type { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; -import type { CallOptions } from '../CallOptions'; import type { DeoptimizableEntity } from '../DeoptimizableEntity'; import type { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { EVENT_ACCESSED, EVENT_ASSIGNED, type NodeEvent } from '../NodeEvents'; +import { + INTERACTION_ACCESSED, + INTERACTION_ASSIGNED, + NodeInteraction, + NodeInteractionAccessed, + NodeInteractionAssigned, + NodeInteractionCalled, + NodeInteractionWithThisArg +} from '../NodeInteractions'; import { EMPTY_PATH, type ObjectPath, @@ -20,7 +28,6 @@ import { import ExternalVariable from '../variables/ExternalVariable'; import type NamespaceVariable from '../variables/NamespaceVariable'; import type Variable from '../variables/Variable'; -import AssignmentExpression from './AssignmentExpression'; import Identifier from './Identifier'; import Literal from './Literal'; import type * as NodeType from './NodeType'; @@ -89,6 +96,9 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE declare propertyKey: ObjectPathKey | null; declare type: NodeType.tMemberExpression; variable: Variable | null = null; + protected declare assignmentInteraction: NodeInteractionAssigned & { thisArg: ExpressionEntity }; + private declare accessInteraction: NodeInteractionAccessed & { thisArg: ExpressionEntity }; + private assignmentDeoptimized = false; private bound = false; private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; private replacement: string | null = null; @@ -98,7 +108,11 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE const path = getPathIfNotComputed(this); const baseVariable = path && this.scope.findVariable(path[0].key); if (baseVariable && baseVariable.isNamespace) { - const resolvedVariable = this.resolveNamespaceVariables(baseVariable, path!.slice(1)); + const resolvedVariable = resolveNamespaceVariables( + baseVariable, + path!.slice(1), + this.context + ); if (!resolvedVariable) { super.bind(); } else if (typeof resolvedVariable === 'string') { @@ -137,24 +151,22 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE } } - deoptimizeThisOnEventAtPath( - event: NodeEvent, + deoptimizeThisOnInteractionAtPath( + interaction: NodeInteractionWithThisArg, path: ObjectPath, - thisParameter: ExpressionEntity, recursionTracker: PathTracker ): void { if (this.variable) { - this.variable.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + this.variable.deoptimizeThisOnInteractionAtPath(interaction, path, recursionTracker); } else if (!this.replacement) { if (path.length < MAX_PATH_DEPTH) { - this.object.deoptimizeThisOnEventAtPath( - event, + this.object.deoptimizeThisOnInteractionAtPath( + interaction, [this.getPropertyKey(), ...path], - thisParameter, recursionTracker ); } else { - thisParameter.deoptimizePath(UNKNOWN_PATH); + interaction.thisArg.deoptimizePath(UNKNOWN_PATH); } } } @@ -164,7 +176,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE recursionTracker: PathTracker, origin: DeoptimizableEntity ): LiteralValueOrUnknown { - if (this.variable !== null) { + if (this.variable) { return this.variable.getLiteralValueAtPath(path, recursionTracker, origin); } if (this.replacement) { @@ -183,14 +195,14 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE getReturnExpressionWhenCalledAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteractionCalled, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { - if (this.variable !== null) { + if (this.variable) { return this.variable.getReturnExpressionWhenCalledAtPath( path, - callOptions, + interaction, recursionTracker, origin ); @@ -202,7 +214,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE if (path.length < MAX_PATH_DEPTH) { return this.object.getReturnExpressionWhenCalledAtPath( [this.getPropertyKey(), ...path], - callOptions, + interaction, recursionTracker, origin ); @@ -212,64 +224,39 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE hasEffects(context: HasEffectsContext): boolean { if (!this.deoptimized) this.applyDeoptimizations(); - const { propertyReadSideEffects } = this.context.options - .treeshake as NormalizedTreeshakingOptions; return ( this.property.hasEffects(context) || this.object.hasEffects(context) || - // Assignments do not access the property before assigning - (!( - this.variable || - this.replacement || - (this.parent instanceof AssignmentExpression && this.parent.operator === '=') - ) && - propertyReadSideEffects && - (propertyReadSideEffects === 'always' || - this.object.hasEffectsWhenAccessedAtPath([this.getPropertyKey()], context))) + this.hasAccessEffect(context) ); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (this.variable !== null) { - return this.variable.hasEffectsWhenAccessedAtPath(path, context); - } - if (this.replacement) { - return true; - } - if (path.length < MAX_PATH_DEPTH) { - return this.object.hasEffectsWhenAccessedAtPath([this.getPropertyKey(), ...path], context); - } - return true; - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (this.variable !== null) { - return this.variable.hasEffectsWhenAssignedAtPath(path, context); - } - if (this.replacement) { - return true; - } - if (path.length < MAX_PATH_DEPTH) { - return this.object.hasEffectsWhenAssignedAtPath([this.getPropertyKey(), ...path], context); - } - return true; + hasEffectsAsAssignmentTarget(context: HasEffectsContext, checkAccess: boolean): boolean { + if (checkAccess && !this.deoptimized) this.applyDeoptimizations(); + if (!this.assignmentDeoptimized) this.applyAssignmentDeoptimization(); + return ( + this.property.hasEffects(context) || + this.object.hasEffects(context) || + (checkAccess && this.hasAccessEffect(context)) || + this.hasEffectsOnInteractionAtPath(EMPTY_PATH, this.assignmentInteraction, context) + ); } - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - if (this.variable !== null) { - return this.variable.hasEffectsWhenCalledAtPath(path, callOptions, context); + if (this.variable) { + return this.variable.hasEffectsOnInteractionAtPath(path, interaction, context); } if (this.replacement) { return true; } if (path.length < MAX_PATH_DEPTH) { - return this.object.hasEffectsWhenCalledAtPath( + return this.object.hasEffectsOnInteractionAtPath( [this.getPropertyKey(), ...path], - callOptions, + interaction, context ); } @@ -278,14 +265,20 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { if (!this.deoptimized) this.applyDeoptimizations(); - if (!this.included) { - this.included = true; - if (this.variable !== null) { - this.context.includeVariableInModule(this.variable); - } + this.includeProperties(context, includeChildrenRecursively); + } + + includeAsAssignmentTarget( + context: InclusionContext, + includeChildrenRecursively: IncludeChildren, + deoptimizeAccess: boolean + ): void { + if (!this.assignmentDeoptimized) this.applyAssignmentDeoptimization(); + if (deoptimizeAccess) { + this.include(context, includeChildrenRecursively); + } else { + this.includeProperties(context, includeChildrenRecursively); } - this.object.include(context, includeChildrenRecursively); - this.property.include(context, includeChildrenRecursively); } includeCallArguments( @@ -301,6 +294,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE initialise(): void { this.propertyKey = getResolvablePropertyKey(this); + this.accessInteraction = { thisArg: this.object, type: INTERACTION_ACCESSED }; } render( @@ -331,6 +325,14 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE } } + setAssignedValue(value: ExpressionEntity) { + this.assignmentInteraction = { + args: [value], + thisArg: this.object, + type: INTERACTION_ASSIGNED + }; + } + protected applyDeoptimizations(): void { this.deoptimized = true; const { propertyReadSideEffects } = this.context.options @@ -341,23 +343,31 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE propertyReadSideEffects && !(this.variable || this.replacement) ) { - // Regular Assignments do not access the property before assigning - if (!(this.parent instanceof AssignmentExpression && this.parent.operator === '=')) { - this.object.deoptimizeThisOnEventAtPath( - EVENT_ACCESSED, - [this.propertyKey!], - this.object, - SHARED_RECURSION_TRACKER - ); - } - if (this.parent instanceof AssignmentExpression) { - this.object.deoptimizeThisOnEventAtPath( - EVENT_ASSIGNED, - [this.propertyKey!], - this.object, - SHARED_RECURSION_TRACKER - ); - } + const propertyKey = this.getPropertyKey(); + this.object.deoptimizeThisOnInteractionAtPath( + this.accessInteraction, + [propertyKey], + SHARED_RECURSION_TRACKER + ); + this.context.requestTreeshakingPass(); + } + } + + private applyAssignmentDeoptimization(): void { + this.assignmentDeoptimized = true; + const { propertyReadSideEffects } = this.context.options + .treeshake as NormalizedTreeshakingOptions; + if ( + // Namespaces are not bound and should not be deoptimized + this.bound && + propertyReadSideEffects && + !(this.variable || this.replacement) + ) { + this.object.deoptimizeThisOnInteractionAtPath( + this.assignmentInteraction, + [this.getPropertyKey()], + SHARED_RECURSION_TRACKER + ); this.context.requestTreeshakingPass(); } } @@ -389,29 +399,59 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE return this.propertyKey; } - private resolveNamespaceVariables( - baseVariable: Variable, - path: PathWithPositions - ): Variable | string | null { - if (path.length === 0) return baseVariable; - if (!baseVariable.isNamespace || baseVariable instanceof ExternalVariable) return null; - const exportName = path[0].key; - const variable = (baseVariable as NamespaceVariable).context.traceExport(exportName); - if (!variable) { - const fileName = (baseVariable as NamespaceVariable).context.fileName; - this.context.warn( - { - code: 'MISSING_EXPORT', - exporter: relativeId(fileName), - importer: relativeId(this.context.fileName), - message: `'${exportName}' is not exported by '${relativeId(fileName)}'`, - missing: exportName, - url: `https://rollupjs.org/guide/en/#error-name-is-not-exported-by-module` - }, - path[0].pos - ); - return 'undefined'; + private hasAccessEffect(context: HasEffectsContext) { + const { propertyReadSideEffects } = this.context.options + .treeshake as NormalizedTreeshakingOptions; + return ( + !(this.variable || this.replacement) && + propertyReadSideEffects && + (propertyReadSideEffects === 'always' || + this.object.hasEffectsOnInteractionAtPath( + [this.getPropertyKey()], + this.accessInteraction, + context + )) + ); + } + + private includeProperties( + context: InclusionContext, + includeChildrenRecursively: IncludeChildren + ) { + if (!this.included) { + this.included = true; + if (this.variable) { + this.context.includeVariableInModule(this.variable); + } } - return this.resolveNamespaceVariables(variable, path.slice(1)); + this.object.include(context, includeChildrenRecursively); + this.property.include(context, includeChildrenRecursively); + } +} + +function resolveNamespaceVariables( + baseVariable: Variable, + path: PathWithPositions, + astContext: AstContext +): Variable | string | null { + if (path.length === 0) return baseVariable; + if (!baseVariable.isNamespace || baseVariable instanceof ExternalVariable) return null; + const exportName = path[0].key; + const variable = (baseVariable as NamespaceVariable).context.traceExport(exportName); + if (!variable) { + const fileName = (baseVariable as NamespaceVariable).context.fileName; + astContext.warn( + { + code: 'MISSING_EXPORT', + exporter: relativeId(fileName), + importer: relativeId(astContext.fileName), + message: `'${exportName}' is not exported by '${relativeId(fileName)}'`, + missing: exportName, + url: `https://rollupjs.org/guide/en/#error-name-is-not-exported-by-module` + }, + path[0].pos + ); + return 'undefined'; } + return resolveNamespaceVariables(variable, path.slice(1), astContext); } diff --git a/src/ast/nodes/MetaProperty.ts b/src/ast/nodes/MetaProperty.ts index 5ec993c9bc0..9b0c8a5f729 100644 --- a/src/ast/nodes/MetaProperty.ts +++ b/src/ast/nodes/MetaProperty.ts @@ -4,8 +4,9 @@ import type { PluginDriver } from '../../utils/PluginDriver'; import { warnDeprecation } from '../../utils/error'; import type { GenerateCodeSnippets } from '../../utils/generateCodeSnippets'; import { dirname, normalize, relative } from '../../utils/path'; +import { INTERACTION_ACCESSED, NodeInteraction } from '../NodeInteractions'; import type ChildScope from '../scopes/ChildScope'; -import type { ObjectPathKey } from '../utils/PathTracker'; +import type { ObjectPath } from '../utils/PathTracker'; import type Identifier from './Identifier'; import MemberExpression from './MemberExpression'; import type * as NodeType from './NodeType'; @@ -52,8 +53,8 @@ export default class MetaProperty extends NodeBase { return false; } - hasEffectsWhenAccessedAtPath(path: readonly ObjectPathKey[]): boolean { - return path.length > 1; + hasEffectsOnInteractionAtPath(path: ObjectPath, { type }: NodeInteraction): boolean { + return path.length > 1 || type !== INTERACTION_ACCESSED; } include(): void { diff --git a/src/ast/nodes/NewExpression.ts b/src/ast/nodes/NewExpression.ts index 7f395041ff8..e12915475e5 100644 --- a/src/ast/nodes/NewExpression.ts +++ b/src/ast/nodes/NewExpression.ts @@ -2,9 +2,14 @@ import MagicString from 'magic-string'; import type { NormalizedTreeshakingOptions } from '../../rollup/types'; import { renderCallArguments } from '../../utils/renderCallArguments'; import { RenderOptions } from '../../utils/renderHelpers'; -import type { CallOptions } from '../CallOptions'; import type { HasEffectsContext } from '../ExecutionContext'; import { InclusionContext } from '../ExecutionContext'; +import { + INTERACTION_ACCESSED, + INTERACTION_CALLED, + NodeInteraction, + NodeInteractionCalled +} from '../NodeInteractions'; import { EMPTY_PATH, type ObjectPath, UNKNOWN_PATH } from '../utils/PathTracker'; import type * as NodeType from './NodeType'; import { type ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; @@ -13,7 +18,7 @@ export default class NewExpression extends NodeBase { declare arguments: ExpressionNode[]; declare callee: ExpressionNode; declare type: NodeType.tNewExpression; - private declare callOptions: CallOptions; + private declare interaction: NodeInteractionCalled; hasEffects(context: HasEffectsContext): boolean { try { @@ -27,15 +32,15 @@ export default class NewExpression extends NodeBase { return false; return ( this.callee.hasEffects(context) || - this.callee.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.callOptions, context) + this.callee.hasEffectsOnInteractionAtPath(EMPTY_PATH, this.interaction, context) ); } finally { if (!this.deoptimized) this.applyDeoptimizations(); } } - hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean { - return path.length > 0; + hasEffectsOnInteractionAtPath(path: ObjectPath, { type }: NodeInteraction): boolean { + return path.length > 0 || type !== INTERACTION_ACCESSED; } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { @@ -50,9 +55,10 @@ export default class NewExpression extends NodeBase { } initialise(): void { - this.callOptions = { + this.interaction = { args: this.arguments, - thisParam: null, + thisArg: null, + type: INTERACTION_CALLED, withNew: true }; } diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index 8f956916205..eeb273ce656 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -1,10 +1,10 @@ import type MagicString from 'magic-string'; 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 { NodeEvent } from '../NodeEvents'; +import type { NodeInteractionWithThisArg } from '../NodeInteractions'; +import { NodeInteraction, NodeInteractionCalled } from '../NodeInteractions'; import { EMPTY_PATH, type ObjectPath, @@ -35,18 +35,12 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE this.getObjectEntity().deoptimizePath(path); } - deoptimizeThisOnEventAtPath( - event: NodeEvent, + deoptimizeThisOnInteractionAtPath( + interaction: NodeInteractionWithThisArg, path: ObjectPath, - thisParameter: ExpressionEntity, recursionTracker: PathTracker ): void { - this.getObjectEntity().deoptimizeThisOnEventAtPath( - event, - path, - thisParameter, - recursionTracker - ); + this.getObjectEntity().deoptimizeThisOnInteractionAtPath(interaction, path, recursionTracker); } getLiteralValueAtPath( @@ -59,32 +53,24 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE getReturnExpressionWhenCalledAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteractionCalled, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { return this.getObjectEntity().getReturnExpressionWhenCalledAtPath( path, - callOptions, + interaction, recursionTracker, origin ); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return this.getObjectEntity().hasEffectsWhenAccessedAtPath(path, context); - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return this.getObjectEntity().hasEffectsWhenAssignedAtPath(path, context); - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - return this.getObjectEntity().hasEffectsWhenCalledAtPath(path, callOptions, context); + return this.getObjectEntity().hasEffectsOnInteractionAtPath(path, interaction, context); } render( diff --git a/src/ast/nodes/ObjectPattern.ts b/src/ast/nodes/ObjectPattern.ts index af21af5ec27..42fa761eba4 100644 --- a/src/ast/nodes/ObjectPattern.ts +++ b/src/ast/nodes/ObjectPattern.ts @@ -1,4 +1,5 @@ import type { HasEffectsContext } from '../ExecutionContext'; +import { NodeInteractionAssigned } from '../NodeInteractions'; import { EMPTY_PATH, type ObjectPath } from '../utils/PathTracker'; import type LocalVariable from '../variables/LocalVariable'; import type Variable from '../variables/Variable'; @@ -45,10 +46,15 @@ export default class ObjectPattern extends NodeBase implements PatternNode { } } - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length > 0) return true; + hasEffectsOnInteractionAtPath( + // At the moment, this is only triggered for assignment left-hand sides, + // where the path is empty + _path: ObjectPath, + interaction: NodeInteractionAssigned, + context: HasEffectsContext + ): boolean { for (const property of this.properties) { - if (property.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context)) return true; + if (property.hasEffectsOnInteractionAtPath(EMPTY_PATH, interaction, context)) return true; } return false; } diff --git a/src/ast/nodes/PropertyDefinition.ts b/src/ast/nodes/PropertyDefinition.ts index 136d070ddaf..0318e2f0eb1 100644 --- a/src/ast/nodes/PropertyDefinition.ts +++ b/src/ast/nodes/PropertyDefinition.ts @@ -1,7 +1,7 @@ -import type { CallOptions } from '../CallOptions'; import type { DeoptimizableEntity } from '../DeoptimizableEntity'; import type { HasEffectsContext } from '../ExecutionContext'; -import type { NodeEvent } from '../NodeEvents'; +import type { NodeInteractionWithThisArg } from '../NodeInteractions'; +import { NodeInteraction, NodeInteractionCalled } from '../NodeInteractions'; import type { ObjectPath, PathTracker } from '../utils/PathTracker'; import type * as NodeType from './NodeType'; import type PrivateIdentifier from './PrivateIdentifier'; @@ -24,13 +24,12 @@ export default class PropertyDefinition extends NodeBase { this.value?.deoptimizePath(path); } - deoptimizeThisOnEventAtPath( - event: NodeEvent, + deoptimizeThisOnInteractionAtPath( + interaction: NodeInteractionWithThisArg, path: ObjectPath, - thisParameter: ExpressionEntity, recursionTracker: PathTracker ): void { - this.value?.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + this.value?.deoptimizeThisOnInteractionAtPath(interaction, path, recursionTracker); } getLiteralValueAtPath( @@ -45,12 +44,12 @@ export default class PropertyDefinition extends NodeBase { getReturnExpressionWhenCalledAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteractionCalled, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { return this.value - ? this.value.getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin) + ? this.value.getReturnExpressionWhenCalledAtPath(path, interaction, recursionTracker, origin) : UNKNOWN_EXPRESSION; } @@ -58,20 +57,12 @@ export default class PropertyDefinition extends NodeBase { return this.key.hasEffects(context) || (this.static && !!this.value?.hasEffects(context)); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return !this.value || this.value.hasEffectsWhenAccessedAtPath(path, context); - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return !this.value || this.value.hasEffectsWhenAssignedAtPath(path, context); - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - return !this.value || this.value.hasEffectsWhenCalledAtPath(path, callOptions, context); + return !this.value || this.value.hasEffectsOnInteractionAtPath(path, interaction, context); } protected applyDeoptimizations() {} diff --git a/src/ast/nodes/RestElement.ts b/src/ast/nodes/RestElement.ts index b83fada446a..baad03a00c3 100644 --- a/src/ast/nodes/RestElement.ts +++ b/src/ast/nodes/RestElement.ts @@ -1,4 +1,5 @@ import type { HasEffectsContext } from '../ExecutionContext'; +import { NodeInteractionAssigned } from '../NodeInteractions'; import { EMPTY_PATH, type ObjectPath, UnknownKey } from '../utils/PathTracker'; import type LocalVariable from '../variables/LocalVariable'; import type Variable from '../variables/Variable'; @@ -28,8 +29,15 @@ export default class RestElement extends NodeBase implements PatternNode { path.length === 0 && this.argument.deoptimizePath(EMPTY_PATH); } - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return path.length > 0 || this.argument.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context); + hasEffectsOnInteractionAtPath( + path: ObjectPath, + interaction: NodeInteractionAssigned, + context: HasEffectsContext + ): boolean { + return ( + path.length > 0 || + this.argument.hasEffectsOnInteractionAtPath(EMPTY_PATH, interaction, context) + ); } markDeclarationReached(): void { diff --git a/src/ast/nodes/SequenceExpression.ts b/src/ast/nodes/SequenceExpression.ts index 1eaca85ad4b..b2f5a1b48e4 100644 --- a/src/ast/nodes/SequenceExpression.ts +++ b/src/ast/nodes/SequenceExpression.ts @@ -7,14 +7,14 @@ import { type RenderOptions } from '../../utils/renderHelpers'; import { treeshakeNode } from '../../utils/treeshakeNode'; -import type { CallOptions } from '../CallOptions'; import type { DeoptimizableEntity } from '../DeoptimizableEntity'; import type { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import type { NodeEvent } from '../NodeEvents'; +import type { NodeInteractionWithThisArg } from '../NodeInteractions'; +import { NodeInteraction } from '../NodeInteractions'; import type { ObjectPath, PathTracker } from '../utils/PathTracker'; import ExpressionStatement from './ExpressionStatement'; import type * as NodeType from './NodeType'; -import type { ExpressionEntity, LiteralValueOrUnknown } from './shared/Expression'; +import type { LiteralValueOrUnknown } from './shared/Expression'; import { type ExpressionNode, type IncludeChildren, NodeBase } from './shared/Node'; export default class SequenceExpression extends NodeBase { @@ -25,16 +25,14 @@ export default class SequenceExpression extends NodeBase { this.expressions[this.expressions.length - 1].deoptimizePath(path); } - deoptimizeThisOnEventAtPath( - event: NodeEvent, + deoptimizeThisOnInteractionAtPath( + interaction: NodeInteractionWithThisArg, path: ObjectPath, - thisParameter: ExpressionEntity, recursionTracker: PathTracker ): void { - this.expressions[this.expressions.length - 1].deoptimizeThisOnEventAtPath( - event, + this.expressions[this.expressions.length - 1].deoptimizeThisOnInteractionAtPath( + interaction, path, - thisParameter, recursionTracker ); } @@ -58,28 +56,14 @@ export default class SequenceExpression extends NodeBase { return false; } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return ( - path.length > 0 && - this.expressions[this.expressions.length - 1].hasEffectsWhenAccessedAtPath(path, context) - ); - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return this.expressions[this.expressions.length - 1].hasEffectsWhenAssignedAtPath( - path, - context - ); - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - return this.expressions[this.expressions.length - 1].hasEffectsWhenCalledAtPath( + return this.expressions[this.expressions.length - 1].hasEffectsOnInteractionAtPath( path, - callOptions, + interaction, context ); } diff --git a/src/ast/nodes/SpreadElement.ts b/src/ast/nodes/SpreadElement.ts index b6f5849fdbc..4a863c1b785 100644 --- a/src/ast/nodes/SpreadElement.ts +++ b/src/ast/nodes/SpreadElement.ts @@ -1,26 +1,24 @@ import type { NormalizedTreeshakingOptions } from '../../rollup/types'; import type { HasEffectsContext } from '../ExecutionContext'; -import type { NodeEvent } from '../NodeEvents'; +import type { NodeInteractionWithThisArg } from '../NodeInteractions'; +import { NODE_INTERACTION_UNKNOWN_ACCESS } from '../NodeInteractions'; import { type ObjectPath, type PathTracker, UNKNOWN_PATH, UnknownKey } from '../utils/PathTracker'; import type * as NodeType from './NodeType'; -import type { ExpressionEntity } from './shared/Expression'; import { type ExpressionNode, NodeBase } from './shared/Node'; export default class SpreadElement extends NodeBase { declare argument: ExpressionNode; declare type: NodeType.tSpreadElement; - deoptimizeThisOnEventAtPath( - event: NodeEvent, + deoptimizeThisOnInteractionAtPath( + interaction: NodeInteractionWithThisArg, path: ObjectPath, - thisParameter: ExpressionEntity, recursionTracker: PathTracker ): void { if (path.length > 0) { - this.argument.deoptimizeThisOnEventAtPath( - event, + this.argument.deoptimizeThisOnInteractionAtPath( + interaction, [UnknownKey, ...path], - thisParameter, recursionTracker ); } @@ -34,7 +32,11 @@ export default class SpreadElement extends NodeBase { this.argument.hasEffects(context) || (propertyReadSideEffects && (propertyReadSideEffects === 'always' || - this.argument.hasEffectsWhenAccessedAtPath(UNKNOWN_PATH, context))) + this.argument.hasEffectsOnInteractionAtPath( + UNKNOWN_PATH, + NODE_INTERACTION_UNKNOWN_ACCESS, + context + ))) ); } diff --git a/src/ast/nodes/Super.ts b/src/ast/nodes/Super.ts index d04da03870f..dcaee1c292c 100644 --- a/src/ast/nodes/Super.ts +++ b/src/ast/nodes/Super.ts @@ -1,9 +1,8 @@ -import { NodeEvent } from '../NodeEvents'; +import { NodeInteractionWithThisArg } from '../NodeInteractions'; import type { ObjectPath } from '../utils/PathTracker'; 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 { @@ -18,13 +17,12 @@ export default class Super extends NodeBase { this.variable.deoptimizePath(path); } - deoptimizeThisOnEventAtPath( - event: NodeEvent, + deoptimizeThisOnInteractionAtPath( + interaction: NodeInteractionWithThisArg, path: ObjectPath, - thisParameter: ExpressionEntity, recursionTracker: PathTracker ) { - this.variable.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + this.variable.deoptimizeThisOnInteractionAtPath(interaction, path, recursionTracker); } include(): void { diff --git a/src/ast/nodes/TaggedTemplateExpression.ts b/src/ast/nodes/TaggedTemplateExpression.ts index 3a661c29848..10a7dfda304 100644 --- a/src/ast/nodes/TaggedTemplateExpression.ts +++ b/src/ast/nodes/TaggedTemplateExpression.ts @@ -2,7 +2,7 @@ import type MagicString from 'magic-string'; import { type RenderOptions } from '../../utils/renderHelpers'; import type { HasEffectsContext } from '../ExecutionContext'; import { InclusionContext } from '../ExecutionContext'; -import { EVENT_CALLED } from '../NodeEvents'; +import { INTERACTION_CALLED, NodeInteractionWithThisArg } from '../NodeInteractions'; import { EMPTY_PATH, PathTracker, @@ -47,7 +47,7 @@ export default class TaggedTemplateExpression extends CallExpressionBase { } return ( this.tag.hasEffects(context) || - this.tag.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.callOptions, context) + this.tag.hasEffectsOnInteractionAtPath(EMPTY_PATH, this.interaction, context) ); } finally { if (!this.deoptimized) this.applyDeoptimizations(); @@ -63,7 +63,7 @@ export default class TaggedTemplateExpression extends CallExpressionBase { this.tag.include(context, includeChildrenRecursively); this.quasi.include(context, includeChildrenRecursively); } - this.tag.includeCallArguments(context, this.callOptions.args); + this.tag.includeCallArguments(context, this.interaction.args); const returnExpression = this.getReturnExpression(); if (!returnExpression.included) { returnExpression.include(context, false); @@ -71,10 +71,10 @@ export default class TaggedTemplateExpression extends CallExpressionBase { } initialise(): void { - this.callOptions = { + this.interaction = { args: [UNKNOWN_EXPRESSION, ...this.quasi.expressions], - thisParam: - this.tag instanceof MemberExpression && !this.tag.variable ? this.tag.object : null, + thisArg: this.tag instanceof MemberExpression && !this.tag.variable ? this.tag.object : null, + type: INTERACTION_CALLED, withNew: false }; } @@ -86,12 +86,10 @@ export default class TaggedTemplateExpression extends CallExpressionBase { protected applyDeoptimizations(): void { this.deoptimized = true; - const { thisParam } = this.callOptions; - if (thisParam) { - this.tag.deoptimizeThisOnEventAtPath( - EVENT_CALLED, + if (this.interaction.thisArg) { + this.tag.deoptimizeThisOnInteractionAtPath( + this.interaction as NodeInteractionWithThisArg, EMPTY_PATH, - thisParam, SHARED_RECURSION_TRACKER ); } @@ -109,7 +107,7 @@ export default class TaggedTemplateExpression extends CallExpressionBase { this.returnExpression = UNKNOWN_EXPRESSION; return (this.returnExpression = this.tag.getReturnExpressionWhenCalledAtPath( EMPTY_PATH, - this.callOptions, + this.interaction, recursionTracker, this )); diff --git a/src/ast/nodes/TemplateLiteral.ts b/src/ast/nodes/TemplateLiteral.ts index 85ac5537ae7..098ea369724 100644 --- a/src/ast/nodes/TemplateLiteral.ts +++ b/src/ast/nodes/TemplateLiteral.ts @@ -1,7 +1,7 @@ import type MagicString from 'magic-string'; import type { RenderOptions } from '../../utils/renderHelpers'; -import { CallOptions } from '../CallOptions'; import { HasEffectsContext } from '../ExecutionContext'; +import { INTERACTION_ACCESSED, INTERACTION_CALLED, NodeInteraction } from '../NodeInteractions'; import type { ObjectPath } from '../utils/PathTracker'; import { getMemberReturnExpressionWhenCalled, @@ -23,7 +23,7 @@ export default class TemplateLiteral extends NodeBase { declare quasis: TemplateElement[]; declare type: NodeType.tTemplateLiteral; - deoptimizeThisOnEventAtPath(): void {} + deoptimizeThisOnInteractionAtPath(): void {} getLiteralValueAtPath(path: ObjectPath): LiteralValueOrUnknown { if (path.length > 0 || this.quasis.length !== 1) { @@ -39,17 +39,16 @@ export default class TemplateLiteral extends NodeBase { return getMemberReturnExpressionWhenCalled(literalStringMembers, path[0]); } - hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean { - return path.length > 1; - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - if (path.length === 1) { - return hasMemberEffectWhenCalled(literalStringMembers, path[0], callOptions, context); + if (interaction.type === INTERACTION_ACCESSED) { + return path.length > 1; + } + if (interaction.type === INTERACTION_CALLED && path.length === 1) { + return hasMemberEffectWhenCalled(literalStringMembers, path[0], interaction, context); } return true; } diff --git a/src/ast/nodes/ThisExpression.ts b/src/ast/nodes/ThisExpression.ts index 801b11de75e..0ae54d12be1 100644 --- a/src/ast/nodes/ThisExpression.ts +++ b/src/ast/nodes/ThisExpression.ts @@ -1,11 +1,11 @@ import type MagicString from 'magic-string'; import type { HasEffectsContext } from '../ExecutionContext'; -import type { NodeEvent } from '../NodeEvents'; +import type { NodeInteraction, NodeInteractionWithThisArg } from '../NodeInteractions'; +import { INTERACTION_ACCESSED } from '../NodeInteractions'; import ModuleScope from '../scopes/ModuleScope'; import type { ObjectPath, PathTracker } from '../utils/PathTracker'; import type Variable from '../variables/Variable'; import type * as NodeType from './NodeType'; -import type { ExpressionEntity } from './shared/Expression'; import { NodeBase } from './shared/Node'; export default class ThisExpression extends NodeBase { @@ -21,27 +21,28 @@ export default class ThisExpression extends NodeBase { this.variable.deoptimizePath(path); } - deoptimizeThisOnEventAtPath( - event: NodeEvent, + deoptimizeThisOnInteractionAtPath( + interaction: NodeInteractionWithThisArg, path: ObjectPath, - thisParameter: ExpressionEntity, recursionTracker: PathTracker ): void { - this.variable.deoptimizeThisOnEventAtPath( - event, + // We rewrite the parameter so that a ThisVariable can detect self-mutations + this.variable.deoptimizeThisOnInteractionAtPath( + interaction.thisArg === this ? { ...interaction, thisArg: this.variable } : interaction, path, - // We rewrite the parameter so that a ThisVariable can detect self-mutations - thisParameter === this ? this.variable : thisParameter, recursionTracker ); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return path.length > 0 && this.variable.hasEffectsWhenAccessedAtPath(path, context); - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return this.variable.hasEffectsWhenAssignedAtPath(path, context); + hasEffectsOnInteractionAtPath( + path: ObjectPath, + interaction: NodeInteraction, + context: HasEffectsContext + ): boolean { + if (path.length === 0) { + return interaction.type !== INTERACTION_ACCESSED; + } + return this.variable.hasEffectsOnInteractionAtPath(path, interaction, context); } include(): void { diff --git a/src/ast/nodes/UnaryExpression.ts b/src/ast/nodes/UnaryExpression.ts index ae732027e02..617458b276b 100644 --- a/src/ast/nodes/UnaryExpression.ts +++ b/src/ast/nodes/UnaryExpression.ts @@ -1,5 +1,10 @@ import type { DeoptimizableEntity } from '../DeoptimizableEntity'; import type { HasEffectsContext } from '../ExecutionContext'; +import { + INTERACTION_ACCESSED, + NODE_INTERACTION_UNKNOWN_ASSIGNMENT, + NodeInteraction +} from '../NodeInteractions'; import { EMPTY_PATH, type ObjectPath, type PathTracker } from '../utils/PathTracker'; import Identifier from './Identifier'; import type { LiteralValue } from './Literal'; @@ -43,15 +48,16 @@ export default class UnaryExpression extends NodeBase { return ( this.argument.hasEffects(context) || (this.operator === 'delete' && - this.argument.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context)) + this.argument.hasEffectsOnInteractionAtPath( + EMPTY_PATH, + NODE_INTERACTION_UNKNOWN_ASSIGNMENT, + context + )) ); } - hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean { - if (this.operator === 'void') { - return path.length > 0; - } - return path.length > 1; + hasEffectsOnInteractionAtPath(path: ObjectPath, { type }: NodeInteraction): boolean { + return type !== INTERACTION_ACCESSED || path.length > (this.operator === 'void' ? 0 : 1); } protected applyDeoptimizations(): void { diff --git a/src/ast/nodes/UpdateExpression.ts b/src/ast/nodes/UpdateExpression.ts index 68a4c052718..141768649f6 100644 --- a/src/ast/nodes/UpdateExpression.ts +++ b/src/ast/nodes/UpdateExpression.ts @@ -5,28 +5,42 @@ import { renderSystemExportSequenceAfterExpression, renderSystemExportSequenceBeforeExpression } from '../../utils/systemJsRendering'; -import type { HasEffectsContext } from '../ExecutionContext'; +import type { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { + INTERACTION_ACCESSED, + NodeInteraction, + NodeInteractionAssigned +} from '../NodeInteractions'; import { EMPTY_PATH, type ObjectPath } from '../utils/PathTracker'; import Identifier from './Identifier'; import * as NodeType from './NodeType'; -import { type ExpressionNode, NodeBase } from './shared/Node'; +import { UNKNOWN_EXPRESSION } from './shared/Expression'; +import { type ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; export default class UpdateExpression extends NodeBase { declare argument: ExpressionNode; declare operator: '++' | '--'; declare prefix: boolean; declare type: NodeType.tUpdateExpression; + private declare interaction: NodeInteractionAssigned; hasEffects(context: HasEffectsContext): boolean { if (!this.deoptimized) this.applyDeoptimizations(); - return ( - this.argument.hasEffects(context) || - this.argument.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context) - ); + return this.argument.hasEffectsAsAssignmentTarget(context, true); + } + + hasEffectsOnInteractionAtPath(path: ObjectPath, { type }: NodeInteraction): boolean { + return path.length > 1 || type !== INTERACTION_ACCESSED; + } + + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); + this.included = true; + this.argument.includeAsAssignmentTarget(context, includeChildrenRecursively, true); } - hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean { - return path.length > 1; + initialise() { + this.argument.setAssignedValue(UNKNOWN_EXPRESSION); } render(code: MagicString, options: RenderOptions): void { diff --git a/src/ast/nodes/VariableDeclaration.ts b/src/ast/nodes/VariableDeclaration.ts index cdd0b50578c..81f44d94611 100644 --- a/src/ast/nodes/VariableDeclaration.ts +++ b/src/ast/nodes/VariableDeclaration.ts @@ -49,7 +49,7 @@ export default class VariableDeclaration extends NodeBase { } } - hasEffectsWhenAssignedAtPath(): boolean { + hasEffectsOnInteractionAtPath(): boolean { return false; } diff --git a/src/ast/nodes/shared/CallExpressionBase.ts b/src/ast/nodes/shared/CallExpressionBase.ts index 72524d773cc..ad09dcf8c3e 100644 --- a/src/ast/nodes/shared/CallExpressionBase.ts +++ b/src/ast/nodes/shared/CallExpressionBase.ts @@ -1,7 +1,12 @@ -import type { CallOptions } from '../../CallOptions'; import type { DeoptimizableEntity } from '../../DeoptimizableEntity'; import type { HasEffectsContext } from '../../ExecutionContext'; -import { type NodeEvent } from '../../NodeEvents'; +import { + INTERACTION_ASSIGNED, + INTERACTION_CALLED, + NodeInteraction, + NodeInteractionCalled, + NodeInteractionWithThisArg +} from '../../NodeInteractions'; import { type ObjectPath, type PathTracker, UNKNOWN_PATH } from '../../utils/PathTracker'; import { type ExpressionEntity, @@ -12,7 +17,7 @@ import { import { NodeBase } from './Node'; export default abstract class CallExpressionBase extends NodeBase implements DeoptimizableEntity { - protected declare callOptions: CallOptions; + protected declare interaction: NodeInteractionCalled; protected returnExpression: ExpressionEntity | null = null; private readonly deoptimizableDependentExpressions: DeoptimizableEntity[] = []; private readonly expressionsToBeDeoptimized = new Set(); @@ -42,27 +47,21 @@ export default abstract class CallExpressionBase extends NodeBase implements Deo } } - deoptimizeThisOnEventAtPath( - event: NodeEvent, + deoptimizeThisOnInteractionAtPath( + interaction: NodeInteractionWithThisArg, path: ObjectPath, - thisParameter: ExpressionEntity, recursionTracker: PathTracker ): void { const returnExpression = this.getReturnExpression(recursionTracker); if (returnExpression === UNKNOWN_EXPRESSION) { - thisParameter.deoptimizePath(UNKNOWN_PATH); + interaction.thisArg.deoptimizePath(UNKNOWN_PATH); } else { recursionTracker.withTrackedEntityAtPath( path, returnExpression, () => { - this.expressionsToBeDeoptimized.add(thisParameter); - returnExpression.deoptimizeThisOnEventAtPath( - event, - path, - thisParameter, - recursionTracker - ); + this.expressionsToBeDeoptimized.add(interaction.thisArg); + returnExpression.deoptimizeThisOnInteractionAtPath(interaction, path, recursionTracker); }, undefined ); @@ -91,7 +90,7 @@ export default abstract class CallExpressionBase extends NodeBase implements Deo getReturnExpressionWhenCalledAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteractionCalled, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { @@ -106,7 +105,7 @@ export default abstract class CallExpressionBase extends NodeBase implements Deo this.deoptimizableDependentExpressions.push(origin); return returnExpression.getReturnExpressionWhenCalledAtPath( path, - callOptions, + interaction, recursionTracker, origin ); @@ -115,31 +114,30 @@ export default abstract class CallExpressionBase extends NodeBase implements Deo ); } - 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( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - return ( - !( - callOptions.withNew ? context.instantiated : context.called - ).trackEntityAtPathAndGetIfTracked(path, callOptions, this) && - this.getReturnExpression().hasEffectsWhenCalledAtPath(path, callOptions, context) - ); + const { type } = interaction; + if (type === INTERACTION_CALLED) { + if ( + (interaction.withNew + ? context.instantiated + : context.called + ).trackEntityAtPathAndGetIfTracked(path, interaction.args, this) + ) { + return false; + } + } else if ( + (type === INTERACTION_ASSIGNED + ? context.assigned + : context.accessed + ).trackEntityAtPathAndGetIfTracked(path, this) + ) { + return false; + } + return this.getReturnExpression().hasEffectsOnInteractionAtPath(path, interaction, context); } protected abstract getReturnExpression(recursionTracker?: PathTracker): ExpressionEntity; diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index 5a6ee0e81dd..06f54ee3db7 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -1,7 +1,11 @@ -import type { CallOptions } from '../../CallOptions'; import type { DeoptimizableEntity } from '../../DeoptimizableEntity'; import type { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; -import type { NodeEvent } from '../../NodeEvents'; +import { + INTERACTION_CALLED, + NodeInteraction, + NodeInteractionCalled, + NodeInteractionWithThisArg +} from '../../NodeInteractions'; import ChildScope from '../../scopes/ChildScope'; import type Scope from '../../scopes/Scope'; import { @@ -41,18 +45,12 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { this.getObjectEntity().deoptimizePath(path); } - deoptimizeThisOnEventAtPath( - event: NodeEvent, + deoptimizeThisOnInteractionAtPath( + interaction: NodeInteractionWithThisArg, path: ObjectPath, - thisParameter: ExpressionEntity, recursionTracker: PathTracker ): void { - this.getObjectEntity().deoptimizeThisOnEventAtPath( - event, - path, - thisParameter, - recursionTracker - ); + this.getObjectEntity().deoptimizeThisOnInteractionAtPath(interaction, path, recursionTracker); } getLiteralValueAtPath( @@ -65,13 +63,13 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { getReturnExpressionWhenCalledAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteractionCalled, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { return this.getObjectEntity().getReturnExpressionWhenCalledAtPath( path, - callOptions, + interaction, recursionTracker, origin ); @@ -84,29 +82,21 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { return initEffect || super.hasEffects(context); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return this.getObjectEntity().hasEffectsWhenAccessedAtPath(path, context); - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return this.getObjectEntity().hasEffectsWhenAssignedAtPath(path, context); - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - if (path.length === 0) { + if (interaction.type === INTERACTION_CALLED && path.length === 0) { return ( - !callOptions.withNew || + !interaction.withNew || (this.classConstructor !== null - ? this.classConstructor.hasEffectsWhenCalledAtPath(EMPTY_PATH, callOptions, context) - : this.superClass?.hasEffectsWhenCalledAtPath(path, callOptions, context)) || + ? this.classConstructor.hasEffectsOnInteractionAtPath(path, interaction, context) + : this.superClass?.hasEffectsOnInteractionAtPath(path, interaction, context)) || false ); } else { - return this.getObjectEntity().hasEffectsWhenCalledAtPath(path, callOptions, context); + return this.getObjectEntity().hasEffectsOnInteractionAtPath(path, interaction, context); } } diff --git a/src/ast/nodes/shared/Expression.ts b/src/ast/nodes/shared/Expression.ts index a28e47dd769..1b718c0382a 100644 --- a/src/ast/nodes/shared/Expression.ts +++ b/src/ast/nodes/shared/Expression.ts @@ -1,8 +1,11 @@ -import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { WritableEntity } from '../../Entity'; import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; -import { NodeEvent } from '../../NodeEvents'; +import { + NodeInteraction, + NodeInteractionCalled, + NodeInteractionWithThisArg +} from '../../NodeInteractions'; import { ObjectPath, PathTracker, UNKNOWN_PATH } from '../../utils/PathTracker'; import { LiteralValue } from '../Literal'; import SpreadElement from '../SpreadElement'; @@ -25,13 +28,12 @@ export class ExpressionEntity implements WritableEntity { deoptimizePath(_path: ObjectPath): void {} - deoptimizeThisOnEventAtPath( - _event: NodeEvent, + deoptimizeThisOnInteractionAtPath( + { thisArg }: NodeInteractionWithThisArg, _path: ObjectPath, - thisParameter: ExpressionEntity, _recursionTracker: PathTracker ): void { - thisParameter.deoptimizePath(UNKNOWN_PATH); + thisArg!.deoptimizePath(UNKNOWN_PATH); } /** @@ -49,24 +51,16 @@ export class ExpressionEntity implements WritableEntity { getReturnExpressionWhenCalledAtPath( _path: ObjectPath, - _callOptions: CallOptions, + _interaction: NodeInteractionCalled, _recursionTracker: PathTracker, _origin: DeoptimizableEntity ): ExpressionEntity { return UNKNOWN_EXPRESSION; } - hasEffectsWhenAccessedAtPath(_path: ObjectPath, _context: HasEffectsContext): boolean { - return true; - } - - hasEffectsWhenAssignedAtPath(_path: ObjectPath, _context: HasEffectsContext): boolean { - return true; - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( _path: ObjectPath, - _callOptions: CallOptions, + _interaction: NodeInteraction, _context: HasEffectsContext ): boolean { return true; diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index 05d09d3ac47..a1517cb7e08 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -1,12 +1,18 @@ import type { NormalizedTreeshakingOptions } from '../../../rollup/types'; -import { type CallOptions, NO_ARGS } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { BROKEN_FLOW_NONE, type HasEffectsContext, type InclusionContext } from '../../ExecutionContext'; -import { NodeEvent } from '../../NodeEvents'; +import { + INTERACTION_CALLED, + NODE_INTERACTION_UNKNOWN_ACCESS, + NODE_INTERACTION_UNKNOWN_CALL, + NodeInteraction, + NodeInteractionCalled, + NodeInteractionWithThisArg +} from '../../NodeInteractions'; import ReturnValueScope from '../../scopes/ReturnValueScope'; import { type ObjectPath, PathTracker, UNKNOWN_PATH, UnknownKey } from '../../utils/PathTracker'; import BlockStatement from '../BlockStatement'; @@ -41,19 +47,13 @@ export default abstract class FunctionBase extends NodeBase { } } - deoptimizeThisOnEventAtPath( - event: NodeEvent, + deoptimizeThisOnInteractionAtPath( + interaction: NodeInteractionWithThisArg, path: ObjectPath, - thisParameter: ExpressionEntity, recursionTracker: PathTracker ): void { if (path.length > 0) { - this.getObjectEntity().deoptimizeThisOnEventAtPath( - event, - path, - thisParameter, - recursionTracker - ); + this.getObjectEntity().deoptimizeThisOnInteractionAtPath(interaction, path, recursionTracker); } } @@ -67,14 +67,14 @@ export default abstract class FunctionBase extends NodeBase { getReturnExpressionWhenCalledAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteractionCalled, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { if (path.length > 0) { return this.getObjectEntity().getReturnExpressionWhenCalledAtPath( path, - callOptions, + interaction, recursionTracker, origin ); @@ -90,35 +90,31 @@ export default abstract class FunctionBase extends NodeBase { return this.scope.getReturnExpression(); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return this.getObjectEntity().hasEffectsWhenAccessedAtPath(path, context); - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return this.getObjectEntity().hasEffectsWhenAssignedAtPath(path, context); - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - if (path.length > 0) { - return this.getObjectEntity().hasEffectsWhenCalledAtPath(path, callOptions, context); + if (path.length > 0 || interaction.type !== INTERACTION_CALLED) { + return this.getObjectEntity().hasEffectsOnInteractionAtPath(path, interaction, context); } if (this.async) { const { propertyReadSideEffects } = this.context.options .treeshake as NormalizedTreeshakingOptions; const returnExpression = this.scope.getReturnExpression(); if ( - returnExpression.hasEffectsWhenCalledAtPath( + returnExpression.hasEffectsOnInteractionAtPath( ['then'], - { args: NO_ARGS, thisParam: null, withNew: false }, + NODE_INTERACTION_UNKNOWN_CALL, context ) || (propertyReadSideEffects && (propertyReadSideEffects === 'always' || - returnExpression.hasEffectsWhenAccessedAtPath(['then'], context))) + returnExpression.hasEffectsOnInteractionAtPath( + ['then'], + NODE_INTERACTION_UNKNOWN_ACCESS, + context + ))) ) { return true; } diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index 95867d14844..e878009f9fc 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -1,11 +1,14 @@ -import { type CallOptions } from '../../CallOptions'; import { type HasEffectsContext, type InclusionContext } from '../../ExecutionContext'; -import { EVENT_CALLED, type NodeEvent } from '../../NodeEvents'; +import { + INTERACTION_CALLED, + NodeInteraction, + NodeInteractionWithThisArg +} from '../../NodeInteractions'; import FunctionScope from '../../scopes/FunctionScope'; import { type ObjectPath, PathTracker } from '../../utils/PathTracker'; import BlockStatement from '../BlockStatement'; import Identifier, { type IdentifierWithVariable } from '../Identifier'; -import { type ExpressionEntity, UNKNOWN_EXPRESSION } from './Expression'; +import { UNKNOWN_EXPRESSION } from './Expression'; import FunctionBase from './FunctionBase'; import { type IncludeChildren } from './Node'; import { ObjectEntity } from './ObjectEntity'; @@ -25,51 +28,52 @@ export default class FunctionNode extends FunctionBase { this.scope = new FunctionScope(parentScope, this.context); } - deoptimizeThisOnEventAtPath( - event: NodeEvent, + deoptimizeThisOnInteractionAtPath( + interaction: NodeInteractionWithThisArg, path: ObjectPath, - thisParameter: ExpressionEntity, recursionTracker: PathTracker ): void { - super.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); - if (event === EVENT_CALLED && path.length === 0) { - this.scope.thisVariable.addEntityToBeDeoptimized(thisParameter); + super.deoptimizeThisOnInteractionAtPath(interaction, path, recursionTracker); + if (interaction.type === INTERACTION_CALLED && path.length === 0) { + this.scope.thisVariable.addEntityToBeDeoptimized(interaction.thisArg); } } - hasEffects(): boolean { + hasEffects(context: HasEffectsContext): boolean { if (!this.deoptimized) this.applyDeoptimizations(); - return !!this.id?.hasEffects(); + return !!this.id?.hasEffects(context); } - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - if (super.hasEffectsWhenCalledAtPath(path, callOptions, context)) return true; - const thisInit = context.replacedVariableInits.get(this.scope.thisVariable); - context.replacedVariableInits.set( - this.scope.thisVariable, - callOptions.withNew - ? new ObjectEntity(Object.create(null), OBJECT_PROTOTYPE) - : UNKNOWN_EXPRESSION - ); - const { brokenFlow, ignore } = context; - context.ignore = { - breaks: false, - continues: false, - labels: new Set(), - returnYield: true - }; - if (this.body.hasEffects(context)) return true; - context.brokenFlow = brokenFlow; - if (thisInit) { - context.replacedVariableInits.set(this.scope.thisVariable, thisInit); - } else { - context.replacedVariableInits.delete(this.scope.thisVariable); + if (super.hasEffectsOnInteractionAtPath(path, interaction, context)) return true; + if (interaction.type === INTERACTION_CALLED) { + const thisInit = context.replacedVariableInits.get(this.scope.thisVariable); + context.replacedVariableInits.set( + this.scope.thisVariable, + interaction.withNew + ? new ObjectEntity(Object.create(null), OBJECT_PROTOTYPE) + : UNKNOWN_EXPRESSION + ); + const { brokenFlow, ignore } = context; + context.ignore = { + breaks: false, + continues: false, + labels: new Set(), + returnYield: true + }; + if (this.body.hasEffects(context)) return true; + context.brokenFlow = brokenFlow; + if (thisInit) { + context.replacedVariableInits.set(this.scope.thisVariable, thisInit); + } else { + context.replacedVariableInits.delete(this.scope.thisVariable); + } + context.ignore = ignore; } - context.ignore = ignore; return false; } diff --git a/src/ast/nodes/shared/MethodBase.ts b/src/ast/nodes/shared/MethodBase.ts index fb606ae81e3..fd76f898643 100644 --- a/src/ast/nodes/shared/MethodBase.ts +++ b/src/ast/nodes/shared/MethodBase.ts @@ -1,7 +1,15 @@ -import { type CallOptions, NO_ARGS } from '../../CallOptions'; import type { DeoptimizableEntity } from '../../DeoptimizableEntity'; import type { HasEffectsContext } from '../../ExecutionContext'; -import { EVENT_ACCESSED, EVENT_ASSIGNED, EVENT_CALLED, type NodeEvent } from '../../NodeEvents'; +import { + INTERACTION_ACCESSED, + INTERACTION_ASSIGNED, + INTERACTION_CALLED, + NO_ARGS, + NODE_INTERACTION_UNKNOWN_CALL, + NodeInteraction, + NodeInteractionCalled, + NodeInteractionWithThisArg +} from '../../NodeInteractions'; import { EMPTY_PATH, type ObjectPath, @@ -24,11 +32,6 @@ export default class MethodBase extends NodeBase implements DeoptimizableEntity declare value: ExpressionNode | (ExpressionNode & PatternNode); private accessedValue: ExpressionEntity | null = null; - private accessorCallOptions: CallOptions = { - args: NO_ARGS, - thisParam: null, - withNew: false - }; // As getter properties directly receive their values from fixed function // expressions, there is no known situation where a getter is deoptimized. @@ -38,34 +41,36 @@ export default class MethodBase extends NodeBase implements DeoptimizableEntity this.getAccessedValue().deoptimizePath(path); } - deoptimizeThisOnEventAtPath( - event: NodeEvent, + deoptimizeThisOnInteractionAtPath( + interaction: NodeInteractionWithThisArg, path: ObjectPath, - thisParameter: ExpressionEntity, recursionTracker: PathTracker ): void { - if (event === EVENT_ACCESSED && this.kind === 'get' && path.length === 0) { - return this.value.deoptimizeThisOnEventAtPath( - EVENT_CALLED, + if (interaction.type === INTERACTION_ACCESSED && this.kind === 'get' && path.length === 0) { + return this.value.deoptimizeThisOnInteractionAtPath( + { + args: NO_ARGS, + thisArg: interaction.thisArg, + type: INTERACTION_CALLED, + withNew: false + }, EMPTY_PATH, - thisParameter, recursionTracker ); } - if (event === EVENT_ASSIGNED && this.kind === 'set' && path.length === 0) { - return this.value.deoptimizeThisOnEventAtPath( - EVENT_CALLED, + if (interaction.type === INTERACTION_ASSIGNED && this.kind === 'set' && path.length === 0) { + return this.value.deoptimizeThisOnInteractionAtPath( + { + args: interaction.args, + thisArg: interaction.thisArg, + type: INTERACTION_CALLED, + withNew: false + }, EMPTY_PATH, - thisParameter, recursionTracker ); } - this.getAccessedValue().deoptimizeThisOnEventAtPath( - event, - path, - thisParameter, - recursionTracker - ); + this.getAccessedValue().deoptimizeThisOnInteractionAtPath(interaction, path, recursionTracker); } getLiteralValueAtPath( @@ -78,13 +83,13 @@ export default class MethodBase extends NodeBase implements DeoptimizableEntity getReturnExpressionWhenCalledAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteractionCalled, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { return this.getAccessedValue().getReturnExpressionWhenCalledAtPath( path, - callOptions, + interaction, recursionTracker, origin ); @@ -94,26 +99,37 @@ export default class MethodBase extends NodeBase implements DeoptimizableEntity return this.key.hasEffects(context); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (this.kind === 'get' && path.length === 0) { - return this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context); - } - return this.getAccessedValue().hasEffectsWhenAccessedAtPath(path, context); - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (this.kind === 'set') { - return this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context); - } - return this.getAccessedValue().hasEffectsWhenAssignedAtPath(path, context); - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - return this.getAccessedValue().hasEffectsWhenCalledAtPath(path, callOptions, context); + if (this.kind === 'get' && interaction.type === INTERACTION_ACCESSED && path.length === 0) { + return this.value.hasEffectsOnInteractionAtPath( + EMPTY_PATH, + { + args: NO_ARGS, + thisArg: interaction.thisArg, + type: INTERACTION_CALLED, + withNew: false + }, + context + ); + } + // setters are only called for empty paths + if (this.kind === 'set' && interaction.type === INTERACTION_ASSIGNED) { + return this.value.hasEffectsOnInteractionAtPath( + EMPTY_PATH, + { + args: interaction.args, + thisArg: interaction.thisArg, + type: INTERACTION_CALLED, + withNew: false + }, + context + ); + } + return this.getAccessedValue().hasEffectsOnInteractionAtPath(path, interaction, context); } protected applyDeoptimizations() {} @@ -124,7 +140,7 @@ export default class MethodBase extends NodeBase implements DeoptimizableEntity this.accessedValue = UNKNOWN_EXPRESSION; return (this.accessedValue = this.value.getReturnExpressionWhenCalledAtPath( EMPTY_PATH, - this.accessorCallOptions, + NODE_INTERACTION_UNKNOWN_CALL, SHARED_RECURSION_TRACKER, this )); diff --git a/src/ast/nodes/shared/MethodTypes.ts b/src/ast/nodes/shared/MethodTypes.ts index f2cb1ea95a5..993ebbc91c1 100644 --- a/src/ast/nodes/shared/MethodTypes.ts +++ b/src/ast/nodes/shared/MethodTypes.ts @@ -1,6 +1,13 @@ -import { type CallOptions, NO_ARGS } from '../../CallOptions'; import type { HasEffectsContext } from '../../ExecutionContext'; -import { EVENT_CALLED, type NodeEvent } from '../../NodeEvents'; +import { + INTERACTION_ACCESSED, + INTERACTION_CALLED, + NODE_INTERACTION_UNKNOWN_ASSIGNMENT, + NODE_INTERACTION_UNKNOWN_CALL, + NodeInteraction, + NodeInteractionCalled, + NodeInteractionWithThisArg +} from '../../NodeInteractions'; import { EMPTY_PATH, type ObjectPath, UNKNOWN_INTEGER_PATH } from '../../utils/PathTracker'; import { UNKNOWN_LITERAL_BOOLEAN, @@ -28,19 +35,18 @@ export class Method extends ExpressionEntity { super(); } - deoptimizeThisOnEventAtPath( - event: NodeEvent, - path: ObjectPath, - thisParameter: ExpressionEntity + deoptimizeThisOnInteractionAtPath( + { type, thisArg }: NodeInteractionWithThisArg, + path: ObjectPath ): void { - if (event === EVENT_CALLED && path.length === 0 && this.description.mutatesSelfAsArray) { - thisParameter.deoptimizePath(UNKNOWN_INTEGER_PATH); + if (type === INTERACTION_CALLED && path.length === 0 && this.description.mutatesSelfAsArray) { + thisArg.deoptimizePath(UNKNOWN_INTEGER_PATH); } } getReturnExpressionWhenCalledAtPath( path: ObjectPath, - callOptions: CallOptions + { thisArg }: NodeInteractionCalled ): ExpressionEntity { if (path.length > 0) { return UNKNOWN_EXPRESSION; @@ -48,48 +54,44 @@ export class Method extends ExpressionEntity { return ( this.description.returnsPrimitive || (this.description.returns === 'self' - ? callOptions.thisParam || UNKNOWN_EXPRESSION + ? thisArg || UNKNOWN_EXPRESSION : this.description.returns()) ); } - hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean { - return path.length > 1; - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath): boolean { - return path.length > 0; - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - if ( - path.length > 0 || - (this.description.mutatesSelfAsArray === true && - callOptions.thisParam?.hasEffectsWhenAssignedAtPath(UNKNOWN_INTEGER_PATH, context)) - ) { + const { type } = interaction; + if (path.length > (type === INTERACTION_ACCESSED ? 1 : 0)) { return true; } - if (!this.description.callsArgs) { - return false; - } - for (const argIndex of this.description.callsArgs) { + if (type === INTERACTION_CALLED) { if ( - callOptions.args[argIndex]?.hasEffectsWhenCalledAtPath( - EMPTY_PATH, - { - args: NO_ARGS, - thisParam: null, - withNew: false - }, + this.description.mutatesSelfAsArray === true && + interaction.thisArg?.hasEffectsOnInteractionAtPath( + UNKNOWN_INTEGER_PATH, + NODE_INTERACTION_UNKNOWN_ASSIGNMENT, context ) ) { return true; } + if (this.description.callsArgs) { + for (const argIndex of this.description.callsArgs) { + if ( + interaction.args[argIndex]?.hasEffectsOnInteractionAtPath( + EMPTY_PATH, + NODE_INTERACTION_UNKNOWN_CALL, + context + ) + ) { + return true; + } + } + } } return false; } diff --git a/src/ast/nodes/shared/MultiExpression.ts b/src/ast/nodes/shared/MultiExpression.ts index 40dd5df76d2..7497a8f914e 100644 --- a/src/ast/nodes/shared/MultiExpression.ts +++ b/src/ast/nodes/shared/MultiExpression.ts @@ -1,6 +1,6 @@ -import type { CallOptions } from '../../CallOptions'; import type { DeoptimizableEntity } from '../../DeoptimizableEntity'; import type { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; +import { NodeInteraction, NodeInteractionCalled } from '../../NodeInteractions'; import type { ObjectPath, PathTracker } from '../../utils/PathTracker'; import { ExpressionEntity } from './Expression'; import type { IncludeChildren } from './Node'; @@ -20,38 +20,24 @@ export class MultiExpression extends ExpressionEntity { getReturnExpressionWhenCalledAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteractionCalled, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { return new MultiExpression( this.expressions.map(expression => - expression.getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin) + expression.getReturnExpressionWhenCalledAtPath(path, interaction, recursionTracker, origin) ) ); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - for (const expression of this.expressions) { - if (expression.hasEffectsWhenAccessedAtPath(path, context)) return true; - } - return false; - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - for (const expression of this.expressions) { - if (expression.hasEffectsWhenAssignedAtPath(path, context)) return true; - } - return false; - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { for (const expression of this.expressions) { - if (expression.hasEffectsWhenCalledAtPath(path, callOptions, context)) return true; + if (expression.hasEffectsOnInteractionAtPath(path, interaction, context)) return true; } return false; } diff --git a/src/ast/nodes/shared/Node.ts b/src/ast/nodes/shared/Node.ts index cabbc7f0813..cb5337284f5 100644 --- a/src/ast/nodes/shared/Node.ts +++ b/src/ast/nodes/shared/Node.ts @@ -10,9 +10,10 @@ import { type HasEffectsContext, type InclusionContext } from '../../ExecutionContext'; +import { INTERACTION_ASSIGNED, NodeInteractionAssigned } from '../../NodeInteractions'; import { getAndCreateKeys, keys } from '../../keys'; import type ChildScope from '../../scopes/ChildScope'; -import { UNKNOWN_PATH } from '../../utils/PathTracker'; +import { EMPTY_PATH, UNKNOWN_PATH } from '../../utils/PathTracker'; import type Variable from '../../variables/Variable'; import * as NodeType from '../NodeType'; import { ExpressionEntity, InclusionOptions } from './Expression'; @@ -44,22 +45,32 @@ export interface Node extends Entity { ): void; /** - * Called once all nodes have been initialised and the scopes have been populated. + * Called once all nodes have been initialised and the scopes have been + * populated. */ bind(): void; /** - * Determine if this Node would have an effect on the bundle. - * This is usually true for already included nodes. Exceptions are e.g. break statements - * which only have an effect if their surrounding loop or switch statement is included. + * Determine if this Node would have an effect on the bundle. This is usually + * true for already included nodes. Exceptions are e.g. break statements 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; /** - * 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. + * Special version of hasEffects for assignment left-hand sides which ensures + * that accessor effects are checked as well. This is necessary to do from the + * child so that member expressions can use the correct thisArg value. + * setAssignedValue needs to be called during initialise to use this. + */ + hasEffectsAsAssignmentTarget(context: HasEffectsContext, checkAccess: boolean): boolean; + + /** + * 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, @@ -67,13 +78,33 @@ export interface Node extends Entity { options?: InclusionOptions ): void; + /** + * Special version of include for assignment left-hand sides which ensures + * that accessors are handled correctly. This is necessary to do from the + * child so that member expressions can use the correct thisArg value. + * setAssignedValue needs to be called during initialise to use this. + */ + includeAsAssignmentTarget( + context: InclusionContext, + includeChildrenRecursively: IncludeChildren, + deoptimizeAccess: boolean + ): void; + render(code: MagicString, options: RenderOptions, nodeRenderOptions?: NodeRenderOptions): void; /** - * Start a new execution path to determine if this node has an effect on the bundle and - * should therefore be included. Included nodes should always be included again in subsequent - * visits as the inclusion of additional variables may require the inclusion of more child - * nodes in e.g. block statements. + * Sets the assigned value e.g. for assignment expression left. This must be + * called during initialise in case hasEffects/includeAsAssignmentTarget are + * used. + */ + setAssignedValue(value: ExpressionEntity): void; + + /** + * Start a new execution path to determine if this node has an effect on the + * bundle and should therefore be included. Included nodes should always be + * included again in subsequent visits as the inclusion of additional + * variables may require the inclusion of more child nodes in e.g. block + * statements. */ shouldBeIncluded(context: InclusionContext): boolean; } @@ -92,10 +123,16 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { declare scope: ChildScope; declare start: number; declare type: keyof typeof NodeType; - // Nodes can apply custom deoptimizations once they become part of the - // executed code. To do this, they must initialize this as false, implement - // applyDeoptimizations and call this from include and hasEffects if they - // have custom handlers + /** + * This will be populated during initialise if setAssignedValue is called. + */ + protected declare assignmentInteraction: NodeInteractionAssigned; + /** + * Nodes can apply custom deoptimizations once they become part of the + * executed code. To do this, they must initialize this as false, implement + * applyDeoptimizations and call this from include and hasEffects if they have + * custom handlers + */ protected deoptimized = false; constructor( @@ -159,6 +196,13 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { return false; } + hasEffectsAsAssignmentTarget(context: HasEffectsContext, _checkAccess: boolean): boolean { + return ( + this.hasEffects(context) || + this.hasEffectsOnInteractionAtPath(EMPTY_PATH, this.assignmentInteraction, context) + ); + } + include( context: InclusionContext, includeChildrenRecursively: IncludeChildren, @@ -179,6 +223,14 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { } } + includeAsAssignmentTarget( + context: InclusionContext, + includeChildrenRecursively: IncludeChildren, + _deoptimizeAccess: boolean + ) { + this.include(context, includeChildrenRecursively); + } + /** * Override to perform special initialisation steps after the scope is initialised */ @@ -236,6 +288,10 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { } } + setAssignedValue(value: ExpressionEntity): void { + this.assignmentInteraction = { args: [value], thisArg: null, type: INTERACTION_ASSIGNED }; + } + shouldBeIncluded(context: InclusionContext): boolean { return this.included || (!context.brokenFlow && this.hasEffects(createHasEffectsContext())); } diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts index c1eb584b80a..efbc2026427 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -1,7 +1,12 @@ -import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; -import { EVENT_ACCESSED, EVENT_CALLED, NodeEvent } from '../../NodeEvents'; +import { + INTERACTION_ACCESSED, + INTERACTION_CALLED, + NodeInteraction, + NodeInteractionCalled, + NodeInteractionWithThisArg +} from '../../NodeInteractions'; import { ObjectPath, ObjectPathKey, @@ -146,10 +151,9 @@ export class ObjectEntity extends ExpressionEntity { this.prototypeExpression?.deoptimizePath(path.length === 1 ? [...path, UnknownKey] : path); } - deoptimizeThisOnEventAtPath( - event: NodeEvent, + deoptimizeThisOnInteractionAtPath( + interaction: NodeInteractionWithThisArg, path: ObjectPath, - thisParameter: ExpressionEntity, recursionTracker: PathTracker ): void { const [key, ...subPath] = path; @@ -157,22 +161,22 @@ export class ObjectEntity extends ExpressionEntity { if ( this.hasLostTrack || // single paths that are deoptimized will not become getters or setters - ((event === EVENT_CALLED || path.length > 1) && + ((interaction.type === INTERACTION_CALLED || path.length > 1) && (this.hasUnknownDeoptimizedProperty || (typeof key === 'string' && this.deoptimizedPaths[key]))) ) { - thisParameter.deoptimizePath(UNKNOWN_PATH); + interaction.thisArg.deoptimizePath(UNKNOWN_PATH); return; } const [propertiesForExactMatchByKey, relevantPropertiesByKey, relevantUnmatchableProperties] = - event === EVENT_CALLED || path.length > 1 + interaction.type === INTERACTION_CALLED || path.length > 1 ? [ this.propertiesAndGettersByKey, this.propertiesAndGettersByKey, this.unmatchablePropertiesAndGetters ] - : event === EVENT_ACCESSED + : interaction.type === INTERACTION_ACCESSED ? [this.propertiesAndGettersByKey, this.gettersByKey, this.unmatchableGetters] : [this.propertiesAndSettersByKey, this.settersByKey, this.unmatchableSetters]; @@ -181,20 +185,20 @@ export class ObjectEntity extends ExpressionEntity { const properties = relevantPropertiesByKey[key]; if (properties) { for (const property of properties) { - property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); + property.deoptimizeThisOnInteractionAtPath(interaction, subPath, recursionTracker); } } if (!this.immutable) { - this.thisParametersToBeDeoptimized.add(thisParameter); + this.thisParametersToBeDeoptimized.add(interaction.thisArg); } return; } for (const property of relevantUnmatchableProperties) { - property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); + property.deoptimizeThisOnInteractionAtPath(interaction, subPath, recursionTracker); } if (INTEGER_REG_EXP.test(key)) { for (const property of this.unknownIntegerProps) { - property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); + property.deoptimizeThisOnInteractionAtPath(interaction, subPath, recursionTracker); } } } else { @@ -202,20 +206,19 @@ export class ObjectEntity extends ExpressionEntity { relevantUnmatchableProperties ])) { for (const property of properties) { - property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); + property.deoptimizeThisOnInteractionAtPath(interaction, subPath, recursionTracker); } } for (const property of this.unknownIntegerProps) { - property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); + property.deoptimizeThisOnInteractionAtPath(interaction, subPath, recursionTracker); } } if (!this.immutable) { - this.thisParametersToBeDeoptimized.add(thisParameter); + this.thisParametersToBeDeoptimized.add(interaction.thisArg); } - this.prototypeExpression?.deoptimizeThisOnEventAtPath( - event, + this.prototypeExpression?.deoptimizeThisOnInteractionAtPath( + interaction, path, - thisParameter, recursionTracker ); } @@ -244,19 +247,19 @@ export class ObjectEntity extends ExpressionEntity { getReturnExpressionWhenCalledAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteractionCalled, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { if (path.length === 0) { return UNKNOWN_EXPRESSION; } - const key = path[0]; + const [key, ...subPath] = path; const expressionAtPath = this.getMemberExpressionAndTrackDeopt(key, origin); if (expressionAtPath) { return expressionAtPath.getReturnExpressionWhenCalledAtPath( - path.slice(1), - callOptions, + subPath, + interaction, recursionTracker, origin ); @@ -264,7 +267,7 @@ export class ObjectEntity extends ExpressionEntity { if (this.prototypeExpression) { return this.prototypeExpression.getReturnExpressionWhenCalledAtPath( path, - callOptions, + interaction, recursionTracker, origin ); @@ -272,113 +275,56 @@ export class ObjectEntity extends ExpressionEntity { return UNKNOWN_EXPRESSION; } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - const [key, ...subPath] = path; - if (path.length > 1) { - if (typeof key !== 'string') { - return true; - } - const expressionAtPath = this.getMemberExpression(key); - if (expressionAtPath) { - return expressionAtPath.hasEffectsWhenAccessedAtPath(subPath, context); - } - if (this.prototypeExpression) { - return this.prototypeExpression.hasEffectsWhenAccessedAtPath(path, context); - } - return true; - } - - if (this.hasLostTrack) return true; - if (typeof key === 'string') { - if (this.propertiesAndGettersByKey[key]) { - const getters = this.gettersByKey[key]; - if (getters) { - for (const getter of getters) { - if (getter.hasEffectsWhenAccessedAtPath(subPath, context)) return true; - } - } - return false; - } - for (const getter of this.unmatchableGetters) { - if (getter.hasEffectsWhenAccessedAtPath(subPath, context)) { - return true; - } - } - } else { - for (const getters of Object.values(this.gettersByKey).concat([this.unmatchableGetters])) { - for (const getter of getters) { - if (getter.hasEffectsWhenAccessedAtPath(subPath, context)) return true; - } - } - } - if (this.prototypeExpression) { - return this.prototypeExpression.hasEffectsWhenAccessedAtPath(path, context); - } - return false; - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + hasEffectsOnInteractionAtPath( + path: ObjectPath, + interaction: NodeInteraction, + context: HasEffectsContext + ): boolean { const [key, ...subPath] = path; - if (path.length > 1) { - if (typeof key !== 'string') { - return true; - } + if (subPath.length || interaction.type === INTERACTION_CALLED) { const expressionAtPath = this.getMemberExpression(key); if (expressionAtPath) { - return expressionAtPath.hasEffectsWhenAssignedAtPath(subPath, context); + return expressionAtPath.hasEffectsOnInteractionAtPath(subPath, interaction, context); } if (this.prototypeExpression) { - return this.prototypeExpression.hasEffectsWhenAssignedAtPath(path, context); + return this.prototypeExpression.hasEffectsOnInteractionAtPath(path, interaction, context); } return true; } - if (key === UnknownNonAccessorKey) return false; if (this.hasLostTrack) return true; + const [propertiesAndAccessorsByKey, accessorsByKey, unmatchableAccessors] = + interaction.type === INTERACTION_ACCESSED + ? [this.propertiesAndGettersByKey, this.gettersByKey, this.unmatchableGetters] + : [this.propertiesAndSettersByKey, this.settersByKey, this.unmatchableSetters]; if (typeof key === 'string') { - if (this.propertiesAndSettersByKey[key]) { - const setters = this.settersByKey[key]; - if (setters) { - for (const setter of setters) { - if (setter.hasEffectsWhenAssignedAtPath(subPath, context)) return true; + if (propertiesAndAccessorsByKey[key]) { + const accessors = accessorsByKey[key]; + if (accessors) { + for (const accessor of accessors) { + if (accessor.hasEffectsOnInteractionAtPath(subPath, interaction, context)) return true; } } return false; } - for (const property of this.unmatchableSetters) { - if (property.hasEffectsWhenAssignedAtPath(subPath, context)) { + for (const accessor of unmatchableAccessors) { + if (accessor.hasEffectsOnInteractionAtPath(subPath, interaction, context)) { return true; } } } else { - for (const setters of Object.values(this.settersByKey).concat([this.unmatchableSetters])) { - for (const setter of setters) { - if (setter.hasEffectsWhenAssignedAtPath(subPath, context)) return true; + for (const accessors of Object.values(accessorsByKey).concat([unmatchableAccessors])) { + for (const accessor of accessors) { + if (accessor.hasEffectsOnInteractionAtPath(subPath, interaction, context)) return true; } } } if (this.prototypeExpression) { - return this.prototypeExpression.hasEffectsWhenAssignedAtPath(path, context); + return this.prototypeExpression.hasEffectsOnInteractionAtPath(path, interaction, context); } return false; } - hasEffectsWhenCalledAtPath( - path: ObjectPath, - callOptions: CallOptions, - context: HasEffectsContext - ): boolean { - const key = path[0]; - const expressionAtPath = this.getMemberExpression(key); - if (expressionAtPath) { - return expressionAtPath.hasEffectsWhenCalledAtPath(path.slice(1), callOptions, context); - } - if (this.prototypeExpression) { - return this.prototypeExpression.hasEffectsWhenCalledAtPath(path, callOptions, context); - } - return true; - } - 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 eded9c87631..e31287f6218 100644 --- a/src/ast/nodes/shared/ObjectMember.ts +++ b/src/ast/nodes/shared/ObjectMember.ts @@ -1,7 +1,7 @@ -import type { CallOptions } from '../../CallOptions'; import type { DeoptimizableEntity } from '../../DeoptimizableEntity'; import type { HasEffectsContext } from '../../ExecutionContext'; -import type { NodeEvent } from '../../NodeEvents'; +import type { NodeInteractionWithThisArg } from '../../NodeInteractions'; +import { NodeInteraction, NodeInteractionCalled } from '../../NodeInteractions'; import type { ObjectPath, PathTracker } from '../../utils/PathTracker'; import { ExpressionEntity, type LiteralValueOrUnknown } from './Expression'; @@ -14,16 +14,14 @@ export class ObjectMember extends ExpressionEntity { this.object.deoptimizePath([this.key, ...path]); } - deoptimizeThisOnEventAtPath( - event: NodeEvent, + deoptimizeThisOnInteractionAtPath( + interaction: NodeInteractionWithThisArg, path: ObjectPath, - thisParameter: ExpressionEntity, recursionTracker: PathTracker ): void { - this.object.deoptimizeThisOnEventAtPath( - event, + this.object.deoptimizeThisOnInteractionAtPath( + interaction, [this.key, ...path], - thisParameter, recursionTracker ); } @@ -38,31 +36,23 @@ export class ObjectMember extends ExpressionEntity { getReturnExpressionWhenCalledAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteractionCalled, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { return this.object.getReturnExpressionWhenCalledAtPath( [this.key, ...path], - callOptions, + interaction, recursionTracker, origin ); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return this.object.hasEffectsWhenAccessedAtPath([this.key, ...path], context); - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return this.object.hasEffectsWhenAssignedAtPath([this.key, ...path], context); - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - return this.object.hasEffectsWhenCalledAtPath([this.key, ...path], callOptions, context); + return this.object.hasEffectsOnInteractionAtPath([this.key, ...path], interaction, context); } } diff --git a/src/ast/nodes/shared/ObjectPrototype.ts b/src/ast/nodes/shared/ObjectPrototype.ts index ca2b8112f61..36fea3a829e 100644 --- a/src/ast/nodes/shared/ObjectPrototype.ts +++ b/src/ast/nodes/shared/ObjectPrototype.ts @@ -1,4 +1,8 @@ -import { EVENT_CALLED, NodeEvent } from '../../NodeEvents'; +import { + INTERACTION_CALLED, + NodeInteraction, + NodeInteractionWithThisArg +} from '../../NodeInteractions'; import { ObjectPath, ObjectPathKey, UNKNOWN_PATH } from '../../utils/PathTracker'; import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue } from './Expression'; import { @@ -16,13 +20,12 @@ const isInteger = (prop: ObjectPathKey): boolean => typeof prop === 'string' && // will improve tree-shaking for out-of-bounds array properties const OBJECT_PROTOTYPE_FALLBACK: ExpressionEntity = new (class ObjectPrototypeFallbackExpression extends ExpressionEntity { - deoptimizeThisOnEventAtPath( - event: NodeEvent, - path: ObjectPath, - thisParameter: ExpressionEntity + deoptimizeThisOnInteractionAtPath( + { type, thisArg }: NodeInteractionWithThisArg, + path: ObjectPath ): void { - if (event === EVENT_CALLED && path.length === 1 && !isInteger(path[0])) { - thisParameter.deoptimizePath(UNKNOWN_PATH); + if (type === INTERACTION_CALLED && path.length === 1 && !isInteger(path[0])) { + thisArg.deoptimizePath(UNKNOWN_PATH); } } @@ -33,12 +36,8 @@ const OBJECT_PROTOTYPE_FALLBACK: ExpressionEntity = return path.length === 1 && isInteger(path[0]) ? undefined : UnknownValue; } - hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean { - return path.length > 1; - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath): boolean { - return path.length > 1; + hasEffectsOnInteractionAtPath(path: ObjectPath, { type }: NodeInteraction): boolean { + return path.length > 1 || type === INTERACTION_CALLED; } })(); diff --git a/src/ast/nodes/shared/knownGlobals.ts b/src/ast/nodes/shared/knownGlobals.ts index b42316d6bc3..bffb9c98813 100644 --- a/src/ast/nodes/shared/knownGlobals.ts +++ b/src/ast/nodes/shared/knownGlobals.ts @@ -1,14 +1,14 @@ /* eslint sort-keys: "off" */ -import { CallOptions } from '../../CallOptions'; import { HasEffectsContext } from '../../ExecutionContext'; -import { UNKNOWN_NON_ACCESSOR_PATH } from '../../utils/PathTracker'; +import { NODE_INTERACTION_UNKNOWN_ASSIGNMENT, NodeInteractionCalled } from '../../NodeInteractions'; import type { ObjectPath } from '../../utils/PathTracker'; +import { UNKNOWN_NON_ACCESSOR_PATH } from '../../utils/PathTracker'; const ValueProperties = Symbol('Value Properties'); interface ValueDescription { - hasEffectsWhenCalled(callOptions: CallOptions, context: HasEffectsContext): boolean; + hasEffectsWhenCalled(interaction: NodeInteractionCalled, context: HasEffectsContext): boolean; } interface GlobalDescription { @@ -46,10 +46,14 @@ const PF: GlobalDescription = { const MUTATES_ARG_WITHOUT_ACCESSOR: GlobalDescription = { __proto__: null, [ValueProperties]: { - hasEffectsWhenCalled(callOptions, context) { + hasEffectsWhenCalled({ args }, context) { return ( - !callOptions.args.length || - callOptions.args[0].hasEffectsWhenAssignedAtPath(UNKNOWN_NON_ACCESSOR_PATH, context) + !args.length || + args[0].hasEffectsOnInteractionAtPath( + UNKNOWN_NON_ACCESSOR_PATH, + NODE_INTERACTION_UNKNOWN_ASSIGNMENT, + context + ) ); } } diff --git a/src/ast/values.ts b/src/ast/values.ts index b6f0f90eb84..c180143921c 100644 --- a/src/ast/values.ts +++ b/src/ast/values.ts @@ -1,5 +1,11 @@ -import { type CallOptions, NO_ARGS } from './CallOptions'; import type { HasEffectsContext } from './ExecutionContext'; +import { + INTERACTION_ACCESSED, + INTERACTION_CALLED, + NODE_INTERACTION_UNKNOWN_CALL, + NodeInteraction, + NodeInteractionCalled +} from './NodeInteractions'; import type { LiteralValue } from './nodes/Literal'; import { ExpressionEntity, UNKNOWN_EXPRESSION } from './nodes/shared/Expression'; import { @@ -10,7 +16,9 @@ import { } from './utils/PathTracker'; export interface MemberDescription { - hasEffectsWhenCalled: ((callOptions: CallOptions, context: HasEffectsContext) => boolean) | null; + hasEffectsWhenCalled: + | ((interaction: NodeInteractionCalled, context: HasEffectsContext) => boolean) + | null; returns: ExpressionEntity; } @@ -52,17 +60,16 @@ export const UNKNOWN_LITERAL_BOOLEAN: ExpressionEntity = return UNKNOWN_EXPRESSION; } - hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean { - return path.length > 1; - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - if (path.length === 1) { - return hasMemberEffectWhenCalled(literalBooleanMembers, path[0], callOptions, context); + if (interaction.type === INTERACTION_ACCESSED) { + return path.length > 1; + } + if (interaction.type === INTERACTION_CALLED && path.length === 1) { + return hasMemberEffectWhenCalled(literalBooleanMembers, path[0], interaction, context); } return true; } @@ -84,17 +91,16 @@ export const UNKNOWN_LITERAL_NUMBER: ExpressionEntity = return UNKNOWN_EXPRESSION; } - hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean { - return path.length > 1; - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - if (path.length === 1) { - return hasMemberEffectWhenCalled(literalNumberMembers, path[0], callOptions, context); + if (interaction.type === INTERACTION_ACCESSED) { + return path.length > 1; + } + if (interaction.type === INTERACTION_CALLED && path.length === 1) { + return hasMemberEffectWhenCalled(literalNumberMembers, path[0], interaction, context); } return true; } @@ -116,17 +122,16 @@ export const UNKNOWN_LITERAL_STRING: ExpressionEntity = return UNKNOWN_EXPRESSION; } - hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean { - return path.length > 1; - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - if (path.length === 1) { - return hasMemberEffectWhenCalled(literalStringMembers, path[0], callOptions, context); + if (interaction.type === INTERACTION_ACCESSED) { + return path.length > 1; + } + if (interaction.type === INTERACTION_CALLED && path.length === 1) { + return hasMemberEffectWhenCalled(literalStringMembers, path[0], interaction, context); } return true; } @@ -141,22 +146,14 @@ const returnsString: RawMemberDescription = { const stringReplace: RawMemberDescription = { value: { - hasEffectsWhenCalled(callOptions, context) { - const arg1 = callOptions.args[1]; + hasEffectsWhenCalled({ args }, context) { + const arg1 = args[1]; return ( - callOptions.args.length < 2 || + args.length < 2 || (typeof arg1.getLiteralValueAtPath(EMPTY_PATH, SHARED_RECURSION_TRACKER, { deoptimizeCache() {} }) === 'symbol' && - arg1.hasEffectsWhenCalledAtPath( - EMPTY_PATH, - { - args: NO_ARGS, - thisParam: null, - withNew: false - }, - context - )) + arg1.hasEffectsOnInteractionAtPath(EMPTY_PATH, NODE_INTERACTION_UNKNOWN_CALL, context)) ); }, returns: UNKNOWN_LITERAL_STRING @@ -262,13 +259,13 @@ export function getLiteralMembersForValue export function hasMemberEffectWhenCalled( members: MemberDescriptions, memberName: ObjectPathKey, - callOptions: CallOptions, + interaction: NodeInteractionCalled, context: HasEffectsContext ): boolean { if (typeof memberName !== 'string' || !members[memberName]) { return true; } - return members[memberName].hasEffectsWhenCalled?.(callOptions, context) || false; + return members[memberName].hasEffectsWhenCalled?.(interaction, context) || false; } export function getMemberReturnExpressionWhenCalled( diff --git a/src/ast/variables/ArgumentsVariable.ts b/src/ast/variables/ArgumentsVariable.ts index 8826e29ac19..94ad7093def 100644 --- a/src/ast/variables/ArgumentsVariable.ts +++ b/src/ast/variables/ArgumentsVariable.ts @@ -1,6 +1,7 @@ import type { AstContext } from '../../Module'; +import { INTERACTION_ACCESSED, NodeInteraction } from '../NodeInteractions'; import { UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; -import type { ObjectPath } from '../utils/PathTracker'; +import { ObjectPath } from '../utils/PathTracker'; import LocalVariable from './LocalVariable'; export default class ArgumentsVariable extends LocalVariable { @@ -8,15 +9,7 @@ export default class ArgumentsVariable extends LocalVariable { super('arguments', null, UNKNOWN_EXPRESSION, context); } - hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean { - return path.length > 1; - } - - hasEffectsWhenAssignedAtPath(): boolean { - return true; - } - - hasEffectsWhenCalledAtPath(): boolean { - return true; + hasEffectsOnInteractionAtPath(path: ObjectPath, { type }: NodeInteraction): boolean { + return type !== INTERACTION_ACCESSED || path.length > 1; } } diff --git a/src/ast/variables/ExternalVariable.ts b/src/ast/variables/ExternalVariable.ts index 18f2f999cb9..ba27368b170 100644 --- a/src/ast/variables/ExternalVariable.ts +++ b/src/ast/variables/ExternalVariable.ts @@ -1,4 +1,5 @@ import type ExternalModule from '../../ExternalModule'; +import { INTERACTION_ACCESSED, NodeInteraction } from '../NodeInteractions'; import type Identifier from '../nodes/Identifier'; import type { ObjectPath } from '../utils/PathTracker'; import Variable from './Variable'; @@ -21,8 +22,8 @@ export default class ExternalVariable extends Variable { } } - hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean { - return path.length > (this.isNamespace ? 1 : 0); + hasEffectsOnInteractionAtPath(path: ObjectPath, { type }: NodeInteraction): boolean { + return type !== INTERACTION_ACCESSED || path.length > (this.isNamespace ? 1 : 0); } include(): void { diff --git a/src/ast/variables/GlobalVariable.ts b/src/ast/variables/GlobalVariable.ts index e79ecd421e5..d3959d8f905 100644 --- a/src/ast/variables/GlobalVariable.ts +++ b/src/ast/variables/GlobalVariable.ts @@ -1,6 +1,11 @@ -import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; +import { + INTERACTION_ACCESSED, + INTERACTION_ASSIGNED, + INTERACTION_CALLED, + NodeInteraction +} from '../NodeInteractions'; import { LiteralValueOrUnknown, UnknownTruthyValue, @@ -24,20 +29,24 @@ export default class GlobalVariable extends Variable { 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]); - } - return !getGlobalAtPath([this.name, ...path].slice(0, -1)); - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - const globalAtPath = getGlobalAtPath([this.name, ...path]); - return !globalAtPath || globalAtPath.hasEffectsWhenCalled(callOptions, context); + switch (interaction.type) { + case INTERACTION_ACCESSED: + if (path.length === 0) { + // Technically, "undefined" is a global variable of sorts + return this.name !== 'undefined' && !getGlobalAtPath([this.name]); + } + return !getGlobalAtPath([this.name, ...path].slice(0, -1)); + case INTERACTION_ASSIGNED: + return true; + case INTERACTION_CALLED: { + const globalAtPath = getGlobalAtPath([this.name, ...path]); + return !globalAtPath || globalAtPath.hasEffectsWhenCalled(interaction, context); + } + } } } diff --git a/src/ast/variables/LocalVariable.ts b/src/ast/variables/LocalVariable.ts index e585ea775a3..628c3a35acb 100644 --- a/src/ast/variables/LocalVariable.ts +++ b/src/ast/variables/LocalVariable.ts @@ -1,8 +1,13 @@ import Module, { AstContext } from '../../Module'; -import type { CallOptions } from '../CallOptions'; import type { DeoptimizableEntity } from '../DeoptimizableEntity'; import { createInclusionContext, HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import type { NodeEvent } from '../NodeEvents'; +import type { NodeInteractionCalled, NodeInteractionWithThisArg } from '../NodeInteractions'; +import { + INTERACTION_ACCESSED, + INTERACTION_ASSIGNED, + INTERACTION_CALLED, + NodeInteraction +} from '../NodeInteractions'; import type ExportDefaultDeclaration from '../nodes/ExportDefaultDeclaration'; import type Identifier from '../nodes/Identifier'; import * as NodeType from '../nodes/NodeType'; @@ -81,19 +86,18 @@ export default class LocalVariable extends Variable { } } - deoptimizeThisOnEventAtPath( - event: NodeEvent, + deoptimizeThisOnInteractionAtPath( + interaction: NodeInteractionWithThisArg, path: ObjectPath, - thisParameter: ExpressionEntity, recursionTracker: PathTracker ): void { if (this.isReassigned || !this.init) { - return thisParameter.deoptimizePath(UNKNOWN_PATH); + return interaction.thisArg.deoptimizePath(UNKNOWN_PATH); } recursionTracker.withTrackedEntityAtPath( path, this.init, - () => this.init!.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker), + () => this.init!.deoptimizeThisOnInteractionAtPath(interaction, path, recursionTracker), undefined ); } @@ -119,7 +123,7 @@ export default class LocalVariable extends Variable { getReturnExpressionWhenCalledAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteractionCalled, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { @@ -133,7 +137,7 @@ export default class LocalVariable extends Variable { this.expressionsToBeDeoptimized.push(origin); return this.init!.getReturnExpressionWhenCalledAtPath( path, - callOptions, + interaction, recursionTracker, origin ); @@ -142,33 +146,32 @@ export default class LocalVariable extends Variable { ); } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (this.isReassigned) return true; - return (this.init && - !context.accessed.trackEntityAtPathAndGetIfTracked(path, this) && - this.init.hasEffectsWhenAccessedAtPath(path, context))!; - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (this.included) return true; - if (path.length === 0) return false; - if (this.isReassigned) return true; - return (this.init && - !context.assigned.trackEntityAtPathAndGetIfTracked(path, this) && - this.init.hasEffectsWhenAssignedAtPath(path, context))!; - } - - hasEffectsWhenCalledAtPath( + hasEffectsOnInteractionAtPath( path: ObjectPath, - callOptions: CallOptions, + interaction: NodeInteraction, context: HasEffectsContext ): boolean { - if (this.isReassigned) return true; - return (this.init && - !( - callOptions.withNew ? context.instantiated : context.called - ).trackEntityAtPathAndGetIfTracked(path, callOptions, this) && - this.init.hasEffectsWhenCalledAtPath(path, callOptions, context))!; + switch (interaction.type) { + case INTERACTION_ACCESSED: + if (this.isReassigned) return true; + return (this.init && + !context.accessed.trackEntityAtPathAndGetIfTracked(path, this) && + this.init.hasEffectsOnInteractionAtPath(path, interaction, context))!; + case INTERACTION_ASSIGNED: + if (this.included) return true; + if (path.length === 0) return false; + if (this.isReassigned) return true; + return (this.init && + !context.assigned.trackEntityAtPathAndGetIfTracked(path, this) && + this.init.hasEffectsOnInteractionAtPath(path, interaction, context))!; + case INTERACTION_CALLED: + if (this.isReassigned) return true; + return (this.init && + !( + interaction.withNew ? context.instantiated : context.called + ).trackEntityAtPathAndGetIfTracked(path, interaction.args, this) && + this.init.hasEffectsOnInteractionAtPath(path, interaction, context))!; + } } include(): void { diff --git a/src/ast/variables/ThisVariable.ts b/src/ast/variables/ThisVariable.ts index 7c4cba21045..cd049b81f27 100644 --- a/src/ast/variables/ThisVariable.ts +++ b/src/ast/variables/ThisVariable.ts @@ -1,6 +1,6 @@ import type { AstContext } from '../../Module'; import type { HasEffectsContext } from '../ExecutionContext'; -import type { NodeEvent } from '../NodeEvents'; +import type { NodeInteraction, NodeInteractionWithThisArg } from '../NodeInteractions'; import { type ExpressionEntity, UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; import { DiscriminatedPathTracker, @@ -9,16 +9,15 @@ import { } from '../utils/PathTracker'; import LocalVariable from './LocalVariable'; -interface ThisDeoptimizationEvent { - event: NodeEvent; +interface ThisDeoptimizationInteraction { + interaction: NodeInteractionWithThisArg; path: ObjectPath; - thisParameter: ExpressionEntity; } export default class ThisVariable extends LocalVariable { private readonly deoptimizedPaths: ObjectPath[] = []; private readonly entitiesToBeDeoptimized = new Set(); - private readonly thisDeoptimizationList: ThisDeoptimizationEvent[] = []; + private readonly thisDeoptimizationList: ThisDeoptimizationInteraction[] = []; private readonly thisDeoptimizations = new DiscriminatedPathTracker(); constructor(context: AstContext) { @@ -29,8 +28,8 @@ export default class ThisVariable extends LocalVariable { for (const path of this.deoptimizedPaths) { entity.deoptimizePath(path); } - for (const thisDeoptimization of this.thisDeoptimizationList) { - this.applyThisDeoptimizationEvent(entity, thisDeoptimization); + for (const { interaction, path } of this.thisDeoptimizationList) { + entity.deoptimizeThisOnInteractionAtPath(interaction, path, SHARED_RECURSION_TRACKER); } this.entitiesToBeDeoptimized.add(entity); } @@ -48,47 +47,36 @@ export default class ThisVariable extends LocalVariable { } } - deoptimizeThisOnEventAtPath( - event: NodeEvent, - path: ObjectPath, - thisParameter: ExpressionEntity + deoptimizeThisOnInteractionAtPath( + interaction: NodeInteractionWithThisArg, + path: ObjectPath ): void { - const thisDeoptimization: ThisDeoptimizationEvent = { - event, - path, - thisParameter + const thisDeoptimization: ThisDeoptimizationInteraction = { + interaction, + path }; - if (!this.thisDeoptimizations.trackEntityAtPathAndGetIfTracked(path, event, thisParameter)) { + if ( + !this.thisDeoptimizations.trackEntityAtPathAndGetIfTracked( + path, + interaction.type, + interaction.thisArg + ) + ) { for (const entity of this.entitiesToBeDeoptimized) { - this.applyThisDeoptimizationEvent(entity, thisDeoptimization); + entity.deoptimizeThisOnInteractionAtPath(interaction, path, SHARED_RECURSION_TRACKER); } this.thisDeoptimizationList.push(thisDeoptimization); } } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - return ( - this.getInit(context).hasEffectsWhenAccessedAtPath(path, context) || - super.hasEffectsWhenAccessedAtPath(path, context) - ); - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + hasEffectsOnInteractionAtPath( + path: ObjectPath, + interaction: NodeInteraction, + context: HasEffectsContext + ): boolean { return ( - this.getInit(context).hasEffectsWhenAssignedAtPath(path, context) || - super.hasEffectsWhenAssignedAtPath(path, context) - ); - } - - private applyThisDeoptimizationEvent( - entity: ExpressionEntity, - { event, path, thisParameter }: ThisDeoptimizationEvent - ) { - entity.deoptimizeThisOnEventAtPath( - event, - path, - thisParameter === this ? entity : thisParameter, - SHARED_RECURSION_TRACKER + this.getInit(context).hasEffectsOnInteractionAtPath(path, interaction, context) || + super.hasEffectsOnInteractionAtPath(path, interaction, context) ); } diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index 51575176de6..3895d81503a 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -1,6 +1,7 @@ import type ExternalModule from '../../ExternalModule'; import type Module from '../../Module'; import type { HasEffectsContext } from '../ExecutionContext'; +import { INTERACTION_ACCESSED, NodeInteraction } from '../NodeInteractions'; import type Identifier from '../nodes/Identifier'; import { ExpressionEntity } from '../nodes/shared/Expression'; import type { ObjectPath } from '../utils/PathTracker'; @@ -36,8 +37,12 @@ export default class Variable extends ExpressionEntity { return this.renderBaseName ? `${this.renderBaseName}${getPropertyAccess(name)}` : name; } - hasEffectsWhenAccessedAtPath(path: ObjectPath, _context: HasEffectsContext): boolean { - return path.length > 0; + hasEffectsOnInteractionAtPath( + path: ObjectPath, + { type }: NodeInteraction, + _context: HasEffectsContext + ): boolean { + return type !== INTERACTION_ACCESSED || path.length > 0; } /** diff --git a/test/function/samples/for-in-accessors/_config.js b/test/function/samples/for-in-accessors/_config.js new file mode 100644 index 00000000000..70c608f68b2 --- /dev/null +++ b/test/function/samples/for-in-accessors/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'deoptimizes "this" for accessors triggered by for-in loops' +}; diff --git a/test/function/samples/for-in-accessors/main.js b/test/function/samples/for-in-accessors/main.js new file mode 100644 index 00000000000..e5194d0d9ee --- /dev/null +++ b/test/function/samples/for-in-accessors/main.js @@ -0,0 +1,10 @@ +const obj = { + setter: false, + set foo(value) { + this.setter = true; + } +}; + +for (obj.foo in {x:1}); + +assert.ok(obj.setter ? true : false); diff --git a/test/function/samples/for-of-accessors/_config.js b/test/function/samples/for-of-accessors/_config.js new file mode 100644 index 00000000000..e54f1f0ac4c --- /dev/null +++ b/test/function/samples/for-of-accessors/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'deoptimizes "this" for accessors triggered by for-of loops' +}; diff --git a/test/function/samples/for-of-accessors/main.js b/test/function/samples/for-of-accessors/main.js new file mode 100644 index 00000000000..b47bc702e82 --- /dev/null +++ b/test/function/samples/for-of-accessors/main.js @@ -0,0 +1,10 @@ +const obj = { + setter: false, + set foo(value) { + this.setter = true; + } +}; + +for (obj.foo of [1]); + +assert.ok(obj.setter ? true : false); diff --git a/test/function/samples/update-expression-accessors/_config.js b/test/function/samples/update-expression-accessors/_config.js new file mode 100644 index 00000000000..f271e4ae349 --- /dev/null +++ b/test/function/samples/update-expression-accessors/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'deoptimizes "this" for accessors triggered by update expressions' +}; diff --git a/test/function/samples/update-expression-accessors/main.js b/test/function/samples/update-expression-accessors/main.js new file mode 100644 index 00000000000..a8658cd7bd4 --- /dev/null +++ b/test/function/samples/update-expression-accessors/main.js @@ -0,0 +1,16 @@ +const obj = { + getter: false, + setter: false, + get foo() { + this.getter = true; + return 0; + }, + set foo(value) { + this.setter = true; + } +}; + +obj.foo++; + +assert.ok(obj.getter ? true : false); +assert.ok(obj.setter ? true : false);