diff --git a/cli/run/build.ts b/cli/run/build.ts index 38c55e2e05f..c5fab831470 100644 --- a/cli/run/build.ts +++ b/cli/run/build.ts @@ -24,9 +24,7 @@ export default async function build( } else if (inputOptions.input instanceof Array) { inputFiles = inputOptions.input.join(', '); } else if (typeof inputOptions.input === 'object' && inputOptions.input !== null) { - inputFiles = Object.keys(inputOptions.input) - .map(name => (inputOptions.input as Record)[name]) - .join(', '); + inputFiles = Object.values(inputOptions.input).join(', '); } stderr(cyan(`\n${bold(inputFiles!)} → ${bold(files.join(', '))}...`)); } diff --git a/cli/run/watch-cli.ts b/cli/run/watch-cli.ts index fb2ef95de75..f6d33fc4fd4 100644 --- a/cli/run/watch-cli.ts +++ b/cli/run/watch-cli.ts @@ -106,9 +106,7 @@ export async function watch(command: any) { if (typeof input !== 'string') { input = Array.isArray(input) ? input.join(', ') - : Object.keys(input as Record) - .map(key => (input as Record)[key]) - .join(', '); + : Object.values(input as Record).join(', '); } stderr( cyan(`bundles ${bold(input)} → ${bold(event.output.map(relativeId).join(', '))}...`) diff --git a/package-lock.json b/package-lock.json index 0830654b069..a3f12ec6d51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "rollup", - "version": "2.48.0", + "version": "2.49.0-1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index cb616b25548..cbb4e3f6b96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rollup", - "version": "2.48.0", + "version": "2.49.0-1", "description": "Next-generation ES module bundler", "main": "dist/rollup.js", "module": "dist/es/rollup.js", diff --git a/src/Bundle.ts b/src/Bundle.ts index 01e731fa2a7..84b506f037d 100644 --- a/src/Bundle.ts +++ b/src/Bundle.ts @@ -6,6 +6,7 @@ import { GetManualChunk, NormalizedInputOptions, NormalizedOutputOptions, + OutputAsset, OutputBundle, OutputBundleWithPlaceholders, OutputChunk, @@ -41,11 +42,7 @@ export default class Bundle { async generate(isWrite: boolean): Promise { timeStart('GENERATE', 1); const outputBundle: OutputBundleWithPlaceholders = Object.create(null); - this.pluginDriver.setOutputBundle( - outputBundle, - this.outputOptions, - this.facadeChunkByModule - ); + this.pluginDriver.setOutputBundle(outputBundle, this.outputOptions, this.facadeChunkByModule); try { await this.pluginDriver.hookParallel('renderStart', [this.outputOptions, this.inputOptions]); @@ -104,9 +101,9 @@ export default class Bundle { ): Promise> { const manualChunkAliasByEntry = new Map(); const chunkEntries = await Promise.all( - Object.keys(manualChunks).map(async alias => ({ + Object.entries(manualChunks).map(async ([alias, files]) => ({ alias, - entries: await this.graph.moduleLoader.addAdditionalModules(manualChunks[alias]) + entries: await this.graph.moduleLoader.addAdditionalModules(files) })) ); for (const { alias, entries } of chunkEntries) { @@ -169,24 +166,23 @@ export default class Bundle { } private finaliseAssets(outputBundle: OutputBundleWithPlaceholders): void { - for (const key of Object.keys(outputBundle)) { - const file = outputBundle[key] as any; + for (const file of Object.values(outputBundle)) { if (!file.type) { warnDeprecation( 'A plugin is directly adding properties to the bundle object in the "generateBundle" hook. This is deprecated and will be removed in a future Rollup version, please use "this.emitFile" instead.', true, this.inputOptions ); - file.type = 'asset'; + (file as OutputAsset).type = 'asset'; } - if (this.outputOptions.validate && typeof file.code == 'string') { + if (this.outputOptions.validate && typeof (file as OutputChunk).code == 'string') { try { - this.graph.contextParse(file.code, { + this.graph.contextParse((file as OutputChunk).code, { allowHashBang: true, ecmaVersion: 'latest' }); } catch (exception) { - this.inputOptions.onwarn(errChunkInvalid(file, exception)); + this.inputOptions.onwarn(errChunkInvalid(file as OutputChunk, exception)); } } } diff --git a/src/Chunk.ts b/src/Chunk.ts index f8258da7b0c..cc03a6e8086 100644 --- a/src/Chunk.ts +++ b/src/Chunk.ts @@ -1326,8 +1326,8 @@ export default class Chunk { if (!this.outputOptions.preserveModules) { if (this.includedNamespaces.has(module)) { const memberVariables = module.namespace.getMemberVariables(); - for (const name of Object.keys(memberVariables)) { - moduleImports.add(memberVariables[name]); + for (const variable of Object.values(memberVariables)) { + moduleImports.add(variable); } } } diff --git a/src/Graph.ts b/src/Graph.ts index 9afb0fdda66..e62703cb37a 100644 --- a/src/Graph.ts +++ b/src/Graph.ts @@ -34,9 +34,9 @@ function normalizeEntryModules( name: null })); } - return Object.keys(entryModules).map(name => ({ + return Object.entries(entryModules).map(([name, id]) => ({ fileName: null, - id: entryModules[name], + id, implicitlyLoadedAfter: [], importer: undefined, name @@ -74,13 +74,14 @@ export default class Graph { // increment access counter for (const name in this.pluginCache) { const cache = this.pluginCache[name]; - for (const key of Object.keys(cache)) cache[key][0]++; + for (const value of Object.values(cache)) value[0]++; } } if (watcher) { this.watchMode = true; - const handleChange: WatchChangeHook = (...args) => this.pluginDriver.hookSeqSync('watchChange', args); + const handleChange: WatchChangeHook = (...args) => + this.pluginDriver.hookSeqSync('watchChange', args); const handleClose = () => this.pluginDriver.hookSeqSync('closeWatcher', []); watcher.on('change', handleChange); watcher.on('close', handleClose); @@ -118,9 +119,9 @@ export default class Graph { if (onCommentOrig && typeof onCommentOrig == 'function') { options.onComment = (block, text, start, end, ...args) => { - comments.push({type: block ? "Block" : "Line", value: text, start, end}); + comments.push({ type: block ? 'Block' : 'Line', value: text, start, end }); return onCommentOrig.call(options, block, text, start, end, ...args); - } + }; } else { options.onComment = comments; } @@ -146,8 +147,8 @@ export default class Graph { for (const name in this.pluginCache) { const cache = this.pluginCache[name]; let allDeleted = true; - for (const key of Object.keys(cache)) { - if (cache[key][0] >= this.options.experimentalCacheExpiry) delete cache[key]; + for (const [key, value] of Object.entries(cache)) { + if (value[0] >= this.options.experimentalCacheExpiry) delete cache[key]; else allDeleted = false; } if (allDeleted) delete this.pluginCache[name]; @@ -238,8 +239,7 @@ export default class Graph { private warnForMissingExports() { for (const module of this.modules) { - for (const importName of Object.keys(module.importDescriptions)) { - const importDescription = module.importDescriptions[importName]; + for (const importDescription of Object.values(module.importDescriptions)) { if ( importDescription.name !== '*' && !(importDescription.module as Module).getVariableForExportName(importDescription.name) diff --git a/src/Module.ts b/src/Module.ts index 26d896a5901..db1f7eeb4d4 100644 --- a/src/Module.ts +++ b/src/Module.ts @@ -1002,8 +1002,7 @@ export default class Module { private addModulesToImportDescriptions(importDescription: { [name: string]: ImportDescription | ReexportDescription; }) { - for (const name of Object.keys(importDescription)) { - const specifier = importDescription[name]; + for (const specifier of Object.values(importDescription)) { const id = this.resolvedIds[specifier.source].id; specifier.module = this.graph.modulesById.get(id)!; } diff --git a/src/ast/CallOptions.ts b/src/ast/CallOptions.ts index ad368d69951..bff7691ea20 100644 --- a/src/ast/CallOptions.ts +++ b/src/ast/CallOptions.ts @@ -5,5 +5,6 @@ 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 f67667c74c4..e08a3d9b33d 100644 --- a/src/ast/Entity.ts +++ b/src/ast/Entity.ts @@ -1,9 +1,7 @@ import { HasEffectsContext } from './ExecutionContext'; import { ObjectPath } from './utils/PathTracker'; -export interface Entity { - toString: () => string; -} +export interface Entity {} export interface WritableEntity extends Entity { /** @@ -13,5 +11,5 @@ export interface WritableEntity extends Entity { * expression of this node is reassigned as well. */ deoptimizePath(path: ObjectPath): void; - hasEffectsWhenAssignedAtPath(path: ObjectPath, execution: HasEffectsContext): boolean; + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean; } diff --git a/src/ast/NodeEvents.ts b/src/ast/NodeEvents.ts new file mode 100644 index 00000000000..e952677032a --- /dev/null +++ b/src/ast/NodeEvents.ts @@ -0,0 +1,4 @@ +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/keys.ts b/src/ast/keys.ts index 9434d9aae03..abd258e6e37 100644 --- a/src/ast/keys.ts +++ b/src/ast/keys.ts @@ -9,7 +9,7 @@ export const keys: { export function getAndCreateKeys(esTreeNode: GenericEsTreeNode) { keys[esTreeNode.type] = Object.keys(esTreeNode).filter( - key => key !== '_rollupAnnotations' && typeof esTreeNode[key] === 'object' + key => typeof esTreeNode[key] === 'object' && key !== '_rollupAnnotations' ); return keys[esTreeNode.type]; } diff --git a/src/ast/nodes/ArrayExpression.ts b/src/ast/nodes/ArrayExpression.ts index f37b81dbbc5..34c24bef3f6 100644 --- a/src/ast/nodes/ArrayExpression.ts +++ b/src/ast/nodes/ArrayExpression.ts @@ -1,34 +1,67 @@ import { CallOptions } from '../CallOptions'; +import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; -import { ObjectPath, UNKNOWN_PATH } from '../utils/PathTracker'; -import { - arrayMembers, - getMemberReturnExpressionWhenCalled, - hasMemberEffectWhenCalled, - UNKNOWN_EXPRESSION -} from '../values'; +import { NodeEvent } from '../NodeEvents'; +import { ObjectPath, PathTracker, UnknownInteger } from '../utils/PathTracker'; +import { UNDEFINED_EXPRESSION, UNKNOWN_LITERAL_NUMBER } from '../values'; import * as NodeType from './NodeType'; +import { ARRAY_PROTOTYPE } from './shared/ArrayPrototype'; +import { ExpressionEntity, LiteralValueOrUnknown } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; +import { ObjectEntity, ObjectProperty } from './shared/ObjectEntity'; import SpreadElement from './SpreadElement'; export default class ArrayExpression extends NodeBase { elements!: (ExpressionNode | SpreadElement | null)[]; type!: NodeType.tArrayExpression; + private objectEntity: ObjectEntity | null = null; - bind() { - super.bind(); - for (const element of this.elements) { - if (element !== null) element.deoptimizePath(UNKNOWN_PATH); - } + deoptimizePath(path: ObjectPath) { + this.getObjectEntity().deoptimizePath(path); + } + + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.getObjectEntity().deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + } + + getLiteralValueAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + return this.getObjectEntity().getLiteralValueAtPath(path, recursionTracker, origin); } - getReturnExpressionWhenCalledAtPath(path: ObjectPath) { - if (path.length !== 1) return UNKNOWN_EXPRESSION; - return getMemberReturnExpressionWhenCalled(arrayMembers, path[0]); + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): ExpressionEntity { + return this.getObjectEntity().getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); + } + + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { + return this.getObjectEntity().hasEffectsWhenAccessedAtPath(path, context); } - hasEffectsWhenAccessedAtPath(path: ObjectPath) { - return path.length > 1; + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext) { + return this.getObjectEntity().hasEffectsWhenAssignedAtPath(path, context); } hasEffectsWhenCalledAtPath( @@ -36,9 +69,26 @@ export default class ArrayExpression extends NodeBase { callOptions: CallOptions, context: HasEffectsContext ): boolean { - if (path.length === 1) { - return hasMemberEffectWhenCalled(arrayMembers, path[0], this.included, callOptions, context); + return this.getObjectEntity().hasEffectsWhenCalledAtPath(path, callOptions, context); + } + + private getObjectEntity(): ObjectEntity { + if (this.objectEntity !== null) { + return this.objectEntity; + } + const properties: ObjectProperty[] = [ + { kind: 'init', key: 'length', property: UNKNOWN_LITERAL_NUMBER } + ]; + for (let index = 0; index < this.elements.length; index++) { + const element = this.elements[index]; + if (element instanceof SpreadElement) { + properties.unshift({ kind: 'init', key: UnknownInteger, property: element }); + } else if (!element) { + properties.push({ kind: 'init', key: String(index), property: UNDEFINED_EXPRESSION }); + } else { + properties.push({ kind: 'init', key: String(index), property: element }); + } } - return true; + return (this.objectEntity = new ObjectEntity(properties, ARRAY_PROTOTYPE)); } } diff --git a/src/ast/nodes/ArrayPattern.ts b/src/ast/nodes/ArrayPattern.ts index e73f591fd7b..e0d23d61be4 100644 --- a/src/ast/nodes/ArrayPattern.ts +++ b/src/ast/nodes/ArrayPattern.ts @@ -1,8 +1,8 @@ import { HasEffectsContext } from '../ExecutionContext'; import { EMPTY_PATH, ObjectPath } from '../utils/PathTracker'; -import { UNKNOWN_EXPRESSION } from '../values'; import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; +import { UNKNOWN_EXPRESSION } from './shared/Expression'; import { NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; diff --git a/src/ast/nodes/ArrowFunctionExpression.ts b/src/ast/nodes/ArrowFunctionExpression.ts index 52c08376857..b9d1ba672f4 100644 --- a/src/ast/nodes/ArrowFunctionExpression.ts +++ b/src/ast/nodes/ArrowFunctionExpression.ts @@ -3,11 +3,11 @@ import { BROKEN_FLOW_NONE, HasEffectsContext, InclusionContext } from '../Execut import ReturnValueScope from '../scopes/ReturnValueScope'; import Scope from '../scopes/Scope'; import { ObjectPath, UnknownKey, UNKNOWN_PATH } from '../utils/PathTracker'; -import { UNKNOWN_EXPRESSION } from '../values'; import BlockStatement from './BlockStatement'; import Identifier from './Identifier'; import * as NodeType from './NodeType'; import RestElement from './RestElement'; +import { UNKNOWN_EXPRESSION } from './shared/Expression'; import { ExpressionNode, GenericEsTreeNode, IncludeChildren, NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; import SpreadElement from './SpreadElement'; @@ -31,6 +31,9 @@ export default class ArrowFunctionExpression extends NodeBase { } } + // Arrow functions do not mutate their context + deoptimizeThisOnEventAtPath() {} + getReturnExpressionWhenCalledAtPath(path: ObjectPath) { return path.length === 0 ? this.scope.getReturnExpression() : UNKNOWN_EXPRESSION; } @@ -98,10 +101,6 @@ export default class ArrowFunctionExpression extends NodeBase { } } - mayModifyThisWhenCalledAtPath() { - return false; - } - parseNode(esTreeNode: GenericEsTreeNode) { if (esTreeNode.body.type === NodeType.BlockStatement) { this.body = new this.context.nodeConstructors.BlockStatement( diff --git a/src/ast/nodes/AssignmentExpression.ts b/src/ast/nodes/AssignmentExpression.ts index f982147be34..a515fc95009 100644 --- a/src/ast/nodes/AssignmentExpression.ts +++ b/src/ast/nodes/AssignmentExpression.ts @@ -33,7 +33,7 @@ export default class AssignmentExpression extends NodeBase { | '**='; right!: ExpressionNode; type!: NodeType.tAssignmentExpression; - private deoptimized = false; + protected deoptimized = false; hasEffects(context: HasEffectsContext): boolean { if (!this.deoptimized) this.applyDeoptimizations(); @@ -122,7 +122,7 @@ export default class AssignmentExpression extends NodeBase { } } - private applyDeoptimizations() { + protected applyDeoptimizations() { this.deoptimized = true; this.left.deoptimizePath(EMPTY_PATH); this.right.deoptimizePath(UNKNOWN_PATH); diff --git a/src/ast/nodes/AssignmentPattern.ts b/src/ast/nodes/AssignmentPattern.ts index 06d6ea3268b..0453a6a1008 100644 --- a/src/ast/nodes/AssignmentPattern.ts +++ b/src/ast/nodes/AssignmentPattern.ts @@ -13,6 +13,7 @@ export default class AssignmentPattern extends NodeBase implements PatternNode { left!: PatternNode; right!: ExpressionNode; type!: NodeType.tAssignmentPattern; + protected deoptimized = false; addExportedVariables( variables: Variable[], @@ -21,12 +22,6 @@ export default class AssignmentPattern extends NodeBase implements PatternNode { this.left.addExportedVariables(variables, exportNamesByVariable); } - bind() { - super.bind(); - this.left.deoptimizePath(EMPTY_PATH); - this.right.deoptimizePath(UNKNOWN_PATH); - } - declare(kind: string, init: ExpressionEntity) { return this.left.declare(kind, init); } @@ -47,4 +42,10 @@ export default class AssignmentPattern extends NodeBase implements PatternNode { this.left.render(code, options, { isShorthandProperty }); this.right.render(code, options); } + + protected applyDeoptimizations():void { + this.deoptimized = true; + this.left.deoptimizePath(EMPTY_PATH); + this.right.deoptimizePath(UNKNOWN_PATH); + } } diff --git a/src/ast/nodes/BinaryExpression.ts b/src/ast/nodes/BinaryExpression.ts index eb85497e630..26f632b307d 100644 --- a/src/ast/nodes/BinaryExpression.ts +++ b/src/ast/nodes/BinaryExpression.ts @@ -6,10 +6,10 @@ import { PathTracker, SHARED_RECURSION_TRACKER } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; import ExpressionStatement from './ExpressionStatement'; import { LiteralValue } from './Literal'; import * as NodeType from './NodeType'; +import { LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; const binaryOperators: { diff --git a/src/ast/nodes/BlockStatement.ts b/src/ast/nodes/BlockStatement.ts index f60c30ee830..614d5aa2de5 100644 --- a/src/ast/nodes/BlockStatement.ts +++ b/src/ast/nodes/BlockStatement.ts @@ -4,9 +4,9 @@ import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import BlockScope from '../scopes/BlockScope'; import ChildScope from '../scopes/ChildScope'; import Scope from '../scopes/Scope'; -import { UNKNOWN_EXPRESSION } from '../values'; import ExpressionStatement from './ExpressionStatement'; import * as NodeType from './NodeType'; +import { UNKNOWN_EXPRESSION } from './shared/Expression'; import { IncludeChildren, Node, StatementBase, StatementNode } from './shared/Node'; export default class BlockStatement extends StatementBase { diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index 4478a272612..a3dffadf3f5 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -9,6 +9,7 @@ import { import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { EVENT_CALLED, NodeEvent } from '../NodeEvents'; import { EMPTY_PATH, ObjectPath, @@ -16,11 +17,15 @@ import { SHARED_RECURSION_TRACKER, UNKNOWN_PATH } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../values'; import Identifier from './Identifier'; import MemberExpression from './MemberExpression'; import * as NodeType from './NodeType'; -import { ExpressionEntity } from './shared/Expression'; +import { + ExpressionEntity, + LiteralValueOrUnknown, + UnknownValue, + UNKNOWN_EXPRESSION +} from './shared/Expression'; import { Annotation, ExpressionNode, @@ -36,11 +41,11 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt callee!: ExpressionNode | Super; optional!: boolean; type!: NodeType.tCallExpression; - + protected deoptimized = false; private callOptions!: CallOptions; - private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; + private deoptimizableDependentExpressions: DeoptimizableEntity[] = []; + private expressionsToBeDeoptimized = new Set(); private returnExpression: ExpressionEntity | null = null; - private wasPathDeoptmizedWhileOptimized = false; bind() { super.bind(); @@ -68,54 +73,68 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt ); } } - // ensure the returnExpression is set for the tree-shaking passes - this.getReturnExpression(SHARED_RECURSION_TRACKER); - // This deoptimizes "this" for non-namespace calls until we have a better solution - if ( - this.callee instanceof MemberExpression && - !this.callee.variable && - this.callee.mayModifyThisWhenCalledAtPath([], SHARED_RECURSION_TRACKER) - ) { - this.callee.object.deoptimizePath(UNKNOWN_PATH); - } - for (const argument of this.arguments) { - // This will make sure all properties of parameters behave as "unknown" - argument.deoptimizePath(UNKNOWN_PATH); - } + this.callOptions = { + args: this.arguments, + thisParam: + this.callee instanceof MemberExpression && !this.callee.variable + ? this.callee.object + : null, + withNew: false + }; } deoptimizeCache() { if (this.returnExpression !== UNKNOWN_EXPRESSION) { - this.returnExpression = null; - const returnExpression = this.getReturnExpression(SHARED_RECURSION_TRACKER); - const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized; - if (returnExpression !== UNKNOWN_EXPRESSION) { - // We need to replace here because is possible new expressions are added - // while we are deoptimizing the old ones - this.expressionsToBeDeoptimized = []; - if (this.wasPathDeoptmizedWhileOptimized) { - returnExpression.deoptimizePath(UNKNOWN_PATH); - this.wasPathDeoptmizedWhileOptimized = false; - } - } - for (const expression of expressionsToBeDeoptimized) { + this.returnExpression = UNKNOWN_EXPRESSION; + for (const expression of this.deoptimizableDependentExpressions) { expression.deoptimizeCache(); } + for (const expression of this.expressionsToBeDeoptimized) { + expression.deoptimizePath(UNKNOWN_PATH); + } } } deoptimizePath(path: ObjectPath) { - if (path.length === 0) return; - const trackedEntities = this.context.deoptimizationTracker.getEntities(path); - if (trackedEntities.has(this)) return; - trackedEntities.add(this); - const returnExpression = this.getReturnExpression(SHARED_RECURSION_TRACKER); + if ( + path.length === 0 || + this.context.deoptimizationTracker.trackEntityAtPathAndGetIfTracked(path, this) + ) { + return; + } + const returnExpression = this.getReturnExpression(); if (returnExpression !== UNKNOWN_EXPRESSION) { - this.wasPathDeoptmizedWhileOptimized = true; returnExpression.deoptimizePath(path); } } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + const returnExpression = this.getReturnExpression(recursionTracker); + if (returnExpression === UNKNOWN_EXPRESSION) { + thisParameter.deoptimizePath(UNKNOWN_PATH); + } else { + recursionTracker.withTrackedEntityAtPath( + path, + returnExpression, + () => { + this.expressionsToBeDeoptimized.add(thisParameter); + returnExpression.deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + }, + undefined + ); + } + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -125,42 +144,45 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt if (returnExpression === UNKNOWN_EXPRESSION) { return UnknownValue; } - const trackedEntities = recursionTracker.getEntities(path); - if (trackedEntities.has(returnExpression)) { - return UnknownValue; - } - this.expressionsToBeDeoptimized.push(origin); - trackedEntities.add(returnExpression); - const value = returnExpression.getLiteralValueAtPath(path, recursionTracker, origin); - trackedEntities.delete(returnExpression); - return value; + return recursionTracker.withTrackedEntityAtPath( + path, + returnExpression, + () => { + this.deoptimizableDependentExpressions.push(origin); + return returnExpression.getLiteralValueAtPath(path, recursionTracker, origin); + }, + UnknownValue + ); } getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity - ) { + ): ExpressionEntity { const returnExpression = this.getReturnExpression(recursionTracker); if (this.returnExpression === UNKNOWN_EXPRESSION) { return UNKNOWN_EXPRESSION; } - const trackedEntities = recursionTracker.getEntities(path); - if (trackedEntities.has(returnExpression)) { - return UNKNOWN_EXPRESSION; - } - this.expressionsToBeDeoptimized.push(origin); - trackedEntities.add(returnExpression); - const value = returnExpression.getReturnExpressionWhenCalledAtPath( + return recursionTracker.withTrackedEntityAtPath( path, - recursionTracker, - origin + returnExpression, + () => { + this.deoptimizableDependentExpressions.push(origin); + return returnExpression.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); + }, + UNKNOWN_EXPRESSION ); - trackedEntities.delete(returnExpression); - return value; } hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); for (const argument of this.arguments) { if (argument.hasEffects(context)) return true; } @@ -176,19 +198,17 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return false; - const trackedExpressions = context.accessed.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return this.returnExpression!.hasEffectsWhenAccessedAtPath(path, context); + return ( + !context.accessed.trackEntityAtPathAndGetIfTracked(path, this) && + this.getReturnExpression().hasEffectsWhenAccessedAtPath(path, context) + ); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return true; - const trackedExpressions = context.assigned.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return this.returnExpression!.hasEffectsWhenAssignedAtPath(path, context); + return ( + !context.assigned.trackEntityAtPathAndGetIfTracked(path, this) && + this.getReturnExpression().hasEffectsWhenAssignedAtPath(path, context) + ); } hasEffectsWhenCalledAtPath( @@ -196,16 +216,17 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt callOptions: CallOptions, context: HasEffectsContext ): boolean { - const trackedExpressions = (callOptions.withNew - ? context.instantiated - : context.called - ).getEntities(path, callOptions); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return this.returnExpression!.hasEffectsWhenCalledAtPath(path, callOptions, context); + return ( + !(callOptions.withNew + ? context.instantiated + : context.called + ).trackEntityAtPathAndGetIfTracked(path, callOptions, this) && + this.getReturnExpression().hasEffectsWhenCalledAtPath(path, callOptions, context) + ); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); if (includeChildrenRecursively) { super.include(context, includeChildrenRecursively); if ( @@ -220,18 +241,12 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt this.callee.include(context, false); } this.callee.includeCallArguments(context, this.arguments); - if (!this.returnExpression!.included) { - this.returnExpression!.include(context, false); + const returnExpression = this.getReturnExpression(); + if (!returnExpression.included) { + returnExpression.include(context, false); } } - initialise() { - this.callOptions = { - args: this.arguments, - withNew: false - }; - } - render( code: MagicString, options: RenderOptions, @@ -275,11 +290,31 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } } - private getReturnExpression(recursionTracker: PathTracker): ExpressionEntity { + protected applyDeoptimizations() { + this.deoptimized = true; + const { thisParam } = this.callOptions; + if (thisParam) { + this.callee.deoptimizeThisOnEventAtPath( + EVENT_CALLED, + EMPTY_PATH, + thisParam, + SHARED_RECURSION_TRACKER + ); + } + for (const argument of this.arguments) { + // This will make sure all properties of parameters behave as "unknown" + argument.deoptimizePath(UNKNOWN_PATH); + } + } + + private getReturnExpression( + recursionTracker: PathTracker = SHARED_RECURSION_TRACKER + ): ExpressionEntity { if (this.returnExpression === null) { this.returnExpression = UNKNOWN_EXPRESSION; return (this.returnExpression = this.callee.getReturnExpressionWhenCalledAtPath( EMPTY_PATH, + this.callOptions, recursionTracker, this )); diff --git a/src/ast/nodes/CatchClause.ts b/src/ast/nodes/CatchClause.ts index c7885e8ab80..77e8eb2eca6 100644 --- a/src/ast/nodes/CatchClause.ts +++ b/src/ast/nodes/CatchClause.ts @@ -1,8 +1,8 @@ import CatchScope from '../scopes/CatchScope'; import Scope from '../scopes/Scope'; -import { UNKNOWN_EXPRESSION } from '../values'; import BlockStatement from './BlockStatement'; import * as NodeType from './NodeType'; +import { UNKNOWN_EXPRESSION } from './shared/Expression'; import { GenericEsTreeNode, NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; diff --git a/src/ast/nodes/ClassBody.ts b/src/ast/nodes/ClassBody.ts index 0dc6efa32a5..3f7c55ff760 100644 --- a/src/ast/nodes/ClassBody.ts +++ b/src/ast/nodes/ClassBody.ts @@ -1,42 +1,40 @@ -import { CallOptions } from '../CallOptions'; -import { HasEffectsContext } from '../ExecutionContext'; +import { InclusionContext } from '../ExecutionContext'; import ClassBodyScope from '../scopes/ClassBodyScope'; import Scope from '../scopes/Scope'; -import { EMPTY_PATH, ObjectPath } from '../utils/PathTracker'; import MethodDefinition from './MethodDefinition'; import * as NodeType from './NodeType'; import PropertyDefinition from './PropertyDefinition'; -import { NodeBase } from './shared/Node'; +import ClassNode from './shared/ClassNode'; +import { GenericEsTreeNode, IncludeChildren, NodeBase } from './shared/Node'; export default class ClassBody extends NodeBase { body!: (MethodDefinition | PropertyDefinition)[]; + scope!: ClassBodyScope; type!: NodeType.tClassBody; - private classConstructor!: MethodDefinition | null; - createScope(parentScope: Scope) { - this.scope = new ClassBodyScope(parentScope); + this.scope = new ClassBodyScope(parentScope, this.parent as ClassNode, this.context); } - hasEffectsWhenCalledAtPath( - path: ObjectPath, - callOptions: CallOptions, - context: HasEffectsContext - ) { - if (path.length > 0) return true; - return ( - this.classConstructor !== null && - this.classConstructor.hasEffectsWhenCalledAtPath(EMPTY_PATH, callOptions, context) - ); + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + this.included = true; + this.context.includeVariableInModule(this.scope.thisVariable); + for (const definition of this.body) { + definition.include(context, includeChildrenRecursively); + } } - initialise() { - for (const method of this.body) { - if (method instanceof MethodDefinition && method.kind === 'constructor') { - this.classConstructor = method; - return; - } + parseNode(esTreeNode: GenericEsTreeNode) { + const body: NodeBase[] = (this.body = []); + for (const definition of esTreeNode.body) { + body.push( + new this.context.nodeConstructors[definition.type]( + definition, + this, + definition.static ? this.scope : this.scope.instanceScope + ) + ); } - this.classConstructor = null; + super.parseNode(esTreeNode); } } diff --git a/src/ast/nodes/ConditionalExpression.ts b/src/ast/nodes/ConditionalExpression.ts index 2b1dfea88a3..d11b2869319 100644 --- a/src/ast/nodes/ConditionalExpression.ts +++ b/src/ast/nodes/ConditionalExpression.ts @@ -11,17 +11,11 @@ import { removeAnnotations } from '../../utils/treeshakeNode'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { - EMPTY_PATH, - ObjectPath, - PathTracker, - SHARED_RECURSION_TRACKER, - UNKNOWN_PATH -} from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; +import { NodeEvent } from '../NodeEvents'; +import { EMPTY_PATH, ObjectPath, PathTracker, SHARED_RECURSION_TRACKER, UNKNOWN_PATH } from '../utils/PathTracker'; import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; -import { ExpressionEntity } from './shared/Expression'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { MultiExpression } from './shared/MultiExpression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; import SpreadElement from './SpreadElement'; @@ -35,42 +29,38 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; private isBranchResolutionAnalysed = false; private usedBranch: ExpressionNode | null = null; - private wasPathDeoptimizedWhileOptimized = false; - - bind() { - super.bind(); - // ensure the usedBranch is set for the tree-shaking passes - this.getUsedBranch(); - } deoptimizeCache() { if (this.usedBranch !== null) { const unusedBranch = this.usedBranch === this.consequent ? this.alternate : this.consequent; this.usedBranch = null; - const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized; - this.expressionsToBeDeoptimized = []; - if (this.wasPathDeoptimizedWhileOptimized) { - unusedBranch.deoptimizePath(UNKNOWN_PATH); - } - for (const expression of expressionsToBeDeoptimized) { + unusedBranch.deoptimizePath(UNKNOWN_PATH); + for (const expression of this.expressionsToBeDeoptimized) { expression.deoptimizeCache(); } } } deoptimizePath(path: ObjectPath) { - if (path.length > 0) { - const usedBranch = this.getUsedBranch(); - if (usedBranch === null) { - this.consequent.deoptimizePath(path); - this.alternate.deoptimizePath(path); - } else { - this.wasPathDeoptimizedWhileOptimized = true; - usedBranch.deoptimizePath(path); - } + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { + this.consequent.deoptimizePath(path); + this.alternate.deoptimizePath(path); + } else { + usedBranch.deoptimizePath(path); } } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.consequent.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + this.alternate.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -84,47 +74,64 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { const usedBranch = this.getUsedBranch(); if (usedBranch === null) return new MultiExpression([ - this.consequent.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin), - this.alternate.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin) + this.consequent.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ), + this.alternate.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ) ]); this.expressionsToBeDeoptimized.push(origin); - return usedBranch.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); + return usedBranch.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); } hasEffects(context: HasEffectsContext): boolean { if (this.test.hasEffects(context)) return true; - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { return this.consequent.hasEffects(context) || this.alternate.hasEffects(context); } - return this.usedBranch.hasEffects(context); + return usedBranch.hasEffects(context); } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return false; - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { return ( this.consequent.hasEffectsWhenAccessedAtPath(path, context) || this.alternate.hasEffectsWhenAccessedAtPath(path, context) ); } - return this.usedBranch.hasEffectsWhenAccessedAtPath(path, context); + return usedBranch.hasEffectsWhenAccessedAtPath(path, context); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return true; - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { return ( this.consequent.hasEffectsWhenAssignedAtPath(path, context) || this.alternate.hasEffectsWhenAssignedAtPath(path, context) ); } - return this.usedBranch.hasEffectsWhenAssignedAtPath(path, context); + return usedBranch.hasEffectsWhenAssignedAtPath(path, context); } hasEffectsWhenCalledAtPath( @@ -132,36 +139,35 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz callOptions: CallOptions, context: HasEffectsContext ): boolean { - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { return ( this.consequent.hasEffectsWhenCalledAtPath(path, callOptions, context) || this.alternate.hasEffectsWhenCalledAtPath(path, callOptions, context) ); } - return this.usedBranch.hasEffectsWhenCalledAtPath(path, callOptions, context); + return usedBranch.hasEffectsWhenCalledAtPath(path, callOptions, context); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { this.included = true; - if ( - includeChildrenRecursively || - this.test.shouldBeIncluded(context) || - this.usedBranch === null - ) { + const usedBranch = this.getUsedBranch(); + if (includeChildrenRecursively || this.test.shouldBeIncluded(context) || usedBranch === null) { this.test.include(context, includeChildrenRecursively); this.consequent.include(context, includeChildrenRecursively); this.alternate.include(context, includeChildrenRecursively); } else { - this.usedBranch.include(context, includeChildrenRecursively); + usedBranch.include(context, includeChildrenRecursively); } } includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { this.consequent.includeCallArguments(context, args); this.alternate.includeCallArguments(context, args); } else { - this.usedBranch.includeCallArguments(context, args); + usedBranch.includeCallArguments(context, args); } } @@ -170,6 +176,7 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz options: RenderOptions, { renderedParentType, isCalleeOfRenderedParent, preventASI }: NodeRenderOptions = BLANK ) { + const usedBranch = this.getUsedBranch(); if (!this.test.included) { const colonPos = findFirstOccurrenceOutsideComment(code.original, ':', this.consequent.end); const inclusionStart = findNonWhiteSpace( @@ -179,14 +186,14 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz : colonPos) + 1 ); if (preventASI) { - removeLineBreaks(code, inclusionStart, this.usedBranch!.start); + removeLineBreaks(code, inclusionStart, usedBranch!.start); } code.remove(this.start, inclusionStart); if (this.consequent.included) { code.remove(colonPos, this.end); } removeAnnotations(this, code); - this.usedBranch!.render(code, options, { + usedBranch!.render(code, options, { isCalleeOfRenderedParent: renderedParentType ? isCalleeOfRenderedParent : (this.parent as CallExpression).callee === this, diff --git a/src/ast/nodes/ForInStatement.ts b/src/ast/nodes/ForInStatement.ts index 609f0d022c2..4c92457fb19 100644 --- a/src/ast/nodes/ForInStatement.ts +++ b/src/ast/nodes/ForInStatement.ts @@ -14,19 +14,14 @@ export default class ForInStatement extends StatementBase { left!: VariableDeclaration | PatternNode; right!: ExpressionNode; type!: NodeType.tForInStatement; - - bind() { - this.left.bind(); - this.left.deoptimizePath(EMPTY_PATH); - this.right.bind(); - this.body.bind(); - } + protected deoptimized = false; createScope(parentScope: Scope) { this.scope = new BlockScope(parentScope); } hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); if ( (this.left && (this.left.hasEffects(context) || @@ -48,9 +43,9 @@ export default class ForInStatement extends StatementBase { } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); this.included = true; this.left.include(context, includeChildrenRecursively || true); - this.left.deoptimizePath(EMPTY_PATH); this.right.include(context, includeChildrenRecursively); const { brokenFlow } = context; this.body.includeAsSingleStatement(context, includeChildrenRecursively); @@ -66,4 +61,9 @@ export default class ForInStatement extends StatementBase { } this.body.render(code, options); } + + protected applyDeoptimizations():void { + this.deoptimized = true; + this.left.deoptimizePath(EMPTY_PATH); + } } diff --git a/src/ast/nodes/ForOfStatement.ts b/src/ast/nodes/ForOfStatement.ts index 65527662fc0..030d36ece9e 100644 --- a/src/ast/nodes/ForOfStatement.ts +++ b/src/ast/nodes/ForOfStatement.ts @@ -15,27 +15,22 @@ export default class ForOfStatement extends StatementBase { left!: VariableDeclaration | PatternNode; right!: ExpressionNode; type!: NodeType.tForOfStatement; - - bind() { - this.left.bind(); - this.left.deoptimizePath(EMPTY_PATH); - this.right.bind(); - this.body.bind(); - } + protected deoptimized = false; createScope(parentScope: Scope) { this.scope = new BlockScope(parentScope); } hasEffects(): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); // Placeholder until proper Symbol.Iterator support return true; } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); this.included = true; this.left.include(context, includeChildrenRecursively || true); - this.left.deoptimizePath(EMPTY_PATH); this.right.include(context, includeChildrenRecursively); const { brokenFlow } = context; this.body.includeAsSingleStatement(context, includeChildrenRecursively); @@ -51,4 +46,9 @@ export default class ForOfStatement extends StatementBase { } this.body.render(code, options); } + + protected applyDeoptimizations(): void { + this.deoptimized = true; + this.left.deoptimizePath(EMPTY_PATH); + } } diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index 0c1394198ef..6fabee91ebf 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -6,14 +6,14 @@ import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { NodeEvent } from '../NodeEvents'; import FunctionScope from '../scopes/FunctionScope'; import { EMPTY_PATH, ObjectPath, PathTracker } from '../utils/PathTracker'; -import { LiteralValueOrUnknown } from '../values'; import GlobalVariable from '../variables/GlobalVariable'; import LocalVariable from '../variables/LocalVariable'; import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; -import { ExpressionEntity } from './shared/Expression'; +import { ExpressionEntity, LiteralValueOrUnknown } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; import SpreadElement from './SpreadElement'; @@ -25,7 +25,7 @@ export default class Identifier extends NodeBase implements PatternNode { type!: NodeType.tIdentifier; variable: Variable | null = null; - private bound = false; + protected deoptimized = false; addExportedVariables( variables: Variable[], @@ -37,19 +37,10 @@ export default class Identifier extends NodeBase implements PatternNode { } bind() { - if (this.bound) return; - this.bound = true; if (this.variable === null && isReference(this, this.parent as any)) { this.variable = this.scope.findVariable(this.name); this.variable.addReference(this); } - if ( - this.variable !== null && - this.variable instanceof LocalVariable && - this.variable.additionalInitializers !== null - ) { - this.variable.consolidateInitializers(); - } } declare(kind: string, init: ExpressionEntity) { @@ -79,32 +70,45 @@ export default class Identifier extends NodeBase implements PatternNode { } deoptimizePath(path: ObjectPath) { - if (!this.bound) this.bind(); if (path.length === 0 && !this.scope.contains(this.name)) { this.disallowImportReassignment(); } this.variable!.deoptimizePath(path); } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.variable!.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, origin: DeoptimizableEntity ): LiteralValueOrUnknown { - if (!this.bound) this.bind(); return this.variable!.getLiteralValueAtPath(path, recursionTracker, origin); } getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity - ) { - if (!this.bound) this.bind(); - return this.variable!.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); + ): ExpressionEntity { + return this.variable!.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); } hasEffects(): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); return ( (this.context.options.treeshake as NormalizedTreeshakingOptions).unknownGlobalSideEffects && this.variable instanceof GlobalVariable && @@ -129,6 +133,7 @@ export default class Identifier extends NodeBase implements PatternNode { } include() { + if (!this.deoptimized) this.applyDeoptimizations(); if (!this.included) { this.included = true; if (this.variable !== null) { @@ -141,12 +146,6 @@ export default class Identifier extends NodeBase implements PatternNode { this.variable!.includeCallArguments(context, args); } - mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker) { - return this.variable - ? this.variable.mayModifyThisWhenCalledAtPath(path, recursionTracker) - : true; - } - render( code: MagicString, _options: RenderOptions, @@ -175,6 +174,16 @@ export default class Identifier extends NodeBase implements PatternNode { } } + protected applyDeoptimizations() { + this.deoptimized = true; + if ( + this.variable !== null && + this.variable instanceof LocalVariable + ) { + this.variable.consolidateInitializers(); + } + } + private disallowImportReassignment() { return this.context.error( { diff --git a/src/ast/nodes/IfStatement.ts b/src/ast/nodes/IfStatement.ts index 23667804428..4231e1382d3 100644 --- a/src/ast/nodes/IfStatement.ts +++ b/src/ast/nodes/IfStatement.ts @@ -5,10 +5,10 @@ import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { BROKEN_FLOW_NONE, HasEffectsContext, InclusionContext } from '../ExecutionContext'; import TrackingScope from '../scopes/TrackingScope'; import { EMPTY_PATH, SHARED_RECURSION_TRACKER } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; import BlockStatement from './BlockStatement'; import Identifier from './Identifier'; import * as NodeType from './NodeType'; +import { LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { ExpressionNode, GenericEsTreeNode, diff --git a/src/ast/nodes/ImportDeclaration.ts b/src/ast/nodes/ImportDeclaration.ts index 450381bad86..0f7d177f633 100644 --- a/src/ast/nodes/ImportDeclaration.ts +++ b/src/ast/nodes/ImportDeclaration.ts @@ -13,6 +13,7 @@ export default class ImportDeclaration extends NodeBase { specifiers!: (ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier)[]; type!: NodeType.tImportDeclaration; + // Do not bind specifiers bind() {} hasEffects() { diff --git a/src/ast/nodes/Literal.ts b/src/ast/nodes/Literal.ts index fc55458ea11..d890950c2ea 100644 --- a/src/ast/nodes/Literal.ts +++ b/src/ast/nodes/Literal.ts @@ -6,12 +6,10 @@ import { getLiteralMembersForValue, getMemberReturnExpressionWhenCalled, hasMemberEffectWhenCalled, - LiteralValueOrUnknown, - MemberDescription, - UnknownValue, - UNKNOWN_EXPRESSION + MemberDescription } from '../values'; import * as NodeType from './NodeType'; +import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from './shared/Expression'; import { GenericEsTreeNode, NodeBase } from './shared/Node'; export type LiteralValue = string | boolean | null | number | RegExp | undefined; @@ -26,6 +24,8 @@ export default class Literal extends Node private members!: { [key: string]: MemberDescription }; + deoptimizeThisOnEventAtPath() {} + getLiteralValueAtPath(path: ObjectPath): LiteralValueOrUnknown { if ( path.length > 0 || @@ -62,7 +62,7 @@ export default class Literal extends Node context: HasEffectsContext ): boolean { if (path.length === 1) { - return hasMemberEffectWhenCalled(this.members, path[0], this.included, callOptions, context); + return hasMemberEffectWhenCalled(this.members, path[0], callOptions, context); } return true; } diff --git a/src/ast/nodes/LogicalExpression.ts b/src/ast/nodes/LogicalExpression.ts index e19ba265c9e..9faf815cdcd 100644 --- a/src/ast/nodes/LogicalExpression.ts +++ b/src/ast/nodes/LogicalExpression.ts @@ -11,6 +11,7 @@ import { removeAnnotations } from '../../utils/treeshakeNode'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { NodeEvent } from '../NodeEvents'; import { EMPTY_PATH, ObjectPath, @@ -18,10 +19,9 @@ import { SHARED_RECURSION_TRACKER, UNKNOWN_PATH } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; -import { ExpressionEntity } from './shared/Expression'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { MultiExpression } from './shared/MultiExpression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; @@ -36,25 +36,14 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable // We collect deoptimization information if usedBranch !== null private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; private isBranchResolutionAnalysed = false; - private unusedBranch: ExpressionNode | null = null; private usedBranch: ExpressionNode | null = null; - private wasPathDeoptimizedWhileOptimized = false; - - bind() { - super.bind(); - // ensure the usedBranch is set for the tree-shaking passes - this.getUsedBranch(); - } deoptimizeCache() { if (this.usedBranch !== null) { + const unusedBranch = this.usedBranch === this.left ? this.right : this.left; this.usedBranch = null; - const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized; - this.expressionsToBeDeoptimized = []; - if (this.wasPathDeoptimizedWhileOptimized) { - this.unusedBranch!.deoptimizePath(UNKNOWN_PATH); - } - for (const expression of expressionsToBeDeoptimized) { + unusedBranch.deoptimizePath(UNKNOWN_PATH); + for (const expression of this.expressionsToBeDeoptimized) { expression.deoptimizeCache(); } } @@ -66,11 +55,20 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable this.left.deoptimizePath(path); this.right.deoptimizePath(path); } else { - this.wasPathDeoptimizedWhileOptimized = true; usedBranch.deoptimizePath(path); } } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.left.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + this.right.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -84,49 +82,55 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { const usedBranch = this.getUsedBranch(); if (usedBranch === null) return new MultiExpression([ - this.left.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin), - this.right.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin) + this.left.getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin), + this.right.getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin) ]); this.expressionsToBeDeoptimized.push(origin); - return usedBranch.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); + return usedBranch.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); } hasEffects(context: HasEffectsContext): boolean { if (this.left.hasEffects(context)) { return true; } - if (this.usedBranch !== this.left) { + if (this.getUsedBranch() !== this.left) { return this.right.hasEffects(context); } return false; } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return false; - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { return ( this.left.hasEffectsWhenAccessedAtPath(path, context) || this.right.hasEffectsWhenAccessedAtPath(path, context) ); } - return this.usedBranch.hasEffectsWhenAccessedAtPath(path, context); + return usedBranch.hasEffectsWhenAccessedAtPath(path, context); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return true; - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { return ( this.left.hasEffectsWhenAssignedAtPath(path, context) || this.right.hasEffectsWhenAssignedAtPath(path, context) ); } - return this.usedBranch.hasEffectsWhenAssignedAtPath(path, context); + return usedBranch.hasEffectsWhenAssignedAtPath(path, context); } hasEffectsWhenCalledAtPath( @@ -134,26 +138,28 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable callOptions: CallOptions, context: HasEffectsContext ): boolean { - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { return ( this.left.hasEffectsWhenCalledAtPath(path, callOptions, context) || this.right.hasEffectsWhenCalledAtPath(path, callOptions, context) ); } - return this.usedBranch.hasEffectsWhenCalledAtPath(path, callOptions, context); + return usedBranch.hasEffectsWhenCalledAtPath(path, callOptions, context); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { this.included = true; + const usedBranch = this.getUsedBranch(); if ( includeChildrenRecursively || - (this.usedBranch === this.right && this.left.shouldBeIncluded(context)) || - this.usedBranch === null + (usedBranch === this.right && this.left.shouldBeIncluded(context)) || + usedBranch === null ) { this.left.include(context, includeChildrenRecursively); this.right.include(context, includeChildrenRecursively); } else { - this.usedBranch.include(context, includeChildrenRecursively); + usedBranch.include(context, includeChildrenRecursively); } } @@ -178,7 +184,7 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable code.remove(operatorPos, this.end); } removeAnnotations(this, code); - this.usedBranch!.render(code, options, { + this.getUsedBranch()!.render(code, options, { isCalleeOfRenderedParent: renderedParentType ? isCalleeOfRenderedParent : (this.parent as CallExpression).callee === this, @@ -198,17 +204,12 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable if (leftValue === UnknownValue) { return null; } else { - if ( + this.usedBranch = (this.operator === '||' && leftValue) || (this.operator === '&&' && !leftValue) || (this.operator === '??' && leftValue != null) - ) { - this.usedBranch = this.left; - this.unusedBranch = this.right; - } else { - this.usedBranch = this.right; - this.unusedBranch = this.left; - } + ? this.left + : this.right; } } return this.usedBranch; diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index 240c487ad7b..c5401379781 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -6,6 +6,7 @@ import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { EVENT_ACCESSED, EVENT_ASSIGNED, NodeEvent } from '../NodeEvents'; import { EMPTY_PATH, ObjectPath, @@ -15,14 +16,15 @@ import { UnknownKey, UNKNOWN_PATH } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; import ExternalVariable from '../variables/ExternalVariable'; import NamespaceVariable from '../variables/NamespaceVariable'; import Variable from '../variables/Variable'; +import AssignmentExpression from './AssignmentExpression'; import Identifier from './Identifier'; import Literal from './Literal'; import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from './shared/Expression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; import SpreadElement from './SpreadElement'; import Super from './Super'; @@ -78,14 +80,12 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE propertyKey!: ObjectPathKey | null; type!: NodeType.tMemberExpression; variable: Variable | null = null; - + protected deoptimized = false; private bound = false; private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; private replacement: string | null = null; - private wasPathDeoptimizedWhileOptimized = false; bind() { - if (this.bound) return; this.bound = true; const path = getPathIfNotComputed(this); const baseVariable = path && this.scope.findVariable(path[0].key); @@ -101,8 +101,6 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE } } else { super.bind(); - // ensure the propertyKey is set for the tree-shaking passes - this.getPropertyKey(); } } @@ -110,34 +108,50 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized; this.expressionsToBeDeoptimized = []; this.propertyKey = UnknownKey; - if (this.wasPathDeoptimizedWhileOptimized) { - this.object.deoptimizePath(UNKNOWN_PATH); - } + this.object.deoptimizePath(UNKNOWN_PATH); for (const expression of expressionsToBeDeoptimized) { expression.deoptimizeCache(); } } deoptimizePath(path: ObjectPath) { - if (!this.bound) this.bind(); if (path.length === 0) this.disallowNamespaceReassignment(); if (this.variable) { this.variable.deoptimizePath(path); } else if (!this.replacement) { - this.wasPathDeoptimizedWhileOptimized = true; this.object.deoptimizePath([this.getPropertyKey(), ...path]); } } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ): void { + if (this.variable) { + this.variable.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } else if (!this.replacement) { + this.object.deoptimizeThisOnEventAtPath( + event, + [this.getPropertyKey(), ...path], + thisParameter, + recursionTracker + ); + } + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, origin: DeoptimizableEntity ): LiteralValueOrUnknown { - if (!this.bound) this.bind(); if (this.variable !== null) { return this.variable.getLiteralValueAtPath(path, recursionTracker, origin); } + if (this.replacement) { + return UnknownValue; + } this.expressionsToBeDeoptimized.push(origin); return this.object.getLiteralValueAtPath( [this.getPropertyKey(), ...path], @@ -148,46 +162,67 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity - ) { - if (!this.bound) this.bind(); + ): ExpressionEntity { if (this.variable !== null) { - return this.variable.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); + return this.variable.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); + } + if (this.replacement) { + return UNKNOWN_EXPRESSION; } this.expressionsToBeDeoptimized.push(origin); return this.object.getReturnExpressionWhenCalledAtPath( [this.getPropertyKey(), ...path], + callOptions, recursionTracker, origin ); } hasEffects(context: HasEffectsContext): boolean { - const propertyReadSideEffects = (this.context.options.treeshake as NormalizedTreeshakingOptions) - .propertyReadSideEffects; + if (!this.deoptimized) this.applyDeoptimizations(); + const { propertyReadSideEffects } = this.context.options + .treeshake as NormalizedTreeshakingOptions; return ( - propertyReadSideEffects === 'always' || this.property.hasEffects(context) || this.object.hasEffects(context) || - (propertyReadSideEffects && - this.object.hasEffectsWhenAccessedAtPath([this.propertyKey!], 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))) ); } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return false; if (this.variable !== null) { return this.variable.hasEffectsWhenAccessedAtPath(path, context); } - return this.object.hasEffectsWhenAccessedAtPath([this.propertyKey!, ...path], context); + if (this.replacement) { + return true; + } + return this.object.hasEffectsWhenAccessedAtPath([this.getPropertyKey(), ...path], context); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { if (this.variable !== null) { return this.variable.hasEffectsWhenAssignedAtPath(path, context); } - return this.object.hasEffectsWhenAssignedAtPath([this.propertyKey!, ...path], context); + if (this.replacement) { + return true; + } + return this.object.hasEffectsWhenAssignedAtPath([this.getPropertyKey(), ...path], context); } hasEffectsWhenCalledAtPath( @@ -198,14 +233,18 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE if (this.variable !== null) { return this.variable.hasEffectsWhenCalledAtPath(path, callOptions, context); } + if (this.replacement) { + return true; + } return this.object.hasEffectsWhenCalledAtPath( - [this.propertyKey!, ...path], + [this.getPropertyKey(), ...path], callOptions, context ); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); if (!this.included) { this.included = true; if (this.variable !== null) { @@ -228,16 +267,6 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE this.propertyKey = getResolvablePropertyKey(this); } - mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker) { - if (this.variable) { - return this.variable.mayModifyThisWhenCalledAtPath(path, recursionTracker); - } - return this.object.mayModifyThisWhenCalledAtPath( - [this.propertyKey as ObjectPathKey].concat(path), - recursionTracker - ); - } - render( code: MagicString, options: RenderOptions, @@ -270,6 +299,36 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE } } + protected applyDeoptimizations() { + this.deoptimized = 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) + ) { + // 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 + ); + } + } + } + private disallowNamespaceReassignment() { if (this.object instanceof Identifier) { const variable = this.scope.findVariable(this.object.name); diff --git a/src/ast/nodes/MethodDefinition.ts b/src/ast/nodes/MethodDefinition.ts index 7ce5195cc2a..07b0e88366a 100644 --- a/src/ast/nodes/MethodDefinition.ts +++ b/src/ast/nodes/MethodDefinition.ts @@ -1,30 +1,13 @@ -import { CallOptions } from '../CallOptions'; -import { HasEffectsContext } from '../ExecutionContext'; -import { EMPTY_PATH, ObjectPath } from '../utils/PathTracker'; import FunctionExpression from './FunctionExpression'; import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; -import { ExpressionNode, NodeBase } from './shared/Node'; +import MethodBase from './shared/MethodBase'; +import { ExpressionNode } from './shared/Node'; -export default class MethodDefinition extends NodeBase { - computed!: boolean; +export default class MethodDefinition extends MethodBase { key!: ExpressionNode | PrivateIdentifier; kind!: 'constructor' | 'method' | 'get' | 'set'; static!: boolean; type!: NodeType.tMethodDefinition; value!: FunctionExpression; - - hasEffects(context: HasEffectsContext) { - return this.key.hasEffects(context); - } - - hasEffectsWhenCalledAtPath( - path: ObjectPath, - callOptions: CallOptions, - context: HasEffectsContext - ) { - return ( - path.length > 0 || this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, callOptions, context) - ); - } } diff --git a/src/ast/nodes/NewExpression.ts b/src/ast/nodes/NewExpression.ts index 79633447566..acff4ce3630 100644 --- a/src/ast/nodes/NewExpression.ts +++ b/src/ast/nodes/NewExpression.ts @@ -9,18 +9,11 @@ export default class NewExpression extends NodeBase { arguments!: ExpressionNode[]; callee!: ExpressionNode; type!: NodeType.tNewExpression; - + protected deoptimized = false; private callOptions!: CallOptions; - bind() { - super.bind(); - for (const argument of this.arguments) { - // This will make sure all properties of parameters behave as "unknown" - argument.deoptimizePath(UNKNOWN_PATH); - } - } - hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); for (const argument of this.arguments) { if (argument.hasEffects(context)) return true; } @@ -36,13 +29,22 @@ export default class NewExpression extends NodeBase { } hasEffectsWhenAccessedAtPath(path: ObjectPath) { - return path.length > 1; + return path.length > 0; } initialise() { this.callOptions = { args: this.arguments, + thisParam: null, withNew: true }; } + + protected applyDeoptimizations(): void { + this.deoptimized = true; + for (const argument of this.arguments) { + // This will make sure all properties of parameters behave as "unknown" + argument.deoptimizePath(UNKNOWN_PATH); + } + } } diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index d4190a3d4d0..561ef146cf8 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -1,96 +1,57 @@ import MagicString from 'magic-string'; import { BLANK } from '../../utils/blank'; -import { getOrCreate } from '../../utils/getOrCreate'; import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; +import { NodeEvent } from '../NodeEvents'; import { EMPTY_PATH, ObjectPath, PathTracker, SHARED_RECURSION_TRACKER, - UNKNOWN_PATH + UnknownKey } from '../utils/PathTracker'; -import { - getMemberReturnExpressionWhenCalled, - hasMemberEffectWhenCalled, - LiteralValueOrUnknown, - objectMembers, - UnknownValue, - UNKNOWN_EXPRESSION -} from '../values'; import Identifier from './Identifier'; import Literal from './Literal'; import * as NodeType from './NodeType'; import Property from './Property'; -import { ExpressionEntity } from './shared/Expression'; +import { + ExpressionEntity, + LiteralValueOrUnknown, + UnknownValue, + UNKNOWN_EXPRESSION +} from './shared/Expression'; import { NodeBase } from './shared/Node'; +import { ObjectEntity, ObjectProperty } from './shared/ObjectEntity'; +import { OBJECT_PROTOTYPE } from './shared/ObjectPrototype'; import SpreadElement from './SpreadElement'; -interface PropertyMap { - [key: string]: { - exactMatchRead: Property | null; - exactMatchWrite: Property | null; - propertiesRead: (Property | SpreadElement)[]; - propertiesWrite: Property[]; - }; -} - export default class ObjectExpression extends NodeBase implements DeoptimizableEntity { properties!: (Property | SpreadElement)[]; type!: NodeType.tObjectExpression; + private objectEntity: ObjectEntity | null = null; - private deoptimizedPaths = new Set(); - - // We collect deoptimization information if we can resolve a computed property access - private expressionsToBeDeoptimized = new Map(); - private hasUnknownDeoptimizedProperty = false; - private propertyMap: PropertyMap | null = null; - private unmatchablePropertiesRead: (Property | SpreadElement)[] = []; - private unmatchablePropertiesWrite: Property[] = []; - - bind() { - super.bind(); - // ensure the propertyMap is set for the tree-shaking passes - this.getPropertyMap(); - } - - // We could also track this per-property but this would quickly become much more complex deoptimizeCache() { - if (!this.hasUnknownDeoptimizedProperty) this.deoptimizeAllProperties(); + this.getObjectEntity().deoptimizeAllProperties(); } deoptimizePath(path: ObjectPath) { - if (this.hasUnknownDeoptimizedProperty) return; - const propertyMap = this.getPropertyMap(); - const key = path[0]; - if (path.length === 1) { - if (typeof key !== 'string') { - this.deoptimizeAllProperties(); - return; - } - if (!this.deoptimizedPaths.has(key)) { - this.deoptimizedPaths.add(key); + this.getObjectEntity().deoptimizePath(path); + } - // we only deoptimizeCache exact matches as in all other cases, - // we do not return a literal value or return expression - const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized.get(key); - if (expressionsToBeDeoptimized) { - for (const expression of expressionsToBeDeoptimized) { - expression.deoptimizeCache(); - } - } - } - } - const subPath = path.length === 1 ? UNKNOWN_PATH : path.slice(1); - for (const property of typeof key === 'string' - ? propertyMap[key] - ? propertyMap[key].propertiesRead - : [] - : this.properties) { - property.deoptimizePath(subPath); - } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.getObjectEntity().deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); } getLiteralValueAtPath( @@ -98,135 +59,29 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE recursionTracker: PathTracker, origin: DeoptimizableEntity ): LiteralValueOrUnknown { - const propertyMap = this.getPropertyMap(); - const key = path[0]; - - if ( - path.length === 0 || - this.hasUnknownDeoptimizedProperty || - typeof key !== 'string' || - this.deoptimizedPaths.has(key) - ) { - return UnknownValue; - } - - if ( - path.length === 1 && - !propertyMap[key] && - !objectMembers[key] && - this.unmatchablePropertiesRead.length === 0 - ) { - getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); - return undefined; - } - - if ( - !propertyMap[key] || - propertyMap[key].exactMatchRead === null || - propertyMap[key].propertiesRead.length > 1 - ) { - return UnknownValue; - } - - getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); - return propertyMap[key].exactMatchRead!.getLiteralValueAtPath( - path.slice(1), - recursionTracker, - origin - ); + return this.getObjectEntity().getLiteralValueAtPath(path, recursionTracker, origin); } getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { - const propertyMap = this.getPropertyMap(); - const key = path[0]; - - if ( - path.length === 0 || - this.hasUnknownDeoptimizedProperty || - typeof key !== 'string' || - this.deoptimizedPaths.has(key) - ) { - return UNKNOWN_EXPRESSION; - } - - if ( - path.length === 1 && - objectMembers[key] && - this.unmatchablePropertiesRead.length === 0 && - (!propertyMap[key] || propertyMap[key].exactMatchRead === null) - ) { - return getMemberReturnExpressionWhenCalled(objectMembers, key); - } - - if ( - !propertyMap[key] || - propertyMap[key].exactMatchRead === null || - propertyMap[key].propertiesRead.length > 1 - ) { - return UNKNOWN_EXPRESSION; - } - - getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); - return propertyMap[key].exactMatchRead!.getReturnExpressionWhenCalledAtPath( - path.slice(1), + return this.getObjectEntity().getReturnExpressionWhenCalledAtPath( + path, + callOptions, recursionTracker, origin ); } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { - if (path.length === 0) return false; - const key = path[0]; - const propertyMap = this.propertyMap!; - if ( - path.length > 1 && - (this.hasUnknownDeoptimizedProperty || - typeof key !== 'string' || - this.deoptimizedPaths.has(key) || - !propertyMap[key] || - propertyMap[key].exactMatchRead === null) - ) - return true; - - const subPath = path.slice(1); - for (const property of typeof key !== 'string' - ? this.properties - : propertyMap[key] - ? propertyMap[key].propertiesRead - : []) { - if (property.hasEffectsWhenAccessedAtPath(subPath, context)) return true; - } - return false; + return this.getObjectEntity().hasEffectsWhenAccessedAtPath(path, context); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext) { - const key = path[0]; - const propertyMap = this.propertyMap!; - if ( - path.length > 1 && - (this.hasUnknownDeoptimizedProperty || - this.deoptimizedPaths.has(key as string) || - !propertyMap[key as string] || - propertyMap[key as string].exactMatchRead === null) - ) { - return true; - } - - const subPath = path.slice(1); - for (const property of typeof key !== 'string' - ? this.properties - : path.length > 1 - ? propertyMap[key].propertiesRead - : propertyMap[key] - ? propertyMap[key].propertiesWrite - : []) { - if (property.hasEffectsWhenAssignedAtPath(subPath, context)) return true; - } - return false; + return this.getObjectEntity().hasEffectsWhenAssignedAtPath(path, context); } hasEffectsWhenCalledAtPath( @@ -234,36 +89,7 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE callOptions: CallOptions, context: HasEffectsContext ): boolean { - const key = path[0]; - if ( - typeof key !== 'string' || - this.hasUnknownDeoptimizedProperty || - this.deoptimizedPaths.has(key) || - (this.propertyMap![key] - ? !this.propertyMap![key].exactMatchRead - : path.length > 1 || !objectMembers[key]) - ) { - return true; - } - const subPath = path.slice(1); - if (this.propertyMap![key]) { - for (const property of this.propertyMap![key].propertiesRead) { - if (property.hasEffectsWhenCalledAtPath(subPath, callOptions, context)) return true; - } - } - if (path.length === 1 && objectMembers[key]) - return hasMemberEffectWhenCalled(objectMembers, key, this.included, callOptions, context); - return false; - } - - mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker) { - if (!path.length || typeof path[0] !== 'string') { - return true; - } - const property = this.getPropertyMap()[path[0]]?.exactMatchRead; - return property - ? property.value.mayModifyThisWhenCalledAtPath(path.slice(1), recursionTracker) - : true; + return this.getObjectEntity().hasEffectsWhenCalledAtPath(path, callOptions, context); } render( @@ -282,73 +108,41 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE } } - private deoptimizeAllProperties() { - this.hasUnknownDeoptimizedProperty = true; - for (const property of this.properties) { - property.deoptimizePath(UNKNOWN_PATH); - } - for (const expressionsToBeDeoptimized of this.expressionsToBeDeoptimized.values()) { - for (const expression of expressionsToBeDeoptimized) { - expression.deoptimizeCache(); - } - } - } - - private getPropertyMap(): PropertyMap { - if (this.propertyMap !== null) { - return this.propertyMap; + private getObjectEntity(): ObjectEntity { + if (this.objectEntity !== null) { + return this.objectEntity; } - const propertyMap = (this.propertyMap = Object.create(null)); - for (let index = this.properties.length - 1; index >= 0; index--) { - const property = this.properties[index]; + const properties: ObjectProperty[] = []; + for (const property of this.properties) { if (property instanceof SpreadElement) { - this.unmatchablePropertiesRead.push(property); + properties.push({ kind: 'init', key: UnknownKey, property }); continue; } - const isWrite = property.kind !== 'get'; - const isRead = property.kind !== 'set'; let key: string; - let unmatchable = false; if (property.computed) { const keyValue = property.key.getLiteralValueAtPath( EMPTY_PATH, SHARED_RECURSION_TRACKER, this ); - if (keyValue === UnknownValue) unmatchable = true; - key = String(keyValue); - } else if (property.key instanceof Identifier) { - key = property.key.name; - } else { - key = String((property.key as Literal).value); - } - if (unmatchable || (key === '__proto__' && !property.computed)) { - if (isRead) { - this.unmatchablePropertiesRead.push(property); + if (keyValue === UnknownValue) { + properties.push({ kind: property.kind, key: UnknownKey, property }); + continue; } else { - this.unmatchablePropertiesWrite.push(property); + key = String(keyValue); + } + } else { + key = + property.key instanceof Identifier + ? property.key.name + : String((property.key as Literal).value); + if (key === '__proto__' && property.kind === 'init') { + properties.unshift({ kind: 'init', key: UnknownKey, property: UNKNOWN_EXPRESSION }); + continue; } - continue; - } - const propertyMapProperty = propertyMap[key]; - if (!propertyMapProperty) { - propertyMap[key] = { - exactMatchRead: isRead ? property : null, - exactMatchWrite: isWrite ? property : null, - propertiesRead: isRead ? [property, ...this.unmatchablePropertiesRead] : [], - propertiesWrite: isWrite && !isRead ? [property, ...this.unmatchablePropertiesWrite] : [] - }; - continue; - } - if (isRead && propertyMapProperty.exactMatchRead === null) { - propertyMapProperty.exactMatchRead = property; - propertyMapProperty.propertiesRead.push(property, ...this.unmatchablePropertiesRead); - } - if (isWrite && !isRead && propertyMapProperty.exactMatchWrite === null) { - propertyMapProperty.exactMatchWrite = property; - propertyMapProperty.propertiesWrite.push(property, ...this.unmatchablePropertiesWrite); } + properties.push({ kind: property.kind, key, property }); } - return propertyMap; + return (this.objectEntity = new ObjectEntity(properties, OBJECT_PROTOTYPE)); } } diff --git a/src/ast/nodes/Property.ts b/src/ast/nodes/Property.ts index 1cfb12233e1..c8de9bd935d 100644 --- a/src/ast/nodes/Property.ts +++ b/src/ast/nodes/Property.ts @@ -1,147 +1,37 @@ import MagicString from 'magic-string'; import { NormalizedTreeshakingOptions } from '../../rollup/types'; import { RenderOptions } from '../../utils/renderHelpers'; -import { CallOptions, NO_ARGS } from '../CallOptions'; -import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; -import { - EMPTY_PATH, - ObjectPath, - PathTracker, - SHARED_RECURSION_TRACKER, - UnknownKey -} from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION } from '../values'; +import { UnknownKey } from '../utils/PathTracker'; import * as NodeType from './NodeType'; -import { ExpressionEntity } from './shared/Expression'; -import { ExpressionNode, NodeBase } from './shared/Node'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './shared/Expression'; +import MethodBase from './shared/MethodBase'; +import { ExpressionNode } from './shared/Node'; import { PatternNode } from './shared/Pattern'; -export default class Property extends NodeBase implements DeoptimizableEntity, PatternNode { - computed!: boolean; +export default class Property extends MethodBase implements PatternNode { key!: ExpressionNode; kind!: 'init' | 'get' | 'set'; method!: boolean; shorthand!: boolean; type!: NodeType.tProperty; - value!: ExpressionNode | (ExpressionNode & PatternNode); - - private accessorCallOptions!: CallOptions; + protected deoptimized = false; private declarationInit: ExpressionEntity | null = null; - private returnExpression: ExpressionEntity | null = null; - - bind() { - super.bind(); - if (this.kind === 'get') { - // ensure the returnExpression is set for the tree-shaking passes - this.getReturnExpression(); - } - if (this.declarationInit !== null) { - this.declarationInit.deoptimizePath([UnknownKey, UnknownKey]); - } - } declare(kind: string, init: ExpressionEntity) { this.declarationInit = init; return (this.value as PatternNode).declare(kind, UNKNOWN_EXPRESSION); } - // As getter properties directly receive their values from function expressions that always - // have a fixed return value, there is no known situation where a getter is deoptimized. - deoptimizeCache(): void {} - - deoptimizePath(path: ObjectPath) { - if (this.kind === 'get') { - this.getReturnExpression().deoptimizePath(path); - } else { - this.value.deoptimizePath(path); - } - } - - getLiteralValueAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ): LiteralValueOrUnknown { - if (this.kind === 'get') { - return this.getReturnExpression().getLiteralValueAtPath(path, recursionTracker, origin); - } - return this.value.getLiteralValueAtPath(path, recursionTracker, origin); - } - - getReturnExpressionWhenCalledAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ): ExpressionEntity { - if (this.kind === 'get') { - return this.getReturnExpression().getReturnExpressionWhenCalledAtPath( - path, - recursionTracker, - origin - ); - } - return this.value.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); - } - hasEffects(context: HasEffectsContext): boolean { - const propertyReadSideEffects = (this.context.options.treeshake as NormalizedTreeshakingOptions).propertyReadSideEffects; - return this.parent.type === 'ObjectPattern' && propertyReadSideEffects === 'always' || + if (!this.deoptimized) this.applyDeoptimizations(); + const propertyReadSideEffects = (this.context.options.treeshake as NormalizedTreeshakingOptions) + .propertyReadSideEffects; + return ( + (this.parent.type === 'ObjectPattern' && propertyReadSideEffects === 'always') || this.key.hasEffects(context) || - this.value.hasEffects(context); - } - - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (this.kind === 'get') { - const trackedExpressions = context.accessed.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return ( - this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context) || - (path.length > 0 && this.returnExpression!.hasEffectsWhenAccessedAtPath(path, context)) - ); - } - return this.value.hasEffectsWhenAccessedAtPath(path, context); - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (this.kind === 'get') { - const trackedExpressions = context.assigned.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return this.returnExpression!.hasEffectsWhenAssignedAtPath(path, context); - } - if (this.kind === 'set') { - const trackedExpressions = context.assigned.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context); - } - return this.value.hasEffectsWhenAssignedAtPath(path, context); - } - - hasEffectsWhenCalledAtPath( - path: ObjectPath, - callOptions: CallOptions, - context: HasEffectsContext - ) { - if (this.kind === 'get') { - const trackedExpressions = (callOptions.withNew - ? context.instantiated - : context.called - ).getEntities(path, callOptions); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return this.returnExpression!.hasEffectsWhenCalledAtPath(path, callOptions, context); - } - return this.value.hasEffectsWhenCalledAtPath(path, callOptions, context); - } - - initialise() { - this.accessorCallOptions = { - args: NO_ARGS, - withNew: false - }; + this.value.hasEffects(context) + ); } render(code: MagicString, options: RenderOptions) { @@ -151,15 +41,10 @@ export default class Property extends NodeBase implements DeoptimizableEntity, P this.value.render(code, options, { isShorthandProperty: this.shorthand }); } - private getReturnExpression(): ExpressionEntity { - if (this.returnExpression === null) { - this.returnExpression = UNKNOWN_EXPRESSION; - return (this.returnExpression = this.value.getReturnExpressionWhenCalledAtPath( - EMPTY_PATH, - SHARED_RECURSION_TRACKER, - this - )); + protected applyDeoptimizations():void { + this.deoptimized = true; + if (this.declarationInit !== null) { + this.declarationInit.deoptimizePath([UnknownKey, UnknownKey]); } - return this.returnExpression; } } diff --git a/src/ast/nodes/PropertyDefinition.ts b/src/ast/nodes/PropertyDefinition.ts index 0b0b02e3643..92dcd0eec02 100644 --- a/src/ast/nodes/PropertyDefinition.ts +++ b/src/ast/nodes/PropertyDefinition.ts @@ -1,6 +1,16 @@ +import { CallOptions } from '../CallOptions'; +import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; +import { NodeEvent } from '../NodeEvents'; +import { ObjectPath, PathTracker } from '../utils/PathTracker'; import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; +import { + ExpressionEntity, + LiteralValueOrUnknown, + UnknownValue, + UNKNOWN_EXPRESSION +} from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; export default class PropertyDefinition extends NodeBase { @@ -10,10 +20,60 @@ export default class PropertyDefinition extends NodeBase { type!: NodeType.tPropertyDefinition; value!: ExpressionNode | null; + deoptimizePath(path: ObjectPath) { + this.value?.deoptimizePath(path); + } + + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.value?.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } + + getLiteralValueAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + return this.value + ? this.value.getLiteralValueAtPath(path, recursionTracker, origin) + : UnknownValue; + } + + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): ExpressionEntity { + return this.value + ? this.value.getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin) + : UNKNOWN_EXPRESSION; + } + hasEffects(context: HasEffectsContext): boolean { return ( this.key.hasEffects(context) || (this.static && this.value !== null && 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( + path: ObjectPath, + callOptions: CallOptions, + context: HasEffectsContext + ) { + return !this.value || this.value.hasEffectsWhenCalledAtPath(path, callOptions, context); + } } diff --git a/src/ast/nodes/RestElement.ts b/src/ast/nodes/RestElement.ts index 1ea98754e47..6ce201cdfa2 100644 --- a/src/ast/nodes/RestElement.ts +++ b/src/ast/nodes/RestElement.ts @@ -1,16 +1,16 @@ import { HasEffectsContext } from '../ExecutionContext'; import { EMPTY_PATH, ObjectPath, UnknownKey } from '../utils/PathTracker'; -import { UNKNOWN_EXPRESSION } from '../values'; +import LocalVariable from '../variables/LocalVariable'; import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; -import { ExpressionEntity } from './shared/Expression'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './shared/Expression'; import { NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; export default class RestElement extends NodeBase implements PatternNode { argument!: PatternNode; type!: NodeType.tRestElement; - + protected deoptimized = false; private declarationInit: ExpressionEntity | null = null; addExportedVariables( @@ -20,23 +20,23 @@ export default class RestElement extends NodeBase implements PatternNode { this.argument.addExportedVariables(variables, exportNamesByVariable); } - bind() { - super.bind(); - if (this.declarationInit !== null) { - this.declarationInit.deoptimizePath([UnknownKey, UnknownKey]); - } - } - - declare(kind: string, init: ExpressionEntity) { + declare(kind: string, init: ExpressionEntity): LocalVariable[] { this.declarationInit = init; return this.argument.declare(kind, UNKNOWN_EXPRESSION); } - deoptimizePath(path: ObjectPath) { + deoptimizePath(path: ObjectPath): void { path.length === 0 && this.argument.deoptimizePath(EMPTY_PATH); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { return path.length > 0 || this.argument.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context); } + + protected applyDeoptimizations(): void { + this.deoptimized = true; + if (this.declarationInit !== null) { + this.declarationInit.deoptimizePath([UnknownKey, UnknownKey]); + } + } } diff --git a/src/ast/nodes/ReturnStatement.ts b/src/ast/nodes/ReturnStatement.ts index 87bec1a78c2..37b42812bd8 100644 --- a/src/ast/nodes/ReturnStatement.ts +++ b/src/ast/nodes/ReturnStatement.ts @@ -5,8 +5,8 @@ import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { UNKNOWN_EXPRESSION } from '../values'; import * as NodeType from './NodeType'; +import { UNKNOWN_EXPRESSION } from './shared/Expression'; import { ExpressionNode, IncludeChildren, StatementBase } from './shared/Node'; export default class ReturnStatement extends StatementBase { diff --git a/src/ast/nodes/SequenceExpression.ts b/src/ast/nodes/SequenceExpression.ts index 13930c336f7..cc3b234ac76 100644 --- a/src/ast/nodes/SequenceExpression.ts +++ b/src/ast/nodes/SequenceExpression.ts @@ -10,10 +10,11 @@ import { treeshakeNode } from '../../utils/treeshakeNode'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { EVENT_CALLED, NodeEvent } from '../NodeEvents'; import { ObjectPath, PathTracker } from '../utils/PathTracker'; -import { LiteralValueOrUnknown } from '../values'; import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; +import { ExpressionEntity, LiteralValueOrUnknown } from './shared/Expression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; export default class SequenceExpression extends NodeBase { @@ -24,6 +25,17 @@ export default class SequenceExpression extends NodeBase { if (path.length > 0) this.expressions[this.expressions.length - 1].deoptimizePath(path); } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + if (event === EVENT_CALLED || path.length > 0) { + this.expressions[this.expressions.length - 1].deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, diff --git a/src/ast/nodes/SpreadElement.ts b/src/ast/nodes/SpreadElement.ts index cc0807079c4..a8246dddbdc 100644 --- a/src/ast/nodes/SpreadElement.ts +++ b/src/ast/nodes/SpreadElement.ts @@ -1,13 +1,22 @@ -import { UnknownKey } from '../utils/PathTracker'; +import { NodeEvent } from '../NodeEvents'; +import { ObjectPath, PathTracker, UnknownKey } from '../utils/PathTracker'; import * as NodeType from './NodeType'; +import { ExpressionEntity } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; export default class SpreadElement extends NodeBase { argument!: ExpressionNode; type!: NodeType.tSpreadElement; + protected deoptimized = false; - bind() { - super.bind(); + deoptimizeThisOnEventAtPath(event: NodeEvent, path: ObjectPath, thisParameter: ExpressionEntity, recursionTracker: PathTracker) { + if (path.length > 0) { + this.argument.deoptimizeThisOnEventAtPath(event, [UnknownKey, ...path], thisParameter, recursionTracker) + } + } + + protected applyDeoptimizations(): void { + this.deoptimized = true; // Only properties of properties of the argument could become subject to reassignment // This will also reassign the return values of iterators this.argument.deoptimizePath([UnknownKey, UnknownKey]); diff --git a/src/ast/nodes/Super.ts b/src/ast/nodes/Super.ts index d09554da7bc..8a685e6b500 100644 --- a/src/ast/nodes/Super.ts +++ b/src/ast/nodes/Super.ts @@ -1,6 +1,25 @@ +import { ObjectPath } from '../utils/PathTracker'; +import ThisVariable from '../variables/ThisVariable'; import * as NodeType from './NodeType'; import { NodeBase } from './shared/Node'; export default class Super extends NodeBase { type!: NodeType.tSuper; + + variable!: ThisVariable; + + bind() { + this.variable = this.scope.findVariable('this') as ThisVariable; + } + + deoptimizePath(path: ObjectPath) { + this.variable.deoptimizePath(path); + } + + include() { + if (!this.included) { + this.included = true; + this.context.includeVariableInModule(this.variable); + } + } } diff --git a/src/ast/nodes/TaggedTemplateExpression.ts b/src/ast/nodes/TaggedTemplateExpression.ts index 84e9dffc3fe..90a18c81ee2 100644 --- a/src/ast/nodes/TaggedTemplateExpression.ts +++ b/src/ast/nodes/TaggedTemplateExpression.ts @@ -52,6 +52,7 @@ export default class TaggedTemplateExpression extends NodeBase { initialise() { this.callOptions = { args: NO_ARGS, + thisParam: null, withNew: false, }; } diff --git a/src/ast/nodes/TemplateElement.ts b/src/ast/nodes/TemplateElement.ts index e5cc466fc3a..2dbe9317fe3 100644 --- a/src/ast/nodes/TemplateElement.ts +++ b/src/ast/nodes/TemplateElement.ts @@ -9,6 +9,7 @@ export default class TemplateElement extends NodeBase { raw: string; }; + // Do not try to bind value bind() {} hasEffects() { diff --git a/src/ast/nodes/TemplateLiteral.ts b/src/ast/nodes/TemplateLiteral.ts index fbc02c708ee..06314a3bc00 100644 --- a/src/ast/nodes/TemplateLiteral.ts +++ b/src/ast/nodes/TemplateLiteral.ts @@ -1,8 +1,8 @@ import MagicString from 'magic-string'; import { RenderOptions } from '../../utils/renderHelpers'; import { ObjectPath } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; import * as NodeType from './NodeType'; +import { LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; import TemplateElement from './TemplateElement'; diff --git a/src/ast/nodes/ThisExpression.ts b/src/ast/nodes/ThisExpression.ts index f68631adae6..b1f2ada38aa 100644 --- a/src/ast/nodes/ThisExpression.ts +++ b/src/ast/nodes/ThisExpression.ts @@ -1,21 +1,40 @@ import MagicString from 'magic-string'; import { HasEffectsContext } from '../ExecutionContext'; +import { NodeEvent } from '../NodeEvents'; import ModuleScope from '../scopes/ModuleScope'; -import { ObjectPath } from '../utils/PathTracker'; -import ThisVariable from '../variables/ThisVariable'; +import { ObjectPath, PathTracker } from '../utils/PathTracker'; +import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; -import FunctionNode from './shared/FunctionNode'; +import { ExpressionEntity } from './shared/Expression'; import { NodeBase } from './shared/Node'; export default class ThisExpression extends NodeBase { type!: NodeType.tThisExpression; - variable!: ThisVariable; + variable!: Variable; private alias!: string | null; bind() { - super.bind(); - this.variable = this.scope.findVariable('this') as ThisVariable; + this.variable = this.scope.findVariable('this'); + } + + deoptimizePath(path: ObjectPath) { + this.variable.deoptimizePath(path); + } + + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.variable.deoptimizeThisOnEventAtPath( + event, + 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 { @@ -26,6 +45,13 @@ export default class ThisExpression extends NodeBase { return this.variable.hasEffectsWhenAssignedAtPath(path, context); } + include() { + if (!this.included) { + this.included = true; + this.context.includeVariableInModule(this.variable); + } + } + initialise() { this.alias = this.scope.findLexicalBoundary() instanceof ModuleScope ? this.context.moduleContext : null; @@ -39,12 +65,6 @@ export default class ThisExpression extends NodeBase { this.start ); } - for (let parent = this.parent; parent instanceof NodeBase; parent = parent.parent) { - if (parent instanceof FunctionNode) { - parent.referencesThis = true; - break; - } - } } render(code: MagicString) { diff --git a/src/ast/nodes/UnaryExpression.ts b/src/ast/nodes/UnaryExpression.ts index b80b70474ea..11087ca408a 100644 --- a/src/ast/nodes/UnaryExpression.ts +++ b/src/ast/nodes/UnaryExpression.ts @@ -1,10 +1,10 @@ import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; import { EMPTY_PATH, ObjectPath, PathTracker } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; import Identifier from './Identifier'; import { LiteralValue } from './Literal'; import * as NodeType from './NodeType'; +import { LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; const unaryOperators: { @@ -24,13 +24,7 @@ export default class UnaryExpression extends NodeBase { operator!: '!' | '+' | '-' | 'delete' | 'typeof' | 'void' | '~'; prefix!: boolean; type!: NodeType.tUnaryExpression; - - bind() { - super.bind(); - if (this.operator === 'delete') { - this.argument.deoptimizePath(EMPTY_PATH); - } - } + protected deoptimized = false; getLiteralValueAtPath( path: ObjectPath, @@ -45,6 +39,7 @@ export default class UnaryExpression extends NodeBase { } hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); if (this.operator === 'typeof' && this.argument instanceof Identifier) return false; return ( this.argument.hasEffects(context) || @@ -59,4 +54,11 @@ export default class UnaryExpression extends NodeBase { } return path.length > 1; } + + protected applyDeoptimizations(): void { + this.deoptimized = true; + if (this.operator === 'delete') { + this.argument.deoptimizePath(EMPTY_PATH); + } + } } diff --git a/src/ast/nodes/UpdateExpression.ts b/src/ast/nodes/UpdateExpression.ts index 2d2a60f621d..b83864238c9 100644 --- a/src/ast/nodes/UpdateExpression.ts +++ b/src/ast/nodes/UpdateExpression.ts @@ -1,6 +1,9 @@ import MagicString from 'magic-string'; import { RenderOptions } from '../../utils/renderHelpers'; -import { getSystemExportFunctionLeft, getSystemExportStatement } from '../../utils/systemJsRendering'; +import { + getSystemExportFunctionLeft, + getSystemExportStatement +} from '../../utils/systemJsRendering'; import { HasEffectsContext } from '../ExecutionContext'; import { EMPTY_PATH, ObjectPath } from '../utils/PathTracker'; import Identifier from './Identifier'; @@ -12,17 +15,10 @@ export default class UpdateExpression extends NodeBase { operator!: '++' | '--'; prefix!: boolean; type!: NodeType.tUpdateExpression; - - bind() { - super.bind(); - this.argument.deoptimizePath(EMPTY_PATH); - if (this.argument instanceof Identifier) { - const variable = this.scope.findVariable(this.argument.name); - variable.isReassigned = true; - } - } + protected deoptimized = false; hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); return ( this.argument.hasEffects(context) || this.argument.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context) @@ -83,4 +79,13 @@ export default class UpdateExpression extends NodeBase { } } } + + protected applyDeoptimizations() { + this.deoptimized = true; + this.argument.deoptimizePath(EMPTY_PATH); + if (this.argument instanceof Identifier) { + const variable = this.scope.findVariable(this.argument.name); + variable.isReassigned = true; + } + } } diff --git a/src/ast/nodes/YieldExpression.ts b/src/ast/nodes/YieldExpression.ts index d322951c8fd..e7142d777f3 100644 --- a/src/ast/nodes/YieldExpression.ts +++ b/src/ast/nodes/YieldExpression.ts @@ -9,15 +9,10 @@ export default class YieldExpression extends NodeBase { argument!: ExpressionNode | null; delegate!: boolean; type!: NodeType.tYieldExpression; - - bind() { - super.bind(); - if (this.argument !== null) { - this.argument.deoptimizePath(UNKNOWN_PATH); - } - } + protected deoptimized = false; hasEffects(context: HasEffectsContext) { + if (!this.deoptimized) this.applyDeoptimizations(); return ( !context.ignore.returnAwaitYield || (this.argument !== null && this.argument.hasEffects(context)) @@ -32,4 +27,9 @@ export default class YieldExpression extends NodeBase { } } } + + protected applyDeoptimizations() { + this.deoptimized = true; + this.argument?.deoptimizePath(UNKNOWN_PATH); + } } diff --git a/src/ast/nodes/shared/ArrayPrototype.ts b/src/ast/nodes/shared/ArrayPrototype.ts new file mode 100644 index 00000000000..d01ca084612 --- /dev/null +++ b/src/ast/nodes/shared/ArrayPrototype.ts @@ -0,0 +1,144 @@ +import { UnknownInteger } from '../../utils/PathTracker'; +import { UNKNOWN_LITERAL_BOOLEAN, UNKNOWN_LITERAL_NUMBER } from '../../values'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './Expression'; +import { + Method, + METHOD_RETURNS_BOOLEAN, + METHOD_RETURNS_NUMBER, + METHOD_RETURNS_STRING, + METHOD_RETURNS_UNKNOWN +} from './MethodTypes'; +import { ObjectEntity, ObjectProperty } from './ObjectEntity'; +import { OBJECT_PROTOTYPE } from './ObjectPrototype'; + +const NEW_ARRAY_PROPERTIES: ObjectProperty[] = [ + { kind: 'init', key: UnknownInteger, property: UNKNOWN_EXPRESSION }, + { kind: 'init', key: 'length', property: UNKNOWN_LITERAL_NUMBER } +]; + +const METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_BOOLEAN: [ExpressionEntity] = [ + new Method({ + callsArgs: [0], + mutatesSelfAsArray: true, + returns: null, + returnsPrimitive: UNKNOWN_LITERAL_BOOLEAN + }) +]; + +const METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_NUMBER: [ExpressionEntity] = [ + new Method({ + callsArgs: [0], + mutatesSelfAsArray: true, + returns: null, + returnsPrimitive: UNKNOWN_LITERAL_NUMBER + }) +]; + +export const METHOD_RETURNS_NEW_ARRAY: [ExpressionEntity] = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: false, + returns: () => new ObjectEntity(NEW_ARRAY_PROPERTIES, ARRAY_PROTOTYPE), + returnsPrimitive: null + }) +]; + +const METHOD_MUTATES_SELF_RETURNS_NEW_ARRAY: [ExpressionEntity] = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: true, + returns: () => new ObjectEntity(NEW_ARRAY_PROPERTIES, ARRAY_PROTOTYPE), + returnsPrimitive: null + }) +]; + +const METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_NEW_ARRAY: [ExpressionEntity] = [ + new Method({ + callsArgs: [0], + mutatesSelfAsArray: true, + returns: () => new ObjectEntity(NEW_ARRAY_PROPERTIES, ARRAY_PROTOTYPE), + returnsPrimitive: null + }) +]; + +const METHOD_MUTATES_SELF_RETURNS_NUMBER: [ExpressionEntity] = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: true, + returns: null, + returnsPrimitive: UNKNOWN_LITERAL_NUMBER + }) +]; + +const METHOD_MUTATES_SELF_RETURNS_UNKNOWN: [ExpressionEntity] = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: true, + returns: null, + returnsPrimitive: UNKNOWN_EXPRESSION + }) +]; + +const METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_UNKNOWN: [ExpressionEntity] = [ + new Method({ + callsArgs: [0], + mutatesSelfAsArray: true, + returns: null, + returnsPrimitive: UNKNOWN_EXPRESSION + }) +]; + +const METHOD_MUTATES_SELF_RETURNS_SELF: [ExpressionEntity] = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: true, + returns: 'self', + returnsPrimitive: null + }) +]; + +const METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_SELF: [ExpressionEntity] = [ + new Method({ + callsArgs: [0], + mutatesSelfAsArray: true, + returns: 'self', + returnsPrimitive: null + }) +]; + +export const ARRAY_PROTOTYPE = new ObjectEntity( + { + // @ts-ignore + __proto__: null, + // We assume that accessors have effects as we do not track the accessed value afterwards + at: METHOD_MUTATES_SELF_RETURNS_UNKNOWN, + concat: METHOD_RETURNS_NEW_ARRAY, + copyWithin: METHOD_MUTATES_SELF_RETURNS_SELF, + entries: METHOD_MUTATES_SELF_RETURNS_NEW_ARRAY, + every: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_BOOLEAN, + fill: METHOD_MUTATES_SELF_RETURNS_SELF, + filter: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_NEW_ARRAY, + find: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_UNKNOWN, + findIndex: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_NUMBER, + forEach: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_UNKNOWN, + includes: METHOD_RETURNS_BOOLEAN, + indexOf: METHOD_RETURNS_NUMBER, + join: METHOD_RETURNS_STRING, + keys: METHOD_RETURNS_UNKNOWN, + lastIndexOf: METHOD_RETURNS_NUMBER, + map: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_NEW_ARRAY, + pop: METHOD_MUTATES_SELF_RETURNS_UNKNOWN, + push: METHOD_MUTATES_SELF_RETURNS_NUMBER, + reduce: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_UNKNOWN, + reduceRight: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_UNKNOWN, + reverse: METHOD_MUTATES_SELF_RETURNS_SELF, + shift: METHOD_MUTATES_SELF_RETURNS_UNKNOWN, + slice: METHOD_MUTATES_SELF_RETURNS_NEW_ARRAY, + some: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_BOOLEAN, + sort: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_SELF, + splice: METHOD_MUTATES_SELF_RETURNS_NEW_ARRAY, + unshift: METHOD_MUTATES_SELF_RETURNS_NUMBER, + values: METHOD_MUTATES_SELF_RETURNS_UNKNOWN + }, + OBJECT_PROTOTYPE +); diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index 4323d8c789f..7a930b94582 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -1,29 +1,82 @@ import { CallOptions } from '../../CallOptions'; +import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; +import { NodeEvent } from '../../NodeEvents'; import ChildScope from '../../scopes/ChildScope'; import Scope from '../../scopes/Scope'; -import { ObjectPath } from '../../utils/PathTracker'; +import { + EMPTY_PATH, + ObjectPath, + PathTracker, + SHARED_RECURSION_TRACKER, + UnknownKey +} from '../../utils/PathTracker'; import ClassBody from '../ClassBody'; import Identifier from '../Identifier'; +import Literal from '../Literal'; +import MethodDefinition from '../MethodDefinition'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue } from './Expression'; import { ExpressionNode, NodeBase } from './Node'; +import { ObjectEntity, ObjectProperty } from './ObjectEntity'; +import { ObjectMember } from './ObjectMember'; +import { OBJECT_PROTOTYPE } from './ObjectPrototype'; -export default class ClassNode extends NodeBase { +export default class ClassNode extends NodeBase implements DeoptimizableEntity { body!: ClassBody; id!: Identifier | null; superClass!: ExpressionNode | null; + private classConstructor!: MethodDefinition | null; + private objectEntity: ObjectEntity | null = null; createScope(parentScope: Scope) { this.scope = new ChildScope(parentScope); } - hasEffectsWhenAccessedAtPath(path: ObjectPath) { - if (path.length <= 1) return false; - return path.length > 2 || path[0] !== 'prototype'; + deoptimizeCache() { + this.getObjectEntity().deoptimizeAllProperties(); } - hasEffectsWhenAssignedAtPath(path: ObjectPath) { - if (path.length <= 1) return false; - return path.length > 2 || path[0] !== 'prototype'; + deoptimizePath(path: ObjectPath) { + this.getObjectEntity().deoptimizePath(path); + } + + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.getObjectEntity().deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + } + + getLiteralValueAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + return this.getObjectEntity().getLiteralValueAtPath(path, recursionTracker, origin); + } + + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): ExpressionEntity { + return this.getObjectEntity().getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin); + } + + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { + return this.getObjectEntity().hasEffectsWhenAccessedAtPath(path, context); + } + + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext) { + return this.getObjectEntity().hasEffectsWhenAssignedAtPath(path, context); } hasEffectsWhenCalledAtPath( @@ -31,17 +84,76 @@ export default class ClassNode extends NodeBase { callOptions: CallOptions, context: HasEffectsContext ) { - if (!callOptions.withNew) return true; - return ( - this.body.hasEffectsWhenCalledAtPath(path, callOptions, context) || - (this.superClass !== null && - this.superClass.hasEffectsWhenCalledAtPath(path, callOptions, context)) - ); + if (path.length === 0) { + return ( + !callOptions.withNew || + (this.classConstructor !== null + ? this.classConstructor.hasEffectsWhenCalledAtPath(EMPTY_PATH, callOptions, context) + : this.superClass !== null && + this.superClass.hasEffectsWhenCalledAtPath(path, callOptions, context)) + ); + } else { + return this.getObjectEntity().hasEffectsWhenCalledAtPath(path, callOptions, context); + } } initialise() { if (this.id !== null) { this.id.declare('class', this); } + for (const method of this.body.body) { + if (method instanceof MethodDefinition && method.kind === 'constructor') { + this.classConstructor = method; + return; + } + } + this.classConstructor = null; + } + + private getObjectEntity(): ObjectEntity { + if (this.objectEntity !== null) { + return this.objectEntity; + } + const staticProperties: ObjectProperty[] = []; + const dynamicMethods: ObjectProperty[] = []; + for (const definition of this.body.body) { + const properties = definition.static ? staticProperties : dynamicMethods; + const definitionKind = (definition as MethodDefinition | { kind: undefined }).kind; + // Note that class fields do not end up on the prototype + if (properties === dynamicMethods && !definitionKind) continue; + const kind = definitionKind === 'set' || definitionKind === 'get' ? definitionKind : 'init'; + let key: string; + if (definition.computed) { + const keyValue = definition.key.getLiteralValueAtPath( + EMPTY_PATH, + SHARED_RECURSION_TRACKER, + this + ); + if (keyValue === UnknownValue) { + properties.push({ kind, key: UnknownKey, property: definition }); + continue; + } else { + key = String(keyValue); + } + } else { + key = + definition.key instanceof Identifier + ? definition.key.name + : String((definition.key as Literal).value); + } + properties.push({ kind, key, property: definition }); + } + staticProperties.unshift({ + key: 'prototype', + kind: 'init', + property: new ObjectEntity( + dynamicMethods, + this.superClass ? new ObjectMember(this.superClass, 'prototype') : OBJECT_PROTOTYPE + ) + }); + return (this.objectEntity = new ObjectEntity( + staticProperties, + this.superClass || OBJECT_PROTOTYPE + )); } } diff --git a/src/ast/nodes/shared/Expression.ts b/src/ast/nodes/shared/Expression.ts index eba5367420b..4a851c2f523 100644 --- a/src/ast/nodes/shared/Expression.ts +++ b/src/ast/nodes/shared/Expression.ts @@ -2,13 +2,29 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { WritableEntity } from '../../Entity'; import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; -import { ObjectPath, PathTracker } from '../../utils/PathTracker'; -import { LiteralValueOrUnknown } from '../../values'; +import { NodeEvent } from '../../NodeEvents'; +import { ObjectPath, PathTracker, UNKNOWN_PATH } from '../../utils/PathTracker'; +import { LiteralValue } from '../Literal'; import SpreadElement from '../SpreadElement'; import { ExpressionNode, IncludeChildren } from './Node'; -export interface ExpressionEntity extends WritableEntity { - included: boolean; +export const UnknownValue = Symbol('Unknown Value'); + +export type LiteralValueOrUnknown = LiteralValue | typeof UnknownValue; + +export class ExpressionEntity implements WritableEntity { + included = false; + + deoptimizePath(_path: ObjectPath): void {} + + deoptimizeThisOnEventAtPath( + _event: NodeEvent, + _path: ObjectPath, + thisParameter: ExpressionEntity, + _recursionTracker: PathTracker + ): void { + thisParameter.deoptimizePath(UNKNOWN_PATH); + } /** * If possible it returns a stringifyable literal value for this node that can be used @@ -16,22 +32,47 @@ export interface ExpressionEntity extends WritableEntity { * Otherwise it should return UnknownValue. */ getLiteralValueAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ): LiteralValueOrUnknown; + _path: ObjectPath, + _recursionTracker: PathTracker, + _origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + return UnknownValue; + } + getReturnExpressionWhenCalledAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ): ExpressionEntity; - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean; + _path: ObjectPath, + _callOptions: CallOptions, + _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( - path: ObjectPath, - callOptions: CallOptions, - context: HasEffectsContext - ): boolean; - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void; - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void; - mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker): boolean; + _path: ObjectPath, + _callOptions: CallOptions, + _context: HasEffectsContext + ): boolean { + return true; + } + + include(_context: InclusionContext, _includeChildrenRecursively: IncludeChildren): void { + this.included = true; + } + + includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { + for (const arg of args) { + arg.include(context, false); + } + } } + +export const UNKNOWN_EXPRESSION: ExpressionEntity = new (class UnknownExpression extends ExpressionEntity {})(); diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index 65e81bed072..3f7cf1f66a5 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -1,13 +1,16 @@ import { CallOptions } from '../../CallOptions'; import { BROKEN_FLOW_NONE, HasEffectsContext, InclusionContext } from '../../ExecutionContext'; +import { EVENT_CALLED, NodeEvent } from '../../NodeEvents'; import FunctionScope from '../../scopes/FunctionScope'; import { ObjectPath, UnknownKey, UNKNOWN_PATH } from '../../utils/PathTracker'; -import { UnknownObjectExpression, UNKNOWN_EXPRESSION } from '../../values'; import BlockStatement from '../BlockStatement'; import Identifier, { IdentifierWithVariable } from '../Identifier'; import RestElement from '../RestElement'; import SpreadElement from '../SpreadElement'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './Expression'; import { ExpressionNode, GenericEsTreeNode, IncludeChildren, NodeBase } from './Node'; +import { ObjectEntity } from './ObjectEntity'; +import { OBJECT_PROTOTYPE } from './ObjectPrototype'; import { PatternNode } from './Pattern'; export default class FunctionNode extends NodeBase { @@ -16,9 +19,7 @@ export default class FunctionNode extends NodeBase { id!: IdentifierWithVariable | null; params!: PatternNode[]; preventChildBlockScope!: true; - referencesThis!: boolean; scope!: FunctionScope; - private isPrototypeDeoptimized = false; createScope(parentScope: FunctionScope) { @@ -39,6 +40,17 @@ export default class FunctionNode extends NodeBase { } } + // TODO for completeness, we should also track other events here + deoptimizeThisOnEventAtPath(event: NodeEvent, path: ObjectPath, thisParameter: ExpressionEntity) { + if (event === EVENT_CALLED) { + if (path.length > 0 ) { + thisParameter.deoptimizePath(UNKNOWN_PATH); + } else { + this.scope.thisVariable.addEntityToBeDeoptimized(thisParameter); + } + } + } + getReturnExpressionWhenCalledAtPath(path: ObjectPath) { return path.length === 0 ? this.scope.getReturnExpression() : UNKNOWN_EXPRESSION; } @@ -71,7 +83,7 @@ export default class FunctionNode extends NodeBase { const thisInit = context.replacedVariableInits.get(this.scope.thisVariable); context.replacedVariableInits.set( this.scope.thisVariable, - callOptions.withNew ? new UnknownObjectExpression() : UNKNOWN_EXPRESSION + callOptions.withNew ? new ObjectEntity({}, OBJECT_PROTOTYPE) : UNKNOWN_EXPRESSION ); const { brokenFlow, ignore } = context; context.ignore = { @@ -121,14 +133,7 @@ export default class FunctionNode extends NodeBase { this.body.addImplicitReturnExpressionToScope(); } - mayModifyThisWhenCalledAtPath( - path: ObjectPath - ) { - return path.length ? true : this.referencesThis - } - parseNode(esTreeNode: GenericEsTreeNode) { - this.referencesThis = false; this.body = new this.context.nodeConstructors.BlockStatement( esTreeNode.body, this, diff --git a/src/ast/nodes/shared/MethodBase.ts b/src/ast/nodes/shared/MethodBase.ts new file mode 100644 index 00000000000..1a425f7cce5 --- /dev/null +++ b/src/ast/nodes/shared/MethodBase.ts @@ -0,0 +1,131 @@ +import { CallOptions, NO_ARGS } from '../../CallOptions'; +import { DeoptimizableEntity } from '../../DeoptimizableEntity'; +import { HasEffectsContext } from '../../ExecutionContext'; +import { EVENT_ACCESSED, EVENT_ASSIGNED, EVENT_CALLED, NodeEvent } from '../../NodeEvents'; +import { + EMPTY_PATH, + ObjectPath, + PathTracker, + SHARED_RECURSION_TRACKER +} from '../../utils/PathTracker'; +import PrivateIdentifier from '../PrivateIdentifier'; +import { ExpressionEntity, LiteralValueOrUnknown, UNKNOWN_EXPRESSION } from './Expression'; +import { ExpressionNode, NodeBase } from './Node'; +import { PatternNode } from './Pattern'; + +export default class MethodBase extends NodeBase implements DeoptimizableEntity { + computed!: boolean; + key!: ExpressionNode | PrivateIdentifier; + kind!: 'constructor' | 'method' | 'init' | 'get' | 'set'; + 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. + deoptimizeCache(): void {} + + deoptimizePath(path: ObjectPath) { + this.getAccessedValue().deoptimizePath(path); + } + + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + if (event === EVENT_ACCESSED && this.kind === 'get' && path.length === 0) { + return this.value.deoptimizeThisOnEventAtPath( + EVENT_CALLED, + EMPTY_PATH, + thisParameter, + recursionTracker + ); + } + if (event === EVENT_ASSIGNED && this.kind === 'set' && path.length === 0) { + return this.value.deoptimizeThisOnEventAtPath( + EVENT_CALLED, + EMPTY_PATH, + thisParameter, + recursionTracker + ); + } + this.getAccessedValue().deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + } + + getLiteralValueAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + return this.getAccessedValue().getLiteralValueAtPath(path, recursionTracker, origin); + } + + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): ExpressionEntity { + return this.getAccessedValue().getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); + } + + hasEffects(context: HasEffectsContext) { + 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( + path: ObjectPath, + callOptions: CallOptions, + context: HasEffectsContext + ) { + return this.getAccessedValue().hasEffectsWhenCalledAtPath(path, callOptions, context); + } + + protected getAccessedValue(): ExpressionEntity { + if (this.accessedValue === null) { + if (this.kind === 'get') { + this.accessedValue = UNKNOWN_EXPRESSION; + return (this.accessedValue = this.value.getReturnExpressionWhenCalledAtPath( + EMPTY_PATH, + this.accessorCallOptions, + SHARED_RECURSION_TRACKER, + this + )); + } else { + return (this.accessedValue = this.value); + } + } + return this.accessedValue; + } +} diff --git a/src/ast/nodes/shared/MethodTypes.ts b/src/ast/nodes/shared/MethodTypes.ts new file mode 100644 index 00000000000..d43f4324ceb --- /dev/null +++ b/src/ast/nodes/shared/MethodTypes.ts @@ -0,0 +1,132 @@ +import { CallOptions, NO_ARGS } from '../../CallOptions'; +import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; +import { EVENT_CALLED, NodeEvent } from '../../NodeEvents'; +import { EMPTY_PATH, ObjectPath, UNKNOWN_INTEGER_PATH } from '../../utils/PathTracker'; +import { UNKNOWN_LITERAL_BOOLEAN, UNKNOWN_LITERAL_NUMBER, UNKNOWN_LITERAL_STRING } from '../../values'; +import SpreadElement from '../SpreadElement'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './Expression'; +import { ExpressionNode } from './Node'; + +type MethodDescription = { + callsArgs: number[] | null; + mutatesSelfAsArray: boolean; +} & ( + | { + returns: 'self' | (() => ExpressionEntity); + returnsPrimitive: null; + } + | { + returns: null; + returnsPrimitive: ExpressionEntity; + } +); + +export class Method extends ExpressionEntity { + constructor(private readonly description: MethodDescription) { + super(); + } + + deoptimizeThisOnEventAtPath(event: NodeEvent, path: ObjectPath, thisParameter: ExpressionEntity) { + if (event === EVENT_CALLED && path.length === 0 && this.description.mutatesSelfAsArray) { + thisParameter.deoptimizePath(UNKNOWN_INTEGER_PATH); + } + } + + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions + ): ExpressionEntity { + if (path.length > 0) { + return UNKNOWN_EXPRESSION; + } + return ( + this.description.returnsPrimitive || + (this.description.returns === 'self' + ? callOptions.thisParam || UNKNOWN_EXPRESSION + : this.description.returns()) + ); + } + + hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean { + return path.length > 1; + } + + hasEffectsWhenAssignedAtPath(path: ObjectPath): boolean { + return path.length > 0; + } + + hasEffectsWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + context: HasEffectsContext + ): boolean { + if ( + path.length > 0 || + (this.description.mutatesSelfAsArray && + callOptions.thisParam?.hasEffectsWhenAssignedAtPath(UNKNOWN_INTEGER_PATH, context)) + ) { + return true; + } + if (!this.description.callsArgs) { + return false; + } + for (const argIndex of this.description.callsArgs) { + if ( + callOptions.args[argIndex]?.hasEffectsWhenCalledAtPath( + EMPTY_PATH, + { + args: NO_ARGS, + thisParam: null, + withNew: false + }, + context + ) + ) { + return true; + } + } + return false; + } + + includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { + for (const arg of args) { + arg.include(context, false); + } + } +} + +export const METHOD_RETURNS_BOOLEAN = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: false, + returns: null, + returnsPrimitive: UNKNOWN_LITERAL_BOOLEAN + }) +]; + +export const METHOD_RETURNS_STRING = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: false, + returns: null, + returnsPrimitive: UNKNOWN_LITERAL_STRING + }) +]; + +export const METHOD_RETURNS_NUMBER = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: false, + returns: null, + returnsPrimitive: UNKNOWN_LITERAL_NUMBER + }) +]; + +export const METHOD_RETURNS_UNKNOWN = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: false, + returns: null, + returnsPrimitive: UNKNOWN_EXPRESSION + }) +]; diff --git a/src/ast/nodes/shared/MultiExpression.ts b/src/ast/nodes/shared/MultiExpression.ts index f657c7e3454..b7d7426b5db 100644 --- a/src/ast/nodes/shared/MultiExpression.ts +++ b/src/ast/nodes/shared/MultiExpression.ts @@ -2,17 +2,14 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; import { ObjectPath, PathTracker } from '../../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../../values'; import { ExpressionEntity } from './Expression'; import { IncludeChildren } from './Node'; -export class MultiExpression implements ExpressionEntity { +export class MultiExpression extends ExpressionEntity { included = false; - private expressions: ExpressionEntity[]; - - constructor(expressions: ExpressionEntity[]) { - this.expressions = expressions; + constructor(private expressions: ExpressionEntity[]) { + super(); } deoptimizePath(path: ObjectPath): void { @@ -21,18 +18,15 @@ export class MultiExpression implements ExpressionEntity { } } - getLiteralValueAtPath(): LiteralValueOrUnknown { - return UnknownValue; - } - getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { return new MultiExpression( this.expressions.map(expression => - expression.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin) + expression.getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin) ) ); } @@ -71,10 +65,4 @@ export class MultiExpression implements ExpressionEntity { } } } - - includeCallArguments(): void {} - - mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker) { - return this.expressions.some(e => e.mayModifyThisWhenCalledAtPath(path, recursionTracker)); - } } diff --git a/src/ast/nodes/shared/Node.ts b/src/ast/nodes/shared/Node.ts index 26fa04daf55..ccb0e0aa873 100644 --- a/src/ast/nodes/shared/Node.ts +++ b/src/ast/nodes/shared/Node.ts @@ -3,8 +3,6 @@ import { locate } from 'locate-character'; import MagicString from 'magic-string'; import { AstContext } from '../../../Module'; import { NodeRenderOptions, RenderOptions } from '../../../utils/renderHelpers'; -import { CallOptions } from '../../CallOptions'; -import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { Entity } from '../../Entity'; import { createHasEffectsContext, @@ -13,11 +11,8 @@ import { } from '../../ExecutionContext'; import { getAndCreateKeys, keys } from '../../keys'; import ChildScope from '../../scopes/ChildScope'; -import { ObjectPath, PathTracker } from '../../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../../values'; import Variable from '../../variables/Variable'; import * as NodeType from '../NodeType'; -import SpreadElement from '../SpreadElement'; import { ExpressionEntity } from './Expression'; export interface GenericEsTreeNode extends acorn.Node { @@ -91,23 +86,28 @@ export interface StatementNode extends Node {} export interface ExpressionNode extends ExpressionEntity, Node {} -export class NodeBase implements ExpressionNode { +export class NodeBase extends ExpressionEntity implements ExpressionNode { annotations?: Annotation[]; context: AstContext; end!: number; esTreeNode: acorn.Node; - included = false; keys: string[]; parent: Node | { context: AstContext; type: string }; scope!: ChildScope; start!: number; 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 + protected deoptimized?: boolean; constructor( esTreeNode: GenericEsTreeNode, parent: Node | { context: AstContext; type: string }, parentScope: ChildScope ) { + super(); this.esTreeNode = esTreeNode; this.keys = keys[esTreeNode.type] || getAndCreateKeys(esTreeNode); this.parent = parent; @@ -149,25 +149,8 @@ export class NodeBase implements ExpressionNode { this.scope = parentScope; } - deoptimizePath(_path: ObjectPath) {} - - getLiteralValueAtPath( - _path: ObjectPath, - _recursionTracker: PathTracker, - _origin: DeoptimizableEntity - ): LiteralValueOrUnknown { - return UnknownValue; - } - - getReturnExpressionWhenCalledAtPath( - _path: ObjectPath, - _recursionTracker: PathTracker, - _origin: DeoptimizableEntity - ): ExpressionEntity { - return UNKNOWN_EXPRESSION; - } - hasEffects(context: HasEffectsContext): boolean { + if (this.deoptimized === false) this.applyDeoptimizations(); for (const key of this.keys) { const value = (this as GenericEsTreeNode)[key]; if (value === null) continue; @@ -180,23 +163,8 @@ export class NodeBase implements ExpressionNode { return false; } - hasEffectsWhenAccessedAtPath(path: ObjectPath, _context: HasEffectsContext) { - return path.length > 0; - } - - hasEffectsWhenAssignedAtPath(_path: ObjectPath, _context: HasEffectsContext) { - return true; - } - - hasEffectsWhenCalledAtPath( - _path: ObjectPath, - _callOptions: CallOptions, - _context: HasEffectsContext - ) { - return true; - } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (this.deoptimized === false) this.applyDeoptimizations(); this.included = true; for (const key of this.keys) { const value = (this as GenericEsTreeNode)[key]; @@ -215,12 +183,6 @@ export class NodeBase implements ExpressionNode { this.include(context, includeChildrenRecursively); } - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - for (const arg of args) { - arg.include(context, false); - } - } - /** * Override to perform special initialisation steps after the scope is initialised */ @@ -232,15 +194,10 @@ export class NodeBase implements ExpressionNode { } } - mayModifyThisWhenCalledAtPath(_path: ObjectPath, _recursionTracker: PathTracker) { - return true; - } - parseNode(esTreeNode: GenericEsTreeNode) { - for (const key of Object.keys(esTreeNode)) { + for (const [key, value] of Object.entries(esTreeNode)) { // That way, we can override this function to add custom initialisation and then call super.parseNode if (this.hasOwnProperty(key)) continue; - const value = esTreeNode[key]; if (key === '_rollupAnnotations') { this.annotations = value; } else if (typeof value !== 'object' || value === null) { @@ -279,6 +236,8 @@ export class NodeBase implements ExpressionNode { shouldBeIncluded(context: InclusionContext): boolean { return this.included || (!context.brokenFlow && this.hasEffects(createHasEffectsContext())); } + + protected applyDeoptimizations(): void {} } export { NodeBase as StatementBase }; diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts new file mode 100644 index 00000000000..b754c36413f --- /dev/null +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -0,0 +1,477 @@ +import { CallOptions } from '../../CallOptions'; +import { DeoptimizableEntity } from '../../DeoptimizableEntity'; +import { HasEffectsContext } from '../../ExecutionContext'; +import { EVENT_ACCESSED, EVENT_CALLED, NodeEvent } from '../../NodeEvents'; +import { + ObjectPath, + ObjectPathKey, + PathTracker, + UnknownInteger, + UnknownKey, + UNKNOWN_INTEGER_PATH, + UNKNOWN_PATH +} from '../../utils/PathTracker'; +import { + ExpressionEntity, + LiteralValueOrUnknown, + UnknownValue, + UNKNOWN_EXPRESSION +} from './Expression'; + +export interface ObjectProperty { + key: ObjectPathKey; + kind: 'init' | 'set' | 'get'; + property: ExpressionEntity; +} + +type PropertyMap = Record; +const INTEGER_REG_EXP = /^\d+$/; + +export class ObjectEntity extends ExpressionEntity { + private readonly allProperties: ExpressionEntity[] = []; + private readonly deoptimizedPaths: Record = Object.create(null); + private readonly expressionsToBeDeoptimizedByKey: Record< + string, + DeoptimizableEntity[] + > = Object.create(null); + private readonly gettersByKey: PropertyMap = Object.create(null); + private hasUnknownDeoptimizedInteger = false; + private hasUnknownDeoptimizedProperty = false; + private readonly propertiesAndGettersByKey: PropertyMap = Object.create(null); + private readonly propertiesAndSettersByKey: PropertyMap = Object.create(null); + private readonly settersByKey: PropertyMap = Object.create(null); + private readonly thisParametersToBeDeoptimized = new Set(); + private readonly unknownIntegerProps: ExpressionEntity[] = []; + private readonly unmatchableGetters: ExpressionEntity[] = []; + private readonly unmatchablePropertiesAndGetters: ExpressionEntity[] = []; + private readonly unmatchableSetters: ExpressionEntity[] = []; + + // If a PropertyMap is used, this will be taken as propertiesAndGettersByKey + // and we assume there are no setters or getters + constructor( + properties: ObjectProperty[] | PropertyMap, + private prototypeExpression: ExpressionEntity | null + ) { + super(); + if (Array.isArray(properties)) { + this.buildPropertyMaps(properties); + } else { + this.propertiesAndGettersByKey = this.propertiesAndSettersByKey = properties; + for (const propertiesForKey of Object.values(properties)) { + this.allProperties.push(...propertiesForKey); + } + } + } + + deoptimizeAllProperties(): void { + if (this.hasUnknownDeoptimizedProperty) { + return; + } + this.hasUnknownDeoptimizedProperty = true; + for (const properties of Object.values(this.propertiesAndGettersByKey).concat( + Object.values(this.settersByKey) + )) { + for (const property of properties) { + property.deoptimizePath(UNKNOWN_PATH); + } + } + // While the prototype itself cannot be mutated, each property can + this.prototypeExpression?.deoptimizePath([UnknownKey, UnknownKey]); + this.deoptimizeCachedEntities(); + } + + deoptimizeIntegerProperties(): void { + if (this.hasUnknownDeoptimizedProperty || this.hasUnknownDeoptimizedInteger) { + return; + } + this.hasUnknownDeoptimizedInteger = true; + for (const [key, propertiesAndGetters] of Object.entries(this.propertiesAndGettersByKey)) { + if (INTEGER_REG_EXP.test(key)) { + for (const property of propertiesAndGetters) { + property.deoptimizePath(UNKNOWN_PATH); + } + } + } + this.deoptimizeCachedIntegerEntities(); + } + + deoptimizePath(path: ObjectPath) { + if (this.hasUnknownDeoptimizedProperty) return; + const key = path[0]; + if (path.length === 1) { + if (typeof key !== 'string') { + if (key === UnknownInteger) { + return this.deoptimizeIntegerProperties(); + } + return this.deoptimizeAllProperties(); + } + if (!this.deoptimizedPaths[key]) { + this.deoptimizedPaths[key] = true; + + // we only deoptimizeCache exact matches as in all other cases, + // we do not return a literal value or return expression + const expressionsToBeDeoptimized = this.expressionsToBeDeoptimizedByKey[key]; + if (expressionsToBeDeoptimized) { + for (const expression of expressionsToBeDeoptimized) { + expression.deoptimizeCache(); + } + } + } + } + + const subPath = path.length === 1 ? UNKNOWN_PATH : path.slice(1); + for (const property of typeof key === 'string' + ? (this.propertiesAndGettersByKey[key] || this.unmatchablePropertiesAndGetters).concat( + this.settersByKey[key] || this.unmatchableSetters + ) + : this.allProperties) { + property.deoptimizePath(subPath); + } + this.prototypeExpression?.deoptimizePath(path.length === 1 ? [UnknownKey, UnknownKey] : path); + } + + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + if (path.length === 0) { + return; + } + const [key, ...subPath] = path; + + if ( + this.hasUnknownDeoptimizedProperty || + // single paths that are deoptimized will not become getters or setters + ((event === EVENT_CALLED || path.length > 1) && + typeof key === 'string' && + this.deoptimizedPaths[key]) + ) { + thisParameter.deoptimizePath(UNKNOWN_PATH); + return; + } + + const [propertiesForExactMatchByKey, relevantPropertiesByKey, relevantUnmatchableProperties] = + event === EVENT_CALLED || path.length > 1 + ? [ + this.propertiesAndGettersByKey, + this.propertiesAndGettersByKey, + this.unmatchablePropertiesAndGetters + ] + : event === EVENT_ACCESSED + ? [this.propertiesAndGettersByKey, this.gettersByKey, this.unmatchableGetters] + : [this.propertiesAndSettersByKey, this.settersByKey, this.unmatchableSetters]; + + if (typeof key === 'string') { + if (propertiesForExactMatchByKey[key]) { + const properties = relevantPropertiesByKey[key]; + if (properties) { + for (const property of properties) { + property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); + } + } + this.thisParametersToBeDeoptimized.add(thisParameter); + return; + } + for (const property of relevantUnmatchableProperties) { + property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); + } + if (INTEGER_REG_EXP.test(key)) { + for (const property of this.unknownIntegerProps) { + property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); + } + } + } else { + for (const properties of Object.values(relevantPropertiesByKey).concat([ + relevantUnmatchableProperties + ])) { + for (const property of properties) { + property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); + } + } + for (const property of this.unknownIntegerProps) { + property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); + } + } + this.thisParametersToBeDeoptimized.add(thisParameter); + this.prototypeExpression?.deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + } + + getLiteralValueAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + if (path.length === 0) { + return UnknownValue; + } + const key = path[0]; + const expressionAtPath = this.getMemberExpressionAndTrackDeopt(key, origin); + if (expressionAtPath) { + return expressionAtPath.getLiteralValueAtPath(path.slice(1), recursionTracker, origin); + } + if (this.prototypeExpression) { + return this.prototypeExpression.getLiteralValueAtPath(path, recursionTracker, origin); + } + if (path.length === 1) { + return undefined; + } + return UnknownValue; + } + + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): ExpressionEntity { + if (path.length === 0) { + return UNKNOWN_EXPRESSION; + } + const key = path[0]; + const expressionAtPath = this.getMemberExpressionAndTrackDeopt(key, origin); + if (expressionAtPath) { + return expressionAtPath.getReturnExpressionWhenCalledAtPath( + path.slice(1), + callOptions, + recursionTracker, + origin + ); + } + if (this.prototypeExpression) { + return this.prototypeExpression.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); + } + 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.hasUnknownDeoptimizedProperty) 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 { + const [key, ...subPath] = path; + if (path.length > 1) { + if (typeof key !== 'string') { + return true; + } + const expressionAtPath = this.getMemberExpression(key); + if (expressionAtPath) { + return expressionAtPath.hasEffectsWhenAssignedAtPath(subPath, context); + } + if (this.prototypeExpression) { + return this.prototypeExpression.hasEffectsWhenAssignedAtPath(path, context); + } + return true; + } + + if (this.hasUnknownDeoptimizedProperty) return true; + 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; + } + } + return false; + } + for (const property of this.unmatchableSetters) { + if (property.hasEffectsWhenAssignedAtPath(subPath, 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; + } + } + } + if (this.prototypeExpression) { + return this.prototypeExpression.hasEffectsWhenAssignedAtPath(path, 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: ObjectProperty[]): void { + const { + allProperties, + propertiesAndGettersByKey, + propertiesAndSettersByKey, + settersByKey, + gettersByKey, + unknownIntegerProps, + unmatchablePropertiesAndGetters, + unmatchableGetters, + unmatchableSetters + } = this; + const unmatchablePropertiesAndSetters: ExpressionEntity[] = []; + for (let index = properties.length - 1; index >= 0; index--) { + const { key, kind, property } = properties[index]; + allProperties.push(property); + if (typeof key !== 'string') { + if (key === UnknownInteger) { + unknownIntegerProps.push(property); + continue; + } + if (kind === 'set') unmatchableSetters.push(property); + if (kind === 'get') unmatchableGetters.push(property); + if (kind !== 'get') unmatchablePropertiesAndSetters.push(property); + if (kind !== 'set') unmatchablePropertiesAndGetters.push(property); + } else { + if (kind === 'set') { + if (!propertiesAndSettersByKey[key]) { + propertiesAndSettersByKey[key] = [property, ...unmatchablePropertiesAndSetters]; + settersByKey[key] = [property, ...unmatchableSetters]; + } + } else if (kind === 'get') { + if (!propertiesAndGettersByKey[key]) { + propertiesAndGettersByKey[key] = [property, ...unmatchablePropertiesAndGetters]; + gettersByKey[key] = [property, ...unmatchableGetters]; + } + } else { + if (!propertiesAndSettersByKey[key]) { + propertiesAndSettersByKey[key] = [property, ...unmatchablePropertiesAndSetters]; + } + if (!propertiesAndGettersByKey[key]) { + propertiesAndGettersByKey[key] = [property, ...unmatchablePropertiesAndGetters]; + if (INTEGER_REG_EXP.test(key)) { + for (const integerProperty of unknownIntegerProps) { + propertiesAndGettersByKey[key].push(integerProperty); + } + } + } + } + } + } + } + + private deoptimizeCachedEntities() { + for (const expressionsToBeDeoptimized of Object.values(this.expressionsToBeDeoptimizedByKey)) { + for (const expression of expressionsToBeDeoptimized) { + expression.deoptimizeCache(); + } + } + for (const expression of this.thisParametersToBeDeoptimized) { + expression.deoptimizePath(UNKNOWN_PATH); + } + } + + private deoptimizeCachedIntegerEntities() { + for (const [key, expressionsToBeDeoptimized] of Object.entries( + this.expressionsToBeDeoptimizedByKey + )) { + if (INTEGER_REG_EXP.test(key)) { + for (const expression of expressionsToBeDeoptimized) { + expression.deoptimizeCache(); + } + } + } + for (const expression of this.thisParametersToBeDeoptimized) { + expression.deoptimizePath(UNKNOWN_INTEGER_PATH); + } + } + + private getMemberExpression(key: ObjectPathKey): ExpressionEntity | null { + if ( + this.hasUnknownDeoptimizedProperty || + typeof key !== 'string' || + (this.hasUnknownDeoptimizedInteger && INTEGER_REG_EXP.test(key)) || + this.deoptimizedPaths[key] + ) { + return UNKNOWN_EXPRESSION; + } + const properties = this.propertiesAndGettersByKey[key]; + if (properties?.length === 1) { + return properties[0]; + } + if ( + properties || + this.unmatchablePropertiesAndGetters.length > 0 || + (this.unknownIntegerProps.length && INTEGER_REG_EXP.test(key)) + ) { + return UNKNOWN_EXPRESSION; + } + return null; + } + + private getMemberExpressionAndTrackDeopt( + key: ObjectPathKey, + origin: DeoptimizableEntity + ): ExpressionEntity | null { + if (typeof key !== 'string') { + return UNKNOWN_EXPRESSION; + } + const expression = this.getMemberExpression(key); + if (expression !== UNKNOWN_EXPRESSION) { + const expressionsToBeDeoptimized = (this.expressionsToBeDeoptimizedByKey[key] = + this.expressionsToBeDeoptimizedByKey[key] || []); + expressionsToBeDeoptimized.push(origin); + } + return expression; + } +} diff --git a/src/ast/nodes/shared/ObjectMember.ts b/src/ast/nodes/shared/ObjectMember.ts new file mode 100644 index 00000000000..d72db17c612 --- /dev/null +++ b/src/ast/nodes/shared/ObjectMember.ts @@ -0,0 +1,64 @@ +import { CallOptions } from '../../CallOptions'; +import { DeoptimizableEntity } from '../../DeoptimizableEntity'; +import { HasEffectsContext } from '../../ExecutionContext'; +import { NodeEvent } from '../../NodeEvents'; +import { ObjectPath, PathTracker } from '../../utils/PathTracker'; +import { ExpressionEntity, LiteralValueOrUnknown } from './Expression'; + +export class ObjectMember extends ExpressionEntity { + constructor(private readonly object: ExpressionEntity, private readonly key: string) { + super(); + } + + deoptimizePath(path: ObjectPath): void { + this.object.deoptimizePath([this.key, ...path]); + } + + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.object.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } + + getLiteralValueAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + return this.object.getLiteralValueAtPath([this.key, ...path], recursionTracker, origin); + } + + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): ExpressionEntity { + return this.object.getReturnExpressionWhenCalledAtPath( + [this.key, ...path], + callOptions, + recursionTracker, + origin + ); + } + + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + if (path.length === 0) return false; + return this.object.hasEffectsWhenAccessedAtPath([this.key, ...path], context); + } + + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + return this.object.hasEffectsWhenAssignedAtPath([this.key, ...path], context); + } + + hasEffectsWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + context: HasEffectsContext + ): boolean { + return this.object.hasEffectsWhenCalledAtPath([this.key, ...path], callOptions, context); + } +} diff --git a/src/ast/nodes/shared/ObjectPrototype.ts b/src/ast/nodes/shared/ObjectPrototype.ts new file mode 100644 index 00000000000..88e20e2d7a0 --- /dev/null +++ b/src/ast/nodes/shared/ObjectPrototype.ts @@ -0,0 +1,26 @@ +import { + METHOD_RETURNS_BOOLEAN, + METHOD_RETURNS_STRING, + METHOD_RETURNS_UNKNOWN +} from './MethodTypes'; +import { ObjectEntity } from './ObjectEntity'; + +export const OBJECT_PROTOTYPE = new ObjectEntity( + { + // @ts-ignore + __proto__: null, + // @ts-ignore + hasOwnProperty: METHOD_RETURNS_BOOLEAN, + // @ts-ignore + isPrototypeOf: METHOD_RETURNS_BOOLEAN, + // @ts-ignore + propertyIsEnumerable: METHOD_RETURNS_BOOLEAN, + // @ts-ignore + toLocaleString: METHOD_RETURNS_STRING, + // @ts-ignore + toString: METHOD_RETURNS_STRING, + // @ts-ignore + valueOf: METHOD_RETURNS_UNKNOWN + }, + null +); diff --git a/src/ast/scopes/BlockScope.ts b/src/ast/scopes/BlockScope.ts index b72bcc3a19e..d6393239f6e 100644 --- a/src/ast/scopes/BlockScope.ts +++ b/src/ast/scopes/BlockScope.ts @@ -1,7 +1,6 @@ import { AstContext } from '../../Module'; import Identifier from '../nodes/Identifier'; -import { ExpressionEntity } from '../nodes/shared/Expression'; -import { UNKNOWN_EXPRESSION } from '../values'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; import LocalVariable from '../variables/LocalVariable'; import ChildScope from './ChildScope'; diff --git a/src/ast/scopes/ClassBodyScope.ts b/src/ast/scopes/ClassBodyScope.ts index ebda392188a..83952d39dc6 100644 --- a/src/ast/scopes/ClassBodyScope.ts +++ b/src/ast/scopes/ClassBodyScope.ts @@ -1,6 +1,24 @@ +import { AstContext } from '../../Module'; +import { ExpressionEntity } from '../nodes/shared/Expression'; +import LocalVariable from '../variables/LocalVariable'; +import ThisVariable from '../variables/ThisVariable'; import ChildScope from './ChildScope'; +import Scope from './Scope'; export default class ClassBodyScope extends ChildScope { + instanceScope: ChildScope; + thisVariable: LocalVariable; + + constructor(parent: Scope, classNode: ExpressionEntity, context: AstContext) { + super(parent); + this.variables.set( + 'this', + (this.thisVariable = new LocalVariable('this', null, classNode, context)) + ); + this.instanceScope = new ChildScope(this); + this.instanceScope.variables.set('this', new ThisVariable(context)); + } + findLexicalBoundary() { return this; } diff --git a/src/ast/scopes/ParameterScope.ts b/src/ast/scopes/ParameterScope.ts index 8aafd8d4e09..77d30c09d7f 100644 --- a/src/ast/scopes/ParameterScope.ts +++ b/src/ast/scopes/ParameterScope.ts @@ -1,9 +1,9 @@ import { AstContext } from '../../Module'; import { InclusionContext } from '../ExecutionContext'; import Identifier from '../nodes/Identifier'; +import { UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; import { ExpressionNode } from '../nodes/shared/Node'; import SpreadElement from '../nodes/SpreadElement'; -import { UNKNOWN_EXPRESSION } from '../values'; import LocalVariable from '../variables/LocalVariable'; import ChildScope from './ChildScope'; import Scope from './Scope'; diff --git a/src/ast/scopes/ReturnValueScope.ts b/src/ast/scopes/ReturnValueScope.ts index 8647cd299ba..3045740825a 100644 --- a/src/ast/scopes/ReturnValueScope.ts +++ b/src/ast/scopes/ReturnValueScope.ts @@ -1,6 +1,5 @@ -import { ExpressionEntity } from '../nodes/shared/Expression'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; import { UNKNOWN_PATH } from '../utils/PathTracker'; -import { UNKNOWN_EXPRESSION } from '../values'; import ParameterScope from './ParameterScope'; export default class ReturnValueScope extends ParameterScope { diff --git a/src/ast/utils/PathTracker.ts b/src/ast/utils/PathTracker.ts index 74e10553744..0c2d8306e27 100644 --- a/src/ast/utils/PathTracker.ts +++ b/src/ast/utils/PathTracker.ts @@ -2,23 +2,49 @@ import { getOrCreate } from '../../utils/getOrCreate'; import { Entity } from '../Entity'; export const UnknownKey = Symbol('Unknown Key'); -export type ObjectPathKey = string | typeof UnknownKey; +export const UnknownInteger = Symbol('Unknown Integer'); +export type ObjectPathKey = string | typeof UnknownKey | typeof UnknownInteger; export type ObjectPath = ObjectPathKey[]; export const EMPTY_PATH: ObjectPath = []; export const UNKNOWN_PATH: ObjectPath = [UnknownKey]; +export const UNKNOWN_INTEGER_PATH: ObjectPath = [UnknownInteger]; const EntitiesKey = Symbol('Entities'); interface EntityPaths { [EntitiesKey]: Set; [UnknownKey]?: EntityPaths; + [UnknownInteger]?: EntityPaths; [pathSegment: string]: EntityPaths; } export class PathTracker { - entityPaths: EntityPaths = Object.create(null, { [EntitiesKey]: { value: new Set() } }); + private entityPaths: EntityPaths = Object.create(null, { + [EntitiesKey]: { value: new Set() } + }); + + trackEntityAtPathAndGetIfTracked(path: ObjectPath, entity: Entity): boolean { + const trackedEntities = this.getEntities(path); + if (trackedEntities.has(entity)) return true; + trackedEntities.add(entity); + return false; + } + + withTrackedEntityAtPath( + path: ObjectPath, + entity: Entity, + onUntracked: () => T, + returnIfTracked: T + ): T { + const trackedEntities = this.getEntities(path); + if (trackedEntities.has(entity)) return returnIfTracked; + trackedEntities.add(entity); + const result = onUntracked(); + trackedEntities.delete(entity); + return result; + } - getEntities(path: ObjectPath): Set { + private getEntities(path: ObjectPath): Set { let currentPaths = this.entityPaths; for (const pathSegment of path) { currentPaths = currentPaths[pathSegment] = @@ -34,21 +60,29 @@ export const SHARED_RECURSION_TRACKER = new PathTracker(); interface DiscriminatedEntityPaths { [EntitiesKey]: Map>; [UnknownKey]?: DiscriminatedEntityPaths; + [UnknownInteger]?: DiscriminatedEntityPaths; [pathSegment: string]: DiscriminatedEntityPaths; } export class DiscriminatedPathTracker { - entityPaths: DiscriminatedEntityPaths = Object.create(null, { + private entityPaths: DiscriminatedEntityPaths = Object.create(null, { [EntitiesKey]: { value: new Map>() } }); - getEntities(path: ObjectPath, discriminator: object): Set { + trackEntityAtPathAndGetIfTracked( + path: ObjectPath, + discriminator: object, + entity: Entity + ): boolean { let currentPaths = this.entityPaths; for (const pathSegment of path) { currentPaths = currentPaths[pathSegment] = currentPaths[pathSegment] || Object.create(null, { [EntitiesKey]: { value: new Map>() } }); } - return getOrCreate(currentPaths[EntitiesKey], discriminator, () => new Set()); + const trackedEntities = getOrCreate(currentPaths[EntitiesKey], discriminator, () => new Set()); + if (trackedEntities.has(entity)) return true; + trackedEntities.add(entity); + return false; } } diff --git a/src/ast/values.ts b/src/ast/values.ts index 0d7ea2ca50f..1edfae57d84 100644 --- a/src/ast/values.ts +++ b/src/ast/values.ts @@ -1,16 +1,12 @@ import { CallOptions, NO_ARGS } from './CallOptions'; -import { HasEffectsContext, InclusionContext } from './ExecutionContext'; +import { HasEffectsContext } from './ExecutionContext'; import { LiteralValue } from './nodes/Literal'; -import { ExpressionEntity } from './nodes/shared/Expression'; -import { ExpressionNode } from './nodes/shared/Node'; -import SpreadElement from './nodes/SpreadElement'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './nodes/shared/Expression'; import { EMPTY_PATH, ObjectPath, ObjectPathKey } from './utils/PathTracker'; export interface MemberDescription { callsArgs: number[] | null; - mutatesSelf: boolean; - returns: { new (): ExpressionEntity } | null; - returnsPrimitive: ExpressionEntity | null; + returns: ExpressionEntity; } export interface MemberDescriptions { @@ -28,87 +24,23 @@ function assembleMemberDescriptions( return Object.create(inheritedDescriptions, memberDescriptions); } -export const UnknownValue = Symbol('Unknown Value'); -export type LiteralValueOrUnknown = LiteralValue | typeof UnknownValue; - -abstract class ValueBase implements ExpressionEntity { - included = true; - - deoptimizePath() {} - - getLiteralValueAtPath(): LiteralValueOrUnknown { - return UnknownValue; - } - - getReturnExpressionWhenCalledAtPath(_path: ObjectPath) { - return UNKNOWN_EXPRESSION; - } - - hasEffectsWhenAccessedAtPath( - path: ObjectPath, - _context: HasEffectsContext - ) { - return path.length > 0 - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath) { - return path.length > 0 - } - - hasEffectsWhenCalledAtPath( - _path: ObjectPath, - _callOptions: CallOptions, - _context: HasEffectsContext - ) { - return true; - } - - include() {} - - includeCallArguments(_context: InclusionContext, _args: (ExpressionNode | SpreadElement)[]) {} - - mayModifyThisWhenCalledAtPath() { return true; } -} - -function includeAll(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]) { - for (const arg of args) { - arg.include(context, false); - } -} - -export const UNKNOWN_EXPRESSION: ExpressionEntity = new class extends ValueBase { - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - includeAll(context, args); - } -}; - -export const UNDEFINED_EXPRESSION: ExpressionEntity = new class extends ValueBase { +export const UNDEFINED_EXPRESSION: ExpressionEntity = new (class UndefinedExpression extends ExpressionEntity { getLiteralValueAtPath() { return undefined; } -}; +})(); const returnsUnknown: RawMemberDescription = { value: { callsArgs: null, - mutatesSelf: false, - returns: null, - returnsPrimitive: UNKNOWN_EXPRESSION + returns: UNKNOWN_EXPRESSION } }; -const mutatesSelfReturnsUnknown: RawMemberDescription = { - value: { returns: null, returnsPrimitive: UNKNOWN_EXPRESSION, callsArgs: null, mutatesSelf: true } -}; -const callsArgReturnsUnknown: RawMemberDescription = { - value: { returns: null, returnsPrimitive: UNKNOWN_EXPRESSION, callsArgs: [0], mutatesSelf: false } -}; - -export class UnknownArrayExpression extends ValueBase { - included = false; +export const UNKNOWN_LITERAL_BOOLEAN: ExpressionEntity = new (class UnknownBoolean extends ExpressionEntity { getReturnExpressionWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { - return getMemberReturnExpressionWhenCalled(arrayMembers, path[0]); + return getMemberReturnExpressionWhenCalled(literalBooleanMembers, path[0]); } return UNKNOWN_EXPRESSION; } @@ -117,185 +49,60 @@ export class UnknownArrayExpression extends ValueBase { return path.length > 1; } - hasEffectsWhenAssignedAtPath(path: ObjectPath) { - return path.length > 1; - } - hasEffectsWhenCalledAtPath( path: ObjectPath, callOptions: CallOptions, context: HasEffectsContext ) { if (path.length === 1) { - return hasMemberEffectWhenCalled(arrayMembers, path[0], this.included, callOptions, context); + return hasMemberEffectWhenCalled(literalBooleanMembers, path[0], callOptions, context); } return true; } - - include() { - this.included = true; - } - - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - includeAll(context, args); - } -} - -const returnsArray: RawMemberDescription = { - value: { - callsArgs: null, - mutatesSelf: false, - returns: UnknownArrayExpression, - returnsPrimitive: null - } -}; -const mutatesSelfReturnsArray: RawMemberDescription = { - value: { - callsArgs: null, - mutatesSelf: true, - returns: UnknownArrayExpression, - returnsPrimitive: null - } -}; -const callsArgReturnsArray: RawMemberDescription = { - value: { - callsArgs: [0], - mutatesSelf: false, - returns: UnknownArrayExpression, - returnsPrimitive: null - } -}; -const callsArgMutatesSelfReturnsArray: RawMemberDescription = { - value: { - callsArgs: [0], - mutatesSelf: true, - returns: UnknownArrayExpression, - returnsPrimitive: null - } -}; - -const UNKNOWN_LITERAL_BOOLEAN: ExpressionEntity = new class extends ValueBase { - getReturnExpressionWhenCalledAtPath(path: ObjectPath) { - if (path.length === 1) { - return getMemberReturnExpressionWhenCalled(literalBooleanMembers, path[0]); - } - return UNKNOWN_EXPRESSION; - } - hasEffectsWhenAccessedAtPath(path: ObjectPath) { - return path.length > 1 - } - hasEffectsWhenCalledAtPath(path: ObjectPath) { - if (path.length === 1) { - const subPath = path[0]; - return typeof subPath !== 'string' || !literalBooleanMembers[subPath]; - } - return true; - } - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - includeAll(context, args); - } -}; +})(); const returnsBoolean: RawMemberDescription = { value: { callsArgs: null, - mutatesSelf: false, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_BOOLEAN - } -}; -const callsArgReturnsBoolean: RawMemberDescription = { - value: { - callsArgs: [0], - mutatesSelf: false, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_BOOLEAN + returns: UNKNOWN_LITERAL_BOOLEAN } }; -const UNKNOWN_LITERAL_NUMBER: ExpressionEntity = new class extends ValueBase { +export const UNKNOWN_LITERAL_NUMBER: ExpressionEntity = new (class UnknownNumber extends ExpressionEntity { getReturnExpressionWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { return getMemberReturnExpressionWhenCalled(literalNumberMembers, path[0]); } return UNKNOWN_EXPRESSION; } - hasEffectsWhenAccessedAtPath(path: ObjectPath) { return path.length > 1; } - hasEffectsWhenCalledAtPath(path: ObjectPath) { - if (path.length === 1) { - const subPath = path[0]; - return typeof subPath !== 'string' || !literalNumberMembers[subPath]; - } - return true; - } - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - includeAll(context, args); - } -}; -const returnsNumber: RawMemberDescription = { - value: { - callsArgs: null, - mutatesSelf: false, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_NUMBER - } -}; -const mutatesSelfReturnsNumber: RawMemberDescription = { - value: { - callsArgs: null, - mutatesSelf: true, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_NUMBER - } -}; -const callsArgReturnsNumber: RawMemberDescription = { - value: { - callsArgs: [0], - mutatesSelf: false, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_NUMBER + hasEffectsWhenAccessedAtPath(path: ObjectPath) { + return path.length > 1; } -}; -const UNKNOWN_LITERAL_STRING: ExpressionEntity = new class extends ValueBase { - getReturnExpressionWhenCalledAtPath(path: ObjectPath) { - if (path.length === 1) { - return getMemberReturnExpressionWhenCalled(literalStringMembers, path[0]); - } - return UNKNOWN_EXPRESSION; - } - hasEffectsWhenAccessedAtPath(path: ObjectPath) { return path.length > 1 } hasEffectsWhenCalledAtPath( path: ObjectPath, callOptions: CallOptions, context: HasEffectsContext ) { if (path.length === 1) { - return hasMemberEffectWhenCalled(literalStringMembers, path[0], true, callOptions, context); + return hasMemberEffectWhenCalled(literalNumberMembers, path[0], callOptions, context); } return true; } - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - includeAll(context, args); - } -}; +})(); -const returnsString: RawMemberDescription = { +const returnsNumber: RawMemberDescription = { value: { callsArgs: null, - mutatesSelf: false, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_STRING + returns: UNKNOWN_LITERAL_NUMBER } }; -export class UnknownObjectExpression extends ValueBase { - included = false; - +export const UNKNOWN_LITERAL_STRING: ExpressionEntity = new (class UnknownString extends ExpressionEntity { getReturnExpressionWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { - return getMemberReturnExpressionWhenCalled(objectMembers, path[0]); + return getMemberReturnExpressionWhenCalled(literalStringMembers, path[0]); } return UNKNOWN_EXPRESSION; } @@ -304,31 +111,26 @@ export class UnknownObjectExpression extends ValueBase { return path.length > 1; } - hasEffectsWhenAssignedAtPath(path: ObjectPath) { - return path.length > 1; - } - hasEffectsWhenCalledAtPath( path: ObjectPath, callOptions: CallOptions, context: HasEffectsContext ) { if (path.length === 1) { - return hasMemberEffectWhenCalled(objectMembers, path[0], this.included, callOptions, context); + return hasMemberEffectWhenCalled(literalStringMembers, path[0], callOptions, context); } return true; } +})(); - include() { - this.included = true; - } - - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - includeAll(context, args); +const returnsString: RawMemberDescription = { + value: { + callsArgs: null, + returns: UNKNOWN_LITERAL_STRING } -} +}; -export const objectMembers: MemberDescriptions = assembleMemberDescriptions({ +const objectMembers: MemberDescriptions = assembleMemberDescriptions({ hasOwnProperty: returnsBoolean, isPrototypeOf: returnsBoolean, propertyIsEnumerable: returnsBoolean, @@ -337,36 +139,6 @@ export const objectMembers: MemberDescriptions = assembleMemberDescriptions({ valueOf: returnsUnknown }); -export const arrayMembers: MemberDescriptions = assembleMemberDescriptions( - { - concat: returnsArray, - copyWithin: mutatesSelfReturnsArray, - every: callsArgReturnsBoolean, - fill: mutatesSelfReturnsArray, - filter: callsArgReturnsArray, - find: callsArgReturnsUnknown, - findIndex: callsArgReturnsNumber, - forEach: callsArgReturnsUnknown, - includes: returnsBoolean, - indexOf: returnsNumber, - join: returnsString, - lastIndexOf: returnsNumber, - map: callsArgReturnsArray, - pop: mutatesSelfReturnsUnknown, - push: mutatesSelfReturnsNumber, - reduce: callsArgReturnsUnknown, - reduceRight: callsArgReturnsUnknown, - reverse: mutatesSelfReturnsArray, - shift: mutatesSelfReturnsUnknown, - slice: returnsArray, - some: callsArgReturnsBoolean, - sort: callsArgMutatesSelfReturnsArray, - splice: mutatesSelfReturnsArray, - unshift: mutatesSelfReturnsNumber - }, - objectMembers -); - const literalBooleanMembers: MemberDescriptions = assembleMemberDescriptions( { valueOf: returnsBoolean @@ -404,14 +176,12 @@ const literalStringMembers: MemberDescriptions = assembleMemberDescriptions( replace: { value: { callsArgs: [1], - mutatesSelf: false, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_STRING + returns: UNKNOWN_LITERAL_STRING } }, search: returnsNumber, slice: returnsString, - split: returnsArray, + split: returnsUnknown, startsWith: returnsBoolean, substr: returnsString, substring: returnsString, @@ -441,16 +211,12 @@ export function getLiteralMembersForValue export function hasMemberEffectWhenCalled( members: MemberDescriptions, memberName: ObjectPathKey, - parentIncluded: boolean, callOptions: CallOptions, context: HasEffectsContext ) { - if ( - typeof memberName !== 'string' || - !members[memberName] || - (members[memberName].mutatesSelf && parentIncluded) - ) + if (typeof memberName !== 'string' || !members[memberName]) { return true; + } if (!members[memberName].callsArgs) return false; for (const argIndex of members[memberName].callsArgs!) { if ( @@ -459,6 +225,7 @@ export function hasMemberEffectWhenCalled( EMPTY_PATH, { args: NO_ARGS, + thisParam: null, withNew: false }, context @@ -474,7 +241,5 @@ export function getMemberReturnExpressionWhenCalled( memberName: ObjectPathKey ): ExpressionEntity { if (typeof memberName !== 'string' || !members[memberName]) return UNKNOWN_EXPRESSION; - return members[memberName].returnsPrimitive !== null - ? members[memberName].returnsPrimitive! - : new members[memberName].returns!(); + return members[memberName].returns; } diff --git a/src/ast/variables/ArgumentsVariable.ts b/src/ast/variables/ArgumentsVariable.ts index b67ff2f0de4..d511000e0cd 100644 --- a/src/ast/variables/ArgumentsVariable.ts +++ b/src/ast/variables/ArgumentsVariable.ts @@ -1,6 +1,6 @@ import { AstContext } from '../../Module'; +import { UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; import { ObjectPath } from '../utils/PathTracker'; -import { UNKNOWN_EXPRESSION } from '../values'; import LocalVariable from './LocalVariable'; export default class ArgumentsVariable extends LocalVariable { diff --git a/src/ast/variables/LocalVariable.ts b/src/ast/variables/LocalVariable.ts index 6c3ec83133d..346d1687487 100644 --- a/src/ast/variables/LocalVariable.ts +++ b/src/ast/variables/LocalVariable.ts @@ -2,14 +2,19 @@ import Module, { AstContext } from '../../Module'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { createInclusionContext, HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { NodeEvent } from '../NodeEvents'; import ExportDefaultDeclaration from '../nodes/ExportDefaultDeclaration'; import Identifier from '../nodes/Identifier'; import * as NodeType from '../nodes/NodeType'; -import { ExpressionEntity } from '../nodes/shared/Expression'; +import { + ExpressionEntity, + LiteralValueOrUnknown, + UnknownValue, + UNKNOWN_EXPRESSION +} from '../nodes/shared/Expression'; import { ExpressionNode, Node } from '../nodes/shared/Node'; import SpreadElement from '../nodes/SpreadElement'; import { ObjectPath, PathTracker, UNKNOWN_PATH } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../values'; import Variable from './Variable'; // To avoid infinite recursions @@ -24,7 +29,7 @@ export default class LocalVariable extends Variable { // Caching and deoptimization: // We track deoptimization when we do not return something unknown - private deoptimizationTracker: PathTracker; + protected deoptimizationTracker: PathTracker; private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; constructor( @@ -62,10 +67,13 @@ export default class LocalVariable extends Variable { } deoptimizePath(path: ObjectPath) { - if (path.length > MAX_PATH_DEPTH || this.isReassigned) return; - const trackedEntities = this.deoptimizationTracker.getEntities(path); - if (trackedEntities.has(this)) return; - trackedEntities.add(this); + if ( + path.length > MAX_PATH_DEPTH || + this.isReassigned || + this.deoptimizationTracker.trackEntityAtPathAndGetIfTracked(path, this) + ) { + return; + } if (path.length === 0) { if (!this.isReassigned) { this.isReassigned = true; @@ -74,13 +82,28 @@ export default class LocalVariable extends Variable { for (const expression of expressionsToBeDeoptimized) { expression.deoptimizeCache(); } - if (this.init) { - this.init.deoptimizePath(UNKNOWN_PATH); - } + this.init?.deoptimizePath(UNKNOWN_PATH); } - } else if (this.init) { - this.init.deoptimizePath(path); + } else { + this.init?.deoptimizePath(path); + } + } + + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ): void { + if (this.isReassigned || !this.init || path.length > MAX_PATH_DEPTH) { + return thisParameter.deoptimizePath(UNKNOWN_PATH); } + recursionTracker.withTrackedEntityAtPath( + path, + this.init, + () => this.init!.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker), + undefined + ); } getLiteralValueAtPath( @@ -91,53 +114,56 @@ export default class LocalVariable extends Variable { if (this.isReassigned || !this.init || path.length > MAX_PATH_DEPTH) { return UnknownValue; } - const trackedEntities = recursionTracker.getEntities(path); - if (trackedEntities.has(this.init)) { - return UnknownValue; - } - this.expressionsToBeDeoptimized.push(origin); - trackedEntities.add(this.init); - const value = this.init.getLiteralValueAtPath(path, recursionTracker, origin); - trackedEntities.delete(this.init); - return value; + return recursionTracker.withTrackedEntityAtPath( + path, + this.init, + () => { + this.expressionsToBeDeoptimized.push(origin); + return this.init!.getLiteralValueAtPath(path, recursionTracker, origin); + }, + UnknownValue + ); } getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { if (this.isReassigned || !this.init || path.length > MAX_PATH_DEPTH) { return UNKNOWN_EXPRESSION; } - const trackedEntities = recursionTracker.getEntities(path); - if (trackedEntities.has(this.init)) { - return UNKNOWN_EXPRESSION; - } - this.expressionsToBeDeoptimized.push(origin); - trackedEntities.add(this.init); - const value = this.init.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); - trackedEntities.delete(this.init); - return value; + return recursionTracker.withTrackedEntityAtPath( + path, + this.init, + () => { + this.expressionsToBeDeoptimized.push(origin); + return this.init!.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); + }, + UNKNOWN_EXPRESSION + ); } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { - if (path.length === 0) return false; if (this.isReassigned || path.length > MAX_PATH_DEPTH) return true; - const trackedExpressions = context.accessed.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return (this.init && this.init.hasEffectsWhenAccessedAtPath(path, context))!; + return (this.init && + !context.accessed.trackEntityAtPathAndGetIfTracked(path, this) && + this.init.hasEffectsWhenAccessedAtPath(path, context))!; } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext) { if (this.included || path.length > MAX_PATH_DEPTH) return true; if (path.length === 0) return false; if (this.isReassigned) return true; - const trackedExpressions = context.assigned.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return (this.init && this.init.hasEffectsWhenAssignedAtPath(path, context))!; + return (this.init && + !context.accessed.trackEntityAtPathAndGetIfTracked(path, this) && + this.init.hasEffectsWhenAssignedAtPath(path, context))!; } hasEffectsWhenCalledAtPath( @@ -146,13 +172,12 @@ export default class LocalVariable extends Variable { context: HasEffectsContext ) { if (path.length > MAX_PATH_DEPTH || this.isReassigned) return true; - const trackedExpressions = (callOptions.withNew - ? context.instantiated - : context.called - ).getEntities(path, callOptions); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return (this.init && this.init.hasEffectsWhenCalledAtPath(path, callOptions, context))!; + return (this.init && + !(callOptions.withNew + ? context.instantiated + : context.called + ).trackEntityAtPathAndGetIfTracked(path, callOptions, this) && + this.init.hasEffectsWhenCalledAtPath(path, callOptions, context))!; } include() { @@ -188,18 +213,4 @@ export default class LocalVariable extends Variable { markCalledFromTryStatement() { this.calledFromTryStatement = true; } - - mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker) { - if (this.isReassigned || !this.init || path.length > MAX_PATH_DEPTH) { - return true; - } - const trackedEntities = recursionTracker.getEntities(path); - if (trackedEntities.has(this.init)) { - return true; - } - trackedEntities.add(this.init); - const result = this.init.mayModifyThisWhenCalledAtPath(path, recursionTracker); - trackedEntities.delete(this.init); - return result; - } } diff --git a/src/ast/variables/NamespaceVariable.ts b/src/ast/variables/NamespaceVariable.ts index 0ed3fbbcfd1..62bf7ac2b0e 100644 --- a/src/ast/variables/NamespaceVariable.ts +++ b/src/ast/variables/NamespaceVariable.ts @@ -3,7 +3,6 @@ import { RenderOptions } from '../../utils/renderHelpers'; import { RESERVED_NAMES } from '../../utils/reservedNames'; import { getSystemExportStatement } from '../../utils/systemJsRendering'; import Identifier from '../nodes/Identifier'; -import { ObjectPath, UNKNOWN_PATH } from '../utils/PathTracker'; import Variable from './Variable'; export default class NamespaceVariable extends Variable { @@ -29,19 +28,6 @@ export default class NamespaceVariable extends Variable { this.name = identifier.name; } - deoptimizePath(path: ObjectPath) { - const memberVariables = this.getMemberVariables(); - const memberPath = path.length <= 1 ? UNKNOWN_PATH : path.slice(1); - const key = path[0]; - if (typeof key === 'string') { - memberVariables[key]?.deoptimizePath(memberPath); - } else { - for (const key of Object.keys(memberVariables)) { - memberVariables[key].deoptimizePath(memberPath); - } - } - } - getMemberVariables(): { [name: string]: Variable } { if (this.memberVariables) { return this.memberVariables; @@ -80,9 +66,7 @@ export default class NamespaceVariable extends Variable { const t = options.indent; const memberVariables = this.getMemberVariables(); - const members = Object.keys(memberVariables).map(name => { - const original = memberVariables[name]; - + const members = Object.entries(memberVariables).map(([name, original]) => { if (this.referencedEarly || original.isReassigned) { return `${t}get ${name}${_}()${_}{${_}return ${original.getName()}${ options.compact ? '' : ';' diff --git a/src/ast/variables/ThisVariable.ts b/src/ast/variables/ThisVariable.ts index b0fbe27e39a..76ba2127b5a 100644 --- a/src/ast/variables/ThisVariable.ts +++ b/src/ast/variables/ThisVariable.ts @@ -1,18 +1,58 @@ import { AstContext } from '../../Module'; -import { CallOptions } from '../CallOptions'; import { HasEffectsContext } from '../ExecutionContext'; -import { ExpressionEntity } from '../nodes/shared/Expression'; -import { ObjectPath } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../values'; +import { NodeEvent } from '../NodeEvents'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; +import { ObjectPath, SHARED_RECURSION_TRACKER } from '../utils/PathTracker'; import LocalVariable from './LocalVariable'; +interface ThisDeoptimizationEvent { + event: NodeEvent; + path: ObjectPath; + thisParameter: ExpressionEntity; +} + export default class ThisVariable extends LocalVariable { + private deoptimizedPaths: ObjectPath[] = []; + private entitiesToBeDeoptimized = new Set(); + private thisDeoptimizations: ThisDeoptimizationEvent[] = []; + constructor(context: AstContext) { super('this', null, null, context); } - getLiteralValueAtPath(): LiteralValueOrUnknown { - return UnknownValue; + addEntityToBeDeoptimized(entity: ExpressionEntity) { + for (const path of this.deoptimizedPaths) { + entity.deoptimizePath(path); + } + for (const thisDeoptimization of this.thisDeoptimizations) { + this.applyThisDeoptimizationEvent(entity, thisDeoptimization); + } + this.entitiesToBeDeoptimized.add(entity); + } + + deoptimizePath(path: ObjectPath) { + if ( + path.length === 0 || + this.deoptimizationTracker.trackEntityAtPathAndGetIfTracked(path, this) + ) { + return; + } + this.deoptimizedPaths.push(path); + for (const entity of this.entitiesToBeDeoptimized) { + entity.deoptimizePath(path); + } + } + + deoptimizeThisOnEventAtPath(event: NodeEvent, path: ObjectPath, thisParameter: ExpressionEntity) { + const thisDeoptimization: ThisDeoptimizationEvent = { + event, + path, + thisParameter + }; + for (const entity of this.entitiesToBeDeoptimized) { + this.applyThisDeoptimizationEvent(entity, thisDeoptimization); + } + this.thisDeoptimizations.push(thisDeoptimization); } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { @@ -29,14 +69,15 @@ export default class ThisVariable extends LocalVariable { ); } - hasEffectsWhenCalledAtPath( - path: ObjectPath, - callOptions: CallOptions, - context: HasEffectsContext + private applyThisDeoptimizationEvent( + entity: ExpressionEntity, + { event, path, thisParameter }: ThisDeoptimizationEvent ) { - return ( - this.getInit(context).hasEffectsWhenCalledAtPath(path, callOptions, context) || - super.hasEffectsWhenCalledAtPath(path, callOptions, context) + entity.deoptimizeThisOnEventAtPath( + event, + path, + thisParameter === this ? entity : thisParameter, + SHARED_RECURSION_TRACKER ); } diff --git a/src/ast/variables/UndefinedVariable.ts b/src/ast/variables/UndefinedVariable.ts index 32e45285abd..83a8d6220df 100644 --- a/src/ast/variables/UndefinedVariable.ts +++ b/src/ast/variables/UndefinedVariable.ts @@ -1,4 +1,4 @@ -import { LiteralValueOrUnknown } from '../values'; +import { LiteralValueOrUnknown } from '../nodes/shared/Expression'; import Variable from './Variable'; export default class UndefinedVariable extends Variable { diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index 9a3564ff4d7..fef15ed6f25 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -1,30 +1,23 @@ import ExternalModule from '../../ExternalModule'; import Module from '../../Module'; import { RESERVED_NAMES } from '../../utils/reservedNames'; -import { CallOptions } from '../CallOptions'; -import { DeoptimizableEntity } from '../DeoptimizableEntity'; -import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { HasEffectsContext } from '../ExecutionContext'; import Identifier from '../nodes/Identifier'; import { ExpressionEntity } from '../nodes/shared/Expression'; -import { ExpressionNode } from '../nodes/shared/Node'; -import SpreadElement from '../nodes/SpreadElement'; -import { ObjectPath, PathTracker } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../values'; +import { ObjectPath } from '../utils/PathTracker'; -export default class Variable implements ExpressionEntity { +export default class Variable extends ExpressionEntity { alwaysRendered = false; - included = false; isId = false; // both NamespaceVariable and ExternalVariable can be namespaces isNamespace?: boolean; isReassigned = false; module?: Module | ExternalModule; - name: string; renderBaseName: string | null = null; renderName: string | null = null; - constructor(name: string) { - this.name = name; + constructor(public name: string) { + super(); } /** @@ -33,20 +26,10 @@ export default class Variable implements ExpressionEntity { */ addReference(_identifier: Identifier) {} - deoptimizePath(_path: ObjectPath) {} - getBaseVariableName(): string { return this.renderBaseName || this.renderName || this.name; } - getLiteralValueAtPath( - _path: ObjectPath, - _recursionTracker: PathTracker, - _origin: DeoptimizableEntity - ): LiteralValueOrUnknown { - return UnknownValue; - } - getName(): string { const name = this.renderName || this.name; return this.renderBaseName @@ -54,30 +37,10 @@ export default class Variable implements ExpressionEntity { : name; } - getReturnExpressionWhenCalledAtPath( - _path: ObjectPath, - _recursionTracker: PathTracker, - _origin: DeoptimizableEntity - ): ExpressionEntity { - return UNKNOWN_EXPRESSION; - } - hasEffectsWhenAccessedAtPath(path: ObjectPath, _context: HasEffectsContext) { return path.length > 0; } - hasEffectsWhenAssignedAtPath(_path: ObjectPath, _context: HasEffectsContext) { - return true; - } - - hasEffectsWhenCalledAtPath( - _path: ObjectPath, - _callOptions: CallOptions, - _context: HasEffectsContext - ) { - return true; - } - /** * Marks this variable as being part of the bundle, which is usually the case when one of * its identifiers becomes part of the bundle. Returns true if it has not been included @@ -88,18 +51,8 @@ export default class Variable implements ExpressionEntity { this.included = true; } - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - for (const arg of args) { - arg.include(context, false); - } - } - markCalledFromTryStatement() {} - mayModifyThisWhenCalledAtPath(_path: ObjectPath, _recursionTracker: PathTracker) { - return true; - } - setRenderNames(baseName: string | null, name: string | null) { this.renderBaseName = baseName; this.renderName = name; diff --git a/src/rollup/rollup.ts b/src/rollup/rollup.ts index 86519eb5439..b013af1c84f 100644 --- a/src/rollup/rollup.ts +++ b/src/rollup/rollup.ts @@ -170,9 +170,7 @@ async function handleGenerateWrite( message: 'You must specify "output.file" or "output.dir" for the build.' }); } - await Promise.all( - Object.keys(generated).map(chunkId => writeOutputFile(generated[chunkId], outputOptions)) - ); + await Promise.all(Object.values(generated).map(chunk => writeOutputFile(chunk, outputOptions))); await outputPluginDriver.hookParallel('writeBundle', [outputOptions, generated]); } return createOutput(generated); @@ -228,12 +226,9 @@ function getOutputOptions( function createOutput(outputBundle: Record): RollupOutput { return { - output: (Object.keys(outputBundle) - .map(fileName => outputBundle[fileName]) - .filter(outputFile => Object.keys(outputFile).length > 0) as ( - | OutputChunk - | OutputAsset - )[]).sort((outputFileA, outputFileB) => { + output: (Object.values(outputBundle).filter( + outputFile => Object.keys(outputFile).length > 0 + ) as (OutputChunk | OutputAsset)[]).sort((outputFileA, outputFileB) => { const fileTypeA = getSortingFileType(outputFileA); const fileTypeB = getSortingFileType(outputFileB); if (fileTypeA === fileTypeB) return 0; diff --git a/src/utils/FileEmitter.ts b/src/utils/FileEmitter.ts index 65520b91fc3..8f0a3e1abd3 100644 --- a/src/utils/FileEmitter.ts +++ b/src/utils/FileEmitter.ts @@ -106,9 +106,12 @@ function hasValidType( ); } -function hasValidName(emittedFile: { type: 'asset' | 'chunk'; [key: string]: unknown; }): emittedFile is EmittedFile { +function hasValidName(emittedFile: { + type: 'asset' | 'chunk'; + [key: string]: unknown; +}): emittedFile is EmittedFile { const validatedName = emittedFile.fileName || emittedFile.name; - return !validatedName || typeof validatedName === 'string' && !isPathFragment(validatedName); + return !validatedName || (typeof validatedName === 'string' && !isPathFragment(validatedName)); } function getValidSource( @@ -351,8 +354,7 @@ function findExistingAssetFileNameWithSource( bundle: OutputBundleWithPlaceholders, source: string | Uint8Array ): string | null { - for (const fileName of Object.keys(bundle)) { - const outputFile = bundle[fileName]; + for (const [fileName, outputFile] of Object.entries(bundle)) { if (outputFile.type === 'asset' && areSourcesEqual(source, outputFile.source)) return fileName; } return null; diff --git a/src/utils/chunkAssignment.ts b/src/utils/chunkAssignment.ts index b4fb355377c..3490ddaf9ad 100644 --- a/src/utils/chunkAssignment.ts +++ b/src/utils/chunkAssignment.ts @@ -185,8 +185,8 @@ function createChunks( chunkModules[chunkSignature] = [module]; } } - return Object.keys(chunkModules).map(chunkSignature => ({ + return Object.values(chunkModules).map(modules => ({ alias: null, - modules: chunkModules[chunkSignature] + modules })); } diff --git a/src/utils/options/normalizeInputOptions.ts b/src/utils/options/normalizeInputOptions.ts index 649a10c6ff0..52626aff142 100644 --- a/src/utils/options/normalizeInputOptions.ts +++ b/src/utils/options/normalizeInputOptions.ts @@ -187,8 +187,8 @@ const getModuleContext = ( } if (configModuleContext) { const contextByModuleId = Object.create(null); - for (const key of Object.keys(configModuleContext)) { - contextByModuleId[resolve(key)] = configModuleContext[key]; + for (const [key, moduleContext] of Object.entries(configModuleContext)) { + contextByModuleId[resolve(key)] = moduleContext; } return id => contextByModuleId[id] || context; } diff --git a/src/utils/timers.ts b/src/utils/timers.ts index 5f030bbfafb..5ef2f77ba8d 100644 --- a/src/utils/timers.ts +++ b/src/utils/timers.ts @@ -76,8 +76,8 @@ function timeEndImpl(label: string, level = 3) { export function getTimings(): SerializedTimings { const newTimings: SerializedTimings = {}; - for (const label of Object.keys(timers)) { - newTimings[label] = [timers[label].time, timers[label].memory, timers[label].totalMemory]; + for (const [label, { time, memory, totalMemory }] of Object.entries(timers)) { + newTimings[label] = [time, memory, totalMemory]; } return newTimings; } @@ -102,7 +102,7 @@ function getPluginWithTimers(plugin: any, index: number): Plugin { timerLabel += ` (${plugin.name})`; } timerLabel += ` - ${hook}`; - timedPlugin[hook] = function() { + timedPlugin[hook] = function () { timeStart(timerLabel, 4); let result = plugin[hook].apply(this === timedPlugin ? plugin : this, arguments); timeEnd(timerLabel, 4); diff --git a/test/cli/samples/watch/watch-config-no-update/_config.js b/test/cli/samples/watch/watch-config-no-update/_config.js index c93075b0cc3..635d77d42d9 100644 --- a/test/cli/samples/watch/watch-config-no-update/_config.js +++ b/test/cli/samples/watch/watch-config-no-update/_config.js @@ -29,7 +29,7 @@ module.exports = { }, stderr(stderr) { if ( - !/^rollup v\d+\.\d+\.\d+\nbundles main.js → _actual[\\/]main.js...\ncreated _actual[\\/]main.js in \d+ms\n$/.test( + !/^rollup v\d+\.\d+\.\d+(-\d+)?\nbundles main.js → _actual[\\/]main.js...\ncreated _actual[\\/]main.js in \d+ms\n$/.test( stderr ) ) { diff --git a/test/form/samples/array-element-tracking/array-elements/_config.js b/test/form/samples/array-element-tracking/array-elements/_config.js new file mode 100644 index 00000000000..46d8f9b2d3d --- /dev/null +++ b/test/form/samples/array-element-tracking/array-elements/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks elements of arrays' +}; diff --git a/test/form/samples/array-element-tracking/array-elements/_expected.js b/test/form/samples/array-element-tracking/array-elements/_expected.js new file mode 100644 index 00000000000..350400b9918 --- /dev/null +++ b/test/form/samples/array-element-tracking/array-elements/_expected.js @@ -0,0 +1,4 @@ +console.log('retained'); +console.log('retained'); +console.log('retained'); +console.log('retained'); diff --git a/test/form/samples/array-element-tracking/array-elements/main.js b/test/form/samples/array-element-tracking/array-elements/main.js new file mode 100644 index 00000000000..31792cf92ec --- /dev/null +++ b/test/form/samples/array-element-tracking/array-elements/main.js @@ -0,0 +1,10 @@ +const array = [true, false, 3]; + +if (array[0]) console.log('retained'); +else console.log('removed'); +if (array[1]) console.log('removed'); +else console.log('retained'); +if (array[2] === 3) console.log('retained'); +else console.log('removed'); +if (array[3]) console.log('removed'); +else console.log('retained'); diff --git a/test/form/samples/array-element-tracking/array-mutation/_config.js b/test/form/samples/array-element-tracking/array-mutation/_config.js new file mode 100644 index 00000000000..58a8183eebf --- /dev/null +++ b/test/form/samples/array-element-tracking/array-mutation/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks mutations of array elements' +}; diff --git a/test/form/samples/array-element-tracking/array-mutation/_expected.js b/test/form/samples/array-element-tracking/array-mutation/_expected.js new file mode 100644 index 00000000000..b4c4acc5b9a --- /dev/null +++ b/test/form/samples/array-element-tracking/array-mutation/_expected.js @@ -0,0 +1,10 @@ +const array = [true, true]; + +array[1] = false; +array[2] = true; + +console.log('retained'); +if (array[1]) console.log('unimportant'); +else console.log('retained'); +if (array[2]) console.log('retained'); +else console.log('unimportant'); diff --git a/test/form/samples/array-element-tracking/array-mutation/main.js b/test/form/samples/array-element-tracking/array-mutation/main.js new file mode 100644 index 00000000000..37327b20616 --- /dev/null +++ b/test/form/samples/array-element-tracking/array-mutation/main.js @@ -0,0 +1,11 @@ +const array = [true, true]; + +array[1] = false; +array[2] = true; + +if (array[0]) console.log('retained'); +else console.log('removed'); +if (array[1]) console.log('unimportant'); +else console.log('retained'); +if (array[2]) console.log('retained'); +else console.log('unimportant'); diff --git a/test/form/samples/array-element-tracking/array-spread/_config.js b/test/form/samples/array-element-tracking/array-spread/_config.js new file mode 100644 index 00000000000..57b80f7746f --- /dev/null +++ b/test/form/samples/array-element-tracking/array-spread/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks elements of arrays with spread elements' +}; diff --git a/test/form/samples/array-element-tracking/array-spread/_expected.js b/test/form/samples/array-element-tracking/array-spread/_expected.js new file mode 100644 index 00000000000..2d0ec880684 --- /dev/null +++ b/test/form/samples/array-element-tracking/array-spread/_expected.js @@ -0,0 +1,9 @@ +const spread = [true, false]; +const array = [true, false, ...spread]; + +console.log('retained'); +console.log('retained'); +if (array[2]) console.log('retained'); +else console.log('unimportant'); +if (array[3]) console.log('unimportant'); +else console.log('retained'); diff --git a/test/form/samples/array-element-tracking/array-spread/main.js b/test/form/samples/array-element-tracking/array-spread/main.js new file mode 100644 index 00000000000..cf36dae8591 --- /dev/null +++ b/test/form/samples/array-element-tracking/array-spread/main.js @@ -0,0 +1,11 @@ +const spread = [true, false]; +const array = [true, false, ...spread]; + +if (array[0]) console.log('retained'); +else console.log('removed'); +if (array[1]) console.log('removed'); +else console.log('retained'); +if (array[2]) console.log('retained'); +else console.log('unimportant'); +if (array[3]) console.log('unimportant'); +else console.log('retained'); diff --git a/test/form/samples/array-element-tracking/spread-element-deoptimization/_config.js b/test/form/samples/array-element-tracking/spread-element-deoptimization/_config.js new file mode 100644 index 00000000000..b1ce2636a6d --- /dev/null +++ b/test/form/samples/array-element-tracking/spread-element-deoptimization/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'deoptimizes array spread elements' +}; diff --git a/test/form/samples/array-element-tracking/spread-element-deoptimization/_expected.js b/test/form/samples/array-element-tracking/spread-element-deoptimization/_expected.js new file mode 100644 index 00000000000..a2633a81cc0 --- /dev/null +++ b/test/form/samples/array-element-tracking/spread-element-deoptimization/_expected.js @@ -0,0 +1,3 @@ +const spread = [{ effect() {} }]; +[...spread][0].effect = () => console.log('effect'); +spread[0].effect(); diff --git a/test/form/samples/array-element-tracking/spread-element-deoptimization/main.js b/test/form/samples/array-element-tracking/spread-element-deoptimization/main.js new file mode 100644 index 00000000000..a2633a81cc0 --- /dev/null +++ b/test/form/samples/array-element-tracking/spread-element-deoptimization/main.js @@ -0,0 +1,3 @@ +const spread = [{ effect() {} }]; +[...spread][0].effect = () => console.log('effect'); +spread[0].effect(); diff --git a/test/form/samples/arrow-function-return-values/_expected.js b/test/form/samples/arrow-function-return-values/_expected.js index 1bedb0a7bbe..a3fd2df3d87 100644 --- a/test/form/samples/arrow-function-return-values/_expected.js +++ b/test/form/samples/arrow-function-return-values/_expected.js @@ -6,9 +6,5 @@ retained1()(); (() => { return () => console.log( 'effect' ); })()(); - -(() => ({ foo: () => {} }))().foo(); (() => ({ foo: () => console.log( 'effect' ) }))().foo(); - -(() => ({ foo: () => ({ bar: () => ({ baz: () => {} }) }) }))().foo().bar().baz(); (() => ({ foo: () => ({ bar: () => console.log( 'effect' ) }) }))().foo().bar(); diff --git a/test/form/samples/builtin-prototypes/array-expression/_expected.js b/test/form/samples/builtin-prototypes/array-expression/_expected.js index 6d040eeda50..c0eda2afc00 100644 --- a/test/form/samples/builtin-prototypes/array-expression/_expected.js +++ b/test/form/samples/builtin-prototypes/array-expression/_expected.js @@ -1,31 +1,76 @@ -[ 1 ].map( x => console.log( 1 ) ); -[ 1 ].map( x => x ).map( x => console.log( 1 ) ); -[ 1 ].map( x => console.log( 1 ) ).map( x => x ); -[ 1 ].map( x => x ).map( x => x ).map( x => console.log( 1 ) ); -[ 1 ].map( x => x ).map( x => console.log( 1 ) ).map( x => x ); +[1].map(x => console.log(1)); +[1].map(x => x).map(x => console.log(1)); +[1].map(x => console.log(1)).map(x => x); +[1] + .map(x => x) + .map(x => x) + .map(x => console.log(1)); +[1] + .map(x => x) + .map(x => console.log(1)) + .map(x => x); [](); -[ 1 ].every( () => console.log( 1 ) || true ); -[ 1 ].filter( () => console.log( 1 ) || true ); -[ 1 ].find( () => console.log( 1 ) || true ); -[ 1 ].findIndex( () => console.log( 1 ) || true ); -[ 1 ].forEach( () => console.log( 1 ) || true ); -[ 1 ].map( () => console.log( 1 ) || 1 ); -[ 1 ].reduce( () => console.log( 1 ) || 1, 1 ); -[ 1 ].reduceRight( () => console.log( 1 ) || 1, 1 ); -[ 1 ].some( () => console.log( 1 ) || true ); +const _atArray = [{ effect() {} }]; +_atArray.at(0).effect = () => console.log(1); +_atArray[0].effect(); +const _entriesArray = [{ effect() {} }]; +[..._entriesArray.entries()][0][1].effect = () => console.log(1); +_entriesArray[0].effect(); +const _sliceArray = [{ effect() {} }]; +_sliceArray.slice()[0].effect = () => console.log(1); +_sliceArray[0].effect(); +const _valuesArray = [{ effect() {} }]; +[..._valuesArray.values()][0].effect = () => console.log(1); +_valuesArray[0].effect(); +[1].every(() => console.log(1) || true); +const _everyArray = [{ effect() {} }]; +_everyArray.every(element => (element.effect = () => console.log(1))); +_everyArray[0].effect(); +[1].filter(() => console.log(1) || true); +const _filterArray = [{ effect() {} }]; +_filterArray.filter(element => (element.effect = () => console.log(1))); +_filterArray[0].effect(); +[1].find(() => console.log(1) || true); +const _findArray = [{ effect() {} }]; +_findArray.find(element => (element.effect = () => console.log(1))); +_findArray[0].effect(); +[1].findIndex(() => console.log(1) || true); +const _findIndexArray = [{ effect() {} }]; +_findIndexArray.findIndex(element => (element.effect = () => console.log(1))); +_findIndexArray[0].effect(); +[1].forEach(() => console.log(1) || true); +const _forEachArray = [{ effect() {} }]; +_forEachArray.forEach(element => (element.effect = () => console.log(1))); +_forEachArray[0].effect(); +[1].map(() => console.log(1) || 1); +const _mapArray = [{ effect() {} }]; +_mapArray.map(element => (element.effect = () => console.log(1))); +_mapArray[0].effect(); +[1].reduce(() => console.log(1) || 1, 1); +const _reduceArray = [{ effect() {} }]; +_reduceArray.reduce((_, element) => (element.effect = () => console.log(1)), 1); +_reduceArray[0].effect(); +[1].reduceRight(() => console.log(1) || 1, 1); +const _reduceRightArray = [{ effect() {} }]; +_reduceRightArray.reduceRight((_, element) => (element.effect = () => console.log(1)), 1); +_reduceRightArray[0].effect(); +[1].some(() => console.log(1) || true); +const _someArray = [{ effect() {} }]; +_someArray.some(element => (element.effect = () => console.log(1))); +_someArray[0].effect(); // mutator methods -const exported = [ 1 ]; -exported.copyWithin( 0 ); -exported.fill( 0 ); +const exported = [1]; +exported.copyWithin(0); +exported.fill(0); exported.pop(); -exported.push( 0 ); +exported.push(0); exported.reverse(); exported.shift(); -[ 1 ].sort( () => console.log( 1 ) || 0 ); +[1].sort(() => console.log(1) || 0); exported.sort(); -exported.splice( 0 ); -exported.unshift( 0 ); +exported.splice(0); +exported.unshift(0); export { exported }; diff --git a/test/form/samples/builtin-prototypes/array-expression/main.js b/test/form/samples/builtin-prototypes/array-expression/main.js index a04bdfa57e3..0f7b94cf367 100644 --- a/test/form/samples/builtin-prototypes/array-expression/main.js +++ b/test/form/samples/builtin-prototypes/array-expression/main.js @@ -1,73 +1,140 @@ const array = []; -const join1 = array.join( ',' ); -const join2 = [].join( ',' ); -const join3 = [].join( ',' ).trim(); +const join1 = array.join(','); +const join2 = [].join(','); +const join3 = [].join(',').trim(); const length = [].length; -const map1 = [ 1 ].map( x => x ); -const map2 = [ 1 ].map( x => console.log( 1 ) ); -const map3 = [ 1 ].map( x => x ).map( x => x ); -const map4 = [ 1 ].map( x => x ).map( x => console.log( 1 ) ); -const map5 = [ 1 ].map( x => console.log( 1 ) ).map( x => x ); -const map6 = [ 1 ].map( x => x ).map( x => x ).map( x => x ); -const map7 = [ 1 ].map( x => x ).map( x => x ).map( x => console.log( 1 ) ); -const map8 = [ 1 ].map( x => x ).map( x => console.log( 1 ) ).map( x => x ); +const map1 = [1].map(x => x); +const map2 = [1].map(x => console.log(1)); +const map3 = [1].map(x => x).map(x => x); +const map4 = [1].map(x => x).map(x => console.log(1)); +const map5 = [1].map(x => console.log(1)).map(x => x); +const map6 = [1] + .map(x => x) + .map(x => x) + .map(x => x); +const map7 = [1] + .map(x => x) + .map(x => x) + .map(x => console.log(1)); +const map8 = [1] + .map(x => x) + .map(x => console.log(1)) + .map(x => x); [](); // accessor methods -const _includes = [].includes( 1 ).valueOf(); -const _indexOf = [].indexOf( 1 ).toPrecision( 1 ); -const _join = [].join( ',' ).trim(); -const _lastIndexOf = [].lastIndexOf( 1 ).toPrecision( 1 ); -const _slice = [].slice( 1 ).concat( [] ); +const removedTestArray = [{ noEffect() {} }]; +removedTestArray[0].noEffect(); + +const _at = [].at(1); +const _atArray = [{ effect() {} }]; +_atArray.at(0).effect = () => console.log(1); +_atArray[0].effect(); + +const _entries = [].entries(); +const _entriesArray = [{ effect() {} }]; +[..._entriesArray.entries()][0][1].effect = () => console.log(1); +_entriesArray[0].effect(); + +const _includes = [].includes(1).valueOf(); +const _indexOf = [].indexOf(1).toPrecision(1); +const _join = [].join(',').trim(); +const _keys = [].keys(); +const _lastIndexOf = [].lastIndexOf(1).toPrecision(1); + +const _slice = [].slice(1).concat([]); +const _sliceArray = [{ effect() {} }]; +_sliceArray.slice()[0].effect = () => console.log(1); +_sliceArray[0].effect(); + +const _values = [].values(); +const _valuesArray = [{ effect() {} }]; +[..._valuesArray.values()][0].effect = () => console.log(1); +_valuesArray[0].effect(); // iteration methods -const _every = [ 1 ].every( () => true ).valueOf(); -const _everyEffect = [ 1 ].every( () => console.log( 1 ) || true ); -const _filter = [ 1 ].filter( () => true ).join( ',' ); -const _filterEffect = [ 1 ].filter( () => console.log( 1 ) || true ); -const _find = [ 1 ].find( () => true ); -const _findEffect = [ 1 ].find( () => console.log( 1 ) || true ); -const _findIndex = [ 1 ].findIndex( () => true ).toPrecision( 1 ); -const _findIndexEffect = [ 1 ].findIndex( () => console.log( 1 ) || true ); -const _forEach = [ 1 ].forEach( () => {} ); -const _forEachEffect = [ 1 ].forEach( () => console.log( 1 ) || true ); -const _map = [ 1 ].map( () => 1 ).join( ',' ); -const _mapEffect = [ 1 ].map( () => console.log( 1 ) || 1 ); -const _reduce = [ 1 ].reduce( () => 1, 1 ); -const _reduceEffect = [ 1 ].reduce( () => console.log( 1 ) || 1, 1 ); -const _reduceRight = [ 1 ].reduceRight( () => 1, 1 ); -const _reduceRightEffect = [ 1 ].reduceRight( () => console.log( 1 ) || 1, 1 ); -const _some = [ 1 ].some( () => true ).valueOf(); -const _someEffect = [ 1 ].some( () => console.log( 1 ) || true ); +const _every = [1].every(() => true).valueOf(); +const _everyEffect = [1].every(() => console.log(1) || true); +const _everyArray = [{ effect() {} }]; +_everyArray.every(element => (element.effect = () => console.log(1))); +_everyArray[0].effect(); + +const _filter = [1].filter(() => true).join(','); +const _filterEffect = [1].filter(() => console.log(1) || true); +const _filterArray = [{ effect() {} }]; +_filterArray.filter(element => (element.effect = () => console.log(1))); +_filterArray[0].effect(); + +const _find = [1].find(() => true); +const _findEffect = [1].find(() => console.log(1) || true); +const _findArray = [{ effect() {} }]; +_findArray.find(element => (element.effect = () => console.log(1))); +_findArray[0].effect(); + +const _findIndex = [1].findIndex(() => true).toPrecision(1); +const _findIndexEffect = [1].findIndex(() => console.log(1) || true); +const _findIndexArray = [{ effect() {} }]; +_findIndexArray.findIndex(element => (element.effect = () => console.log(1))); +_findIndexArray[0].effect(); + +const _forEach = [1].forEach(() => {}); +const _forEachEffect = [1].forEach(() => console.log(1) || true); +const _forEachArray = [{ effect() {} }]; +_forEachArray.forEach(element => (element.effect = () => console.log(1))); +_forEachArray[0].effect(); + +const _map = [1].map(() => 1).join(','); +const _mapEffect = [1].map(() => console.log(1) || 1); +const _mapArray = [{ effect() {} }]; +_mapArray.map(element => (element.effect = () => console.log(1))); +_mapArray[0].effect(); + +const _reduce = [1].reduce(() => 1, 1); +const _reduceEffect = [1].reduce(() => console.log(1) || 1, 1); +const _reduceArray = [{ effect() {} }]; +_reduceArray.reduce((_, element) => (element.effect = () => console.log(1)), 1); +_reduceArray[0].effect(); + +const _reduceRight = [1].reduceRight(() => 1, 1); +const _reduceRightEffect = [1].reduceRight(() => console.log(1) || 1, 1); +const _reduceRightArray = [{ effect() {} }]; +_reduceRightArray.reduceRight((_, element) => (element.effect = () => console.log(1)), 1); +_reduceRightArray[0].effect(); + +const _some = [1].some(() => true).valueOf(); +const _someEffect = [1].some(() => console.log(1) || true); +const _someArray = [{ effect() {} }]; +_someArray.some(element => (element.effect = () => console.log(1))); +_someArray[0].effect(); // mutator methods -export const exported = [ 1 ]; +export const exported = [1]; -const _copyWithin = [ 1 ].copyWithin( 0 ).join( ',' ); -exported.copyWithin( 0 ); -const _fill = [ 1 ].fill( 0 ).join( ',' ); -exported.fill( 0 ); -const _pop = [ 1 ].pop(); +const _copyWithin = [1].copyWithin(0).join(','); +exported.copyWithin(0); +const _fill = [1].fill(0).join(','); +exported.fill(0); +const _pop = [1].pop(); exported.pop(); -const _push = [ 1 ].push( 0 ).toPrecision( 1 ); -exported.push( 0 ); -const _reverse = [ 1 ].reverse().join( ',' ); +const _push = [1].push(0).toPrecision(1); +exported.push(0); +const _reverse = [1].reverse().join(','); exported.reverse(); -const _shift = [ 1 ].shift(); +const _shift = [1].shift(); exported.shift(); -const _sort = [ 1 ].sort( () => 0 ).join( ',' ); -const _sortEffect = [ 1 ].sort( () => console.log( 1 ) || 0 ); +const _sort = [1].sort(() => 0).join(','); +const _sortEffect = [1].sort(() => console.log(1) || 0); exported.sort(); -const _splice = [ 1 ].splice( 0 ).join( ',' ); -exported.splice( 0 ); -const _unshift = [ 1 ].unshift( 0 ).toPrecision( 1 ); -exported.unshift( 0 ); +const _splice = [1].splice(0).join(','); +exported.splice(0); +const _unshift = [1].unshift(0).toPrecision(1); +exported.unshift(0); // inherited -const _hasOwnProperty = [ 1 ].hasOwnProperty( 'toString' ).valueOf(); -const _isPrototypeOf = [ 1 ].isPrototypeOf( [] ).valueOf(); -const _propertyIsEnumerable = [ 1 ].propertyIsEnumerable( 'toString' ).valueOf(); -const _toLocaleString = [ 1 ].toLocaleString().trim(); -const _toString = [ 1 ].toString().trim(); -const _valueOf = [ 1 ].valueOf(); +const _hasOwnProperty = [1].hasOwnProperty('toString').valueOf(); +const _isPrototypeOf = [1].isPrototypeOf([]).valueOf(); +const _propertyIsEnumerable = [1].propertyIsEnumerable('toString').valueOf(); +const _toLocaleString = [1].toLocaleString().trim(); +const _toString = [1].toString().trim(); +const _valueOf = [1].valueOf(); diff --git a/test/form/samples/builtin-prototypes/literal/_expected.js b/test/form/samples/builtin-prototypes/literal/_expected.js index a2c2161b645..75d80d652e5 100644 --- a/test/form/samples/builtin-prototypes/literal/_expected.js +++ b/test/form/samples/builtin-prototypes/literal/_expected.js @@ -1,3 +1,13 @@ +// retained +true.valueOf().unknown.unknown(); +true.valueOf()(); +(1).valueOf().unknown.unknown(); +(1).valueOf().unknown(); +(1).valueOf()[globalThis.unknown](); +(1).valueOf()(); +'ab'.charAt(1).unknown.unknown(); +'ab'.charAt(1)(); +null.unknown; 'ab'.replace( 'a', () => console.log( 1 ) || 'b' ); // deep property access is forbidden diff --git a/test/form/samples/builtin-prototypes/literal/main.js b/test/form/samples/builtin-prototypes/literal/main.js index edba0b1ee4f..e91c6a56144 100644 --- a/test/form/samples/builtin-prototypes/literal/main.js +++ b/test/form/samples/builtin-prototypes/literal/main.js @@ -4,25 +4,16 @@ const valueOf2 = true.valueOf(); const valueOf3 = true.valueOf().valueOf(); const valueOf4 = true.valueOf().valueOf().valueOf(); -const number = 1; -const toExponential1 = number.toExponential( 2 ); -const toExponential2 = (1).toExponential( 2 ); -const toExponential3 = (1).toExponential( 2 ).trim(); - -const string = ' b '; -const trim1 = string.trim(); -const trim2 = ' x '.trim(); -const trim3 = ' x '.trim().trim(); -const trim4 = ' x '.trim().trim().trim(); - -// boolean prototype -const _booleanValueOf = true.valueOf().valueOf(); -// inherited -const _booleanHasOwnProperty = true.hasOwnProperty( 'toString' ).valueOf(); -const _booleanIsPrototypeOf = true.isPrototypeOf( true ).valueOf(); -const _booleanPropertyIsEnumerable = true.propertyIsEnumerable( 'toString' ).valueOf(); -const _booleanToLocaleString = true.toLocaleString().trim(); -const _booleanToString = true.toString().trim(); +// retained +true.valueOf().unknown.unknown(); +true.valueOf()(); +(1).valueOf().unknown.unknown(); +(1).valueOf().unknown(); +(1).valueOf()[globalThis.unknown](); +(1).valueOf()(); +'ab'.charAt(1).unknown.unknown(); +'ab'.charAt(1)(); +null.unknown; // number prototype const _toExponential = (1).toExponential( 2 ).trim(); @@ -56,7 +47,7 @@ const _replace = 'ab'.replace( 'a', () => 'b' ).trim(); const _replaceEffect = 'ab'.replace( 'a', () => console.log( 1 ) || 'b' ); const _search = 'ab'.search( /a/ ).toExponential( 2 ); const _slice = 'ab'.slice( 0, 1 ).trim(); -const _split = 'ab'.split( 'a' ).join(); +const _split = 'ab'.split( 'a' ); const _startsWith = 'ab'.startsWith( 'a' ).valueOf(); const _substr = 'ab'.substr( 0, 1 ).trim(); const _substring = 'ab'.substring( 0, 1 ).trim(); diff --git a/test/form/samples/builtin-prototypes/object-expression/_expected.js b/test/form/samples/builtin-prototypes/object-expression/_expected.js index 95b55b099ae..e69de29bb2d 100644 --- a/test/form/samples/builtin-prototypes/object-expression/_expected.js +++ b/test/form/samples/builtin-prototypes/object-expression/_expected.js @@ -1,11 +0,0 @@ -const object = {}; -object.propertyIsEnumerable( 'toString' ); -({}).propertyIsEnumerable( 'toString' ); -({}).propertyIsEnumerable( 'toString' ).valueOf(); - -({}).hasOwnProperty( 'toString' ).valueOf(); -({}).isPrototypeOf( {} ).valueOf(); -({}).propertyIsEnumerable( 'toString' ).valueOf(); -({}).toLocaleString().trim(); -({}).toString().trim(); -({}).valueOf(); diff --git a/test/form/samples/class-method-access/_config.js b/test/form/samples/class-method-access/_config.js new file mode 100644 index 00000000000..d44dede48ae --- /dev/null +++ b/test/form/samples/class-method-access/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks side-effects when accessing class methods' +}; diff --git a/test/form/samples/class-method-access/_expected.js b/test/form/samples/class-method-access/_expected.js new file mode 100644 index 00000000000..034e5c35fe7 --- /dev/null +++ b/test/form/samples/class-method-access/_expected.js @@ -0,0 +1,16 @@ +console.log('retained'); + +class Used { + static method() {} + static get getter() { + return { foo: {} }; + } +} +Used.method.doesNotExist.throws; +Used.getter.doesNotExist.throws; +Used.getter.foo.doesNotExist.throws; +Used.getter.throws(); +Used.getter.foo.throws(); + +Used.method.reassigned = 1; +Used.getter.reassigned = 2; diff --git a/test/form/samples/class-method-access/main.js b/test/form/samples/class-method-access/main.js new file mode 100644 index 00000000000..25e980dc120 --- /dev/null +++ b/test/form/samples/class-method-access/main.js @@ -0,0 +1,34 @@ +class Removed { + static get isTrue() { + return true; + } +} + +if (Removed.isTrue) console.log('retained'); +else console.log('removed'); + +class Used { + static method() {} + static get getter() { + return { foo: {} }; + } +} + +Used.method.doesNotExist; +Used.method.doesNotExist.throws; +Used.getter.doesNotExist; +Used.getter.doesNotExist.throws; +Used.getter.foo; +Used.getter.foo.doesNotExist; +Used.getter.foo.doesNotExist.throws; +Used.getter.hasOwnProperty('foo'); +Used.getter.foo.hasOwnProperty('bar'); +Used.getter.throws(); +Used.getter.foo.throws(); + +Used.method.reassigned = 1; +Used.getter.reassigned = 2; + +class ValueEffect { + static foo +} \ No newline at end of file diff --git a/test/form/samples/conditional-expression-paths/_expected.js b/test/form/samples/conditional-expression-paths/_expected.js index f78090cebcb..ae60637a8b9 100644 --- a/test/form/samples/conditional-expression-paths/_expected.js +++ b/test/form/samples/conditional-expression-paths/_expected.js @@ -1,22 +1,11 @@ var unknownValue = globalThis.unknown(); var foo = { x: () => {}, y: {} }; -var bar = { x: () => {}, y: {} }; var baz = { x: () => console.log('effect') }; -// unknown branch without side-effects -(unknownValue ? foo : bar).y.z; -(unknownValue ? foo : bar).x(); - // unknown branch with side-effect (unknownValue ? foo : baz).y.z; (unknownValue ? foo : baz).x(); -// known branch without side-effects -(foo ).y.z; -(foo).y.z; -(foo ).x(); -(foo).x(); - // known branch with side-effect (baz ).y.z; (baz).y.z; diff --git a/test/form/samples/conditional-expression-paths/main.js b/test/form/samples/conditional-expression-paths/main.js index 67d2162a495..bfafbf296c1 100644 --- a/test/form/samples/conditional-expression-paths/main.js +++ b/test/form/samples/conditional-expression-paths/main.js @@ -19,8 +19,8 @@ var d3 = (false ? baz : foo).x(); var foo2 = { y: {} }; var baz2 = {}; -(true ? foo2 : baz).y.z = 1; -(false ? baz : foo2).y.z = 1; +(true ? foo2 : baz2).y.z = 1; +(false ? baz2 : foo2).y.z = 1; // known branch with side-effect var a4 = (true ? baz : foo).y.z; diff --git a/test/form/samples/function-body-return-values/_expected.js b/test/form/samples/function-body-return-values/_expected.js index cd4f77d4ce6..0064a3f4c4a 100644 --- a/test/form/samples/function-body-return-values/_expected.js +++ b/test/form/samples/function-body-return-values/_expected.js @@ -1,9 +1,3 @@ -function removed3 () { - return { x: () => {} }; -} - -removed3().x(); - function retained1 () { return () => console.log( 'effect' ); } diff --git a/test/form/samples/literals-from-class-statics/_config.js b/test/form/samples/literals-from-class-statics/_config.js new file mode 100644 index 00000000000..0847b183aac --- /dev/null +++ b/test/form/samples/literals-from-class-statics/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks literal values in class static fields' +}; diff --git a/test/form/samples/literals-from-class-statics/_expected.js b/test/form/samples/literals-from-class-statics/_expected.js new file mode 100644 index 00000000000..09ce1e861ac --- /dev/null +++ b/test/form/samples/literals-from-class-statics/_expected.js @@ -0,0 +1,13 @@ +log("t"); +log("x"); + +class Undef { + static 'y'; +} +if (Undef.y) log("y"); + +class Deopt { + static z = false; +} +unknown(Deopt); +if (Deopt.z) log("z"); diff --git a/test/form/samples/literals-from-class-statics/main.js b/test/form/samples/literals-from-class-statics/main.js new file mode 100644 index 00000000000..a8cdcb481d4 --- /dev/null +++ b/test/form/samples/literals-from-class-statics/main.js @@ -0,0 +1,21 @@ +class Static { + static t() { return true; } + static f() { return false; } + static x = 10; +} + +if (Static.t()) log("t"); +if (Static.f()) log("f"); +if (!Static.t()) log("!t"); +if (Static.x) log("x"); + +class Undef { + static 'y'; +} +if (Undef.y) log("y"); + +class Deopt { + static z = false; +} +unknown(Deopt); +if (Deopt.z) log("z"); diff --git a/test/form/samples/minimal-this-mutation/_config.js b/test/form/samples/minimal-this-mutation/_config.js new file mode 100644 index 00000000000..d0cecf5de3d --- /dev/null +++ b/test/form/samples/minimal-this-mutation/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'mutates "this" only if necessary on function calls' +}; diff --git a/test/form/samples/minimal-this-mutation/_expected.js b/test/form/samples/minimal-this-mutation/_expected.js new file mode 100644 index 00000000000..bf850340e03 --- /dev/null +++ b/test/form/samples/minimal-this-mutation/_expected.js @@ -0,0 +1,48 @@ +console.log('retained'); + +const obj2 = { + noMutationEffect() { + console.log('effect'); + }, + prop: true +}; +obj2.noMutationEffect(); +console.log('retained'); + +const obj3 = { + mutateProp() { + this.prop = false; + }, + prop: true +}; +obj3.mutateProp(); +if (obj3.prop) console.log('unimportant'); +else console.log('retained'); + +const obj4 = { + mutateUnknownProp() { + this[globalThis.unknown] = false; + }, + prop: true +}; +obj4.mutateUnknownProp(); +if (obj4.prop) console.log('retained'); +else console.log('retained'); + +const obj5 = { + mutateNestedProp() { + this.nested.prop = false; + }, + nested: { + prop: true + } +}; +obj5.mutateNestedProp(); +if (obj5.nested.prop) console.log('unimportant'); +else console.log('retained'); + +const obj6 = { + prop: true +}; +obj6.doesNotExist(); +console.log('retained'); diff --git a/test/form/samples/minimal-this-mutation/main.js b/test/form/samples/minimal-this-mutation/main.js new file mode 100644 index 00000000000..a70c3d521e5 --- /dev/null +++ b/test/form/samples/minimal-this-mutation/main.js @@ -0,0 +1,56 @@ +const obj1 = { + noMutation() {}, + prop: true +}; +obj1.noMutation(); +if (obj1.prop) console.log('retained'); +else console.log('removed'); + +const obj2 = { + noMutationEffect() { + console.log('effect'); + }, + prop: true +}; +obj2.noMutationEffect(); +if (obj2.prop) console.log('retained'); +else console.log('removed'); + +const obj3 = { + mutateProp() { + this.prop = false; + }, + prop: true +}; +obj3.mutateProp(); +if (obj3.prop) console.log('unimportant'); +else console.log('retained'); + +const obj4 = { + mutateUnknownProp() { + this[globalThis.unknown] = false; + }, + prop: true +}; +obj4.mutateUnknownProp(); +if (obj4.prop) console.log('retained'); +else console.log('retained'); + +const obj5 = { + mutateNestedProp() { + this.nested.prop = false; + }, + nested: { + prop: true + } +}; +obj5.mutateNestedProp(); +if (obj5.nested.prop) console.log('unimportant'); +else console.log('retained'); + +const obj6 = { + prop: true +}; +obj6.doesNotExist(); +if (obj6.prop) console.log('retained'); +else console.log('removed'); diff --git a/test/form/samples/modify-class-prototype/_expected.js b/test/form/samples/modify-class-prototype/_expected.js index 61e4f3c329f..8dd53679723 100644 --- a/test/form/samples/modify-class-prototype/_expected.js +++ b/test/form/samples/modify-class-prototype/_expected.js @@ -3,4 +3,12 @@ Retained.prop = 42; Retained.prototype.prop = 43; Retained.prototype.prop2 = Retained.prototype.prop; +class RetainedSuper { + set prop(v) { + console.log('effect', v); + } +} +class RetainedWithSuper extends RetainedSuper {} +RetainedWithSuper.prototype.prop = 42; + export { Retained }; diff --git a/test/form/samples/modify-class-prototype/main.js b/test/form/samples/modify-class-prototype/main.js index 38f8fa9abf0..1bf13d724dc 100644 --- a/test/form/samples/modify-class-prototype/main.js +++ b/test/form/samples/modify-class-prototype/main.js @@ -7,3 +7,18 @@ export class Retained {} Retained.prop = 42; Retained.prototype.prop = 43; Retained.prototype.prop2 = Retained.prototype.prop; + +class RemovedSuper { + prop() {} +} +class RemovedWithSuper extends RemovedSuper {} +RemovedWithSuper.prototype.prop = 42; +RemovedWithSuper.prototype.prop2 = 43; + +class RetainedSuper { + set prop(v) { + console.log('effect', v); + } +} +class RetainedWithSuper extends RetainedSuper {} +RetainedWithSuper.prototype.prop = 42; diff --git a/test/form/samples/namespace-missing-export-effects/_config.js b/test/form/samples/namespace-missing-export-effects/_config.js new file mode 100644 index 00000000000..87a35ab8485 --- /dev/null +++ b/test/form/samples/namespace-missing-export-effects/_config.js @@ -0,0 +1,4 @@ +module.exports = { + description: 'handles interacting with missing namespace members', + expectedWarnings: ['MISSING_EXPORT'] +}; diff --git a/test/form/samples/namespace-missing-export-effects/_expected.js b/test/form/samples/namespace-missing-export-effects/_expected.js new file mode 100644 index 00000000000..8895a217864 --- /dev/null +++ b/test/form/samples/namespace-missing-export-effects/_expected.js @@ -0,0 +1,5 @@ +if (!undefined) console.log(1); +if (undefined()) console.log(2); +const foo = undefined; +foo.bar; +(0, undefined)(); diff --git a/test/form/samples/namespace-missing-export-effects/main.js b/test/form/samples/namespace-missing-export-effects/main.js new file mode 100644 index 00000000000..e37bad3f8ac --- /dev/null +++ b/test/form/samples/namespace-missing-export-effects/main.js @@ -0,0 +1,7 @@ +import * as ns from './other.js'; + +if (!ns.foo) console.log(1); +if (ns.foo()) console.log(2); +const foo = ns.foo; +foo.bar; +(true && ns.foo)(); diff --git a/test/form/samples/namespace-missing-export-effects/other.js b/test/form/samples/namespace-missing-export-effects/other.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/form/samples/namespace-optimization/_expected/es.js b/test/form/samples/namespace-optimization/_expected.js similarity index 100% rename from test/form/samples/namespace-optimization/_expected/es.js rename to test/form/samples/namespace-optimization/_expected.js diff --git a/test/form/samples/namespace-optimization/_expected/amd.js b/test/form/samples/namespace-optimization/_expected/amd.js deleted file mode 100644 index 95231f1878b..00000000000 --- a/test/form/samples/namespace-optimization/_expected/amd.js +++ /dev/null @@ -1,7 +0,0 @@ -define(function () { 'use strict'; - - function a () {} - - console.log( a() ); - -}); diff --git a/test/form/samples/namespace-optimization/_expected/cjs.js b/test/form/samples/namespace-optimization/_expected/cjs.js deleted file mode 100644 index 33a83e92c84..00000000000 --- a/test/form/samples/namespace-optimization/_expected/cjs.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -function a () {} - -console.log( a() ); diff --git a/test/form/samples/namespace-optimization/_expected/iife.js b/test/form/samples/namespace-optimization/_expected/iife.js deleted file mode 100644 index 64673c93d43..00000000000 --- a/test/form/samples/namespace-optimization/_expected/iife.js +++ /dev/null @@ -1,8 +0,0 @@ -(function () { - 'use strict'; - - function a () {} - - console.log( a() ); - -}()); diff --git a/test/form/samples/namespace-optimization/_expected/system.js b/test/form/samples/namespace-optimization/_expected/system.js deleted file mode 100644 index dacc31b2984..00000000000 --- a/test/form/samples/namespace-optimization/_expected/system.js +++ /dev/null @@ -1,12 +0,0 @@ -System.register([], function () { - 'use strict'; - return { - execute: function () { - - function a () {} - - console.log( a() ); - - } - }; -}); diff --git a/test/form/samples/namespace-optimization/_expected/umd.js b/test/form/samples/namespace-optimization/_expected/umd.js deleted file mode 100644 index 2163f63c851..00000000000 --- a/test/form/samples/namespace-optimization/_expected/umd.js +++ /dev/null @@ -1,10 +0,0 @@ -(function (factory) { - typeof define === 'function' && define.amd ? define(factory) : - factory(); -}((function () { 'use strict'; - - function a () {} - - console.log( a() ); - -}))); diff --git a/test/form/samples/namespace-optimization/bar.js b/test/form/samples/namespace-optimization/bar.js index aa96676744d..ad92a1a3d6b 100644 --- a/test/form/samples/namespace-optimization/bar.js +++ b/test/form/samples/namespace-optimization/bar.js @@ -1,3 +1,3 @@ import * as quux from './quux'; -export { quux }; +export { quux }; diff --git a/test/form/samples/namespace-optimization/foo.js b/test/form/samples/namespace-optimization/foo.js index 42a93ae6fe9..d7132d920d3 100644 --- a/test/form/samples/namespace-optimization/foo.js +++ b/test/form/samples/namespace-optimization/foo.js @@ -1,3 +1,3 @@ import * as bar from './bar'; -export { bar }; +export { bar }; diff --git a/test/form/samples/nested-deoptimization/_config.js b/test/form/samples/nested-deoptimization/_config.js new file mode 100644 index 00000000000..440f76d3543 --- /dev/null +++ b/test/form/samples/nested-deoptimization/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles deoptimization of nested properties' +}; diff --git a/test/form/samples/nested-deoptimization/_expected.js b/test/form/samples/nested-deoptimization/_expected.js new file mode 100644 index 00000000000..04bb1a1620c --- /dev/null +++ b/test/form/samples/nested-deoptimization/_expected.js @@ -0,0 +1,11 @@ +const obj = { + foo: { prop: true }, + bar: { otherProp: true }, + prop: true +}; +obj[globalThis.unknown].prop = false; + +if (obj.foo.prop) console.log('retained'); +else console.log('also retained'); +console.log('retained'); +console.log('retained'); diff --git a/test/form/samples/nested-deoptimization/main.js b/test/form/samples/nested-deoptimization/main.js new file mode 100644 index 00000000000..df9ff8d41f7 --- /dev/null +++ b/test/form/samples/nested-deoptimization/main.js @@ -0,0 +1,13 @@ +const obj = { + foo: { prop: true }, + bar: { otherProp: true }, + prop: true +}; +obj[globalThis.unknown].prop = false; + +if (obj.foo.prop) console.log('retained'); +else console.log('also retained'); +if (obj.bar.otherProp) console.log('retained'); +else console.log('removed'); +if (obj.prop) console.log('retained'); +else console.log('removed'); diff --git a/test/form/samples/nested-member-access/main.js b/test/form/samples/nested-member-access/main.js index 323ca9e7885..6a46a73687e 100644 --- a/test/form/samples/nested-member-access/main.js +++ b/test/form/samples/nested-member-access/main.js @@ -4,16 +4,9 @@ const removedResult1 = removed1.foo; const removed2 = { foo: {} }; const removedResult2 = removed2.foo.bar; -const removed3 = new function () {}(); -const removedResult3 = removed3.foo; - const removed4 = !{}; const removedResult4 = removed4.foo; -let removed5a; -const removed5b = removed5a = {}; -const removedResult5 = removed5b.foo; - const removed6 = 1 + 2; const removedResult6 = removed6.foo; diff --git a/test/form/samples/object-expression/return-expressions/_expected.js b/test/form/samples/object-expression/return-expressions/_expected.js index 0405706b107..3538f6d9b82 100644 --- a/test/form/samples/object-expression/return-expressions/_expected.js +++ b/test/form/samples/object-expression/return-expressions/_expected.js @@ -12,8 +12,6 @@ const z = { z.a()(); const v = {}; - -v.toString().charCodeAt(0); // removed v.toString().doesNotExist(0); // retained const w = { diff --git a/test/form/samples/property-setters-and-getters/shadowed-setters/_config.js b/test/form/samples/property-setters-and-getters/shadowed-setters/_config.js new file mode 100644 index 00000000000..f220613b295 --- /dev/null +++ b/test/form/samples/property-setters-and-getters/shadowed-setters/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles setters shadowed by computed setters' +}; diff --git a/test/form/samples/property-setters-and-getters/shadowed-setters/_expected.js b/test/form/samples/property-setters-and-getters/shadowed-setters/_expected.js new file mode 100644 index 00000000000..2758779af18 --- /dev/null +++ b/test/form/samples/property-setters-and-getters/shadowed-setters/_expected.js @@ -0,0 +1,8 @@ +const objRetained = { + set value(v) {}, + set [globalThis.unknown](v) { + console.log('effect'); + } +}; + +objRetained.value = 'retained'; diff --git a/test/form/samples/property-setters-and-getters/shadowed-setters/main.js b/test/form/samples/property-setters-and-getters/shadowed-setters/main.js new file mode 100644 index 00000000000..e5b1a9baf01 --- /dev/null +++ b/test/form/samples/property-setters-and-getters/shadowed-setters/main.js @@ -0,0 +1,17 @@ +const objRemoved = { + set value(v) { + console.log('shadowed'); + }, + set value(v) {} +}; + +objRemoved.value = 'removed'; + +const objRetained = { + set value(v) {}, + set [globalThis.unknown](v) { + console.log('effect'); + } +}; + +objRetained.value = 'retained'; diff --git a/test/form/samples/recursive-calls/_expected.js b/test/form/samples/recursive-calls/_expected.js index 00f0e5d1be2..b5e78683edd 100644 --- a/test/form/samples/recursive-calls/_expected.js +++ b/test/form/samples/recursive-calls/_expected.js @@ -1,13 +1,3 @@ -const removed4 = () => globalThis.unknown ? removed4() : { x: () => {} }; -removed4().x(); - -const removed8 = { - get x () { - return globalThis.unknown ? removed8.x : { y: () => {} }; - } -}; -removed8.x.y(); - const retained1 = () => globalThis.unknown ? retained1() : console.log( 'effect' ); retained1(); diff --git a/test/form/samples/recursive-calls/main.js b/test/form/samples/recursive-calls/main.js index 89bae2a7738..306972ec96b 100644 --- a/test/form/samples/recursive-calls/main.js +++ b/test/form/samples/recursive-calls/main.js @@ -17,13 +17,6 @@ const removed5 = { }; removed5.x; -const removed6 = { - get x () { - return globalThis.unknown ? removed6.x : () => {}; - } -}; -removed6.x(); - const removed7 = { get x () { return globalThis.unknown ? removed7.x : {}; diff --git a/test/form/samples/side-effects-class-getters-setters/_config.js b/test/form/samples/side-effects-class-getters-setters/_config.js new file mode 100644 index 00000000000..201e3e082f9 --- /dev/null +++ b/test/form/samples/side-effects-class-getters-setters/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'treat getters and setters on classes as function calls' +}; diff --git a/test/form/samples/side-effects-class-getters-setters/_expected.js b/test/form/samples/side-effects-class-getters-setters/_expected.js new file mode 100644 index 00000000000..c09724d4c6a --- /dev/null +++ b/test/form/samples/side-effects-class-getters-setters/_expected.js @@ -0,0 +1,38 @@ +class RetainedByGetter { + get a() { log(); } +} +RetainedByGetter.prototype.a; + +class RetainedBySetter { + set a(v) { log(); } +} +RetainedBySetter.prototype.a = 10; + +class RetainedByStaticGetter { + static get a() { log(); } +} +RetainedByStaticGetter.a; + +class RetainedByStaticSetter { + static set a(v) { log(); } +} +RetainedByStaticSetter.a = 10; + +class RetainedSuper { + static get a() { log(); } +} +class RetainedSub extends RetainedSuper {} +RetainedSub.a; +log(); + +class DeoptProto { + a = true; +} +globalThis.unknown(DeoptProto.prototype); +if (!DeoptProto.prototype.a) log(); + +class DeoptComputed { + static get a() {} + static get [globalThis.unknown]() { log(); } +} +DeoptComputed.a; diff --git a/test/form/samples/side-effects-class-getters-setters/main.js b/test/form/samples/side-effects-class-getters-setters/main.js new file mode 100644 index 00000000000..9a8c95b93cb --- /dev/null +++ b/test/form/samples/side-effects-class-getters-setters/main.js @@ -0,0 +1,78 @@ +class Removed { + get a() { log(); } + set a(v) { log(); } + static get a() { log(); } + static set a(v) { log(); } +} + +class RemovedNoEffect { + get a() {} + set a(v) {} + static get a() {} + static set a(v) {} +} +RemovedNoEffect.prototype.a; +RemovedNoEffect.prototype.a = 1; +RemovedNoEffect.a; +RemovedNoEffect.a = 1; + +class RetainedByGetter { + get a() { log(); } +} +RetainedByGetter.prototype.a; + +class RetainedBySetter { + set a(v) { log(); } +} +RetainedBySetter.prototype.a = 10; + +class RetainedByStaticGetter { + static get a() { log(); } +} +RetainedByStaticGetter.a; + +class RetainedByStaticSetter { + static set a(v) { log(); } +} +RetainedByStaticSetter.a = 10; + +class RemovedSetters { + set a(v) { log(); } + static set a(v) { log(); } +} +RemovedSetters.prototype.a; +RemovedSetters.a; + +class RemovedWrongProp { + get a() { log(); } + static get a() { log(); } +} +RemovedWrongProp.prototype.b +RemovedWrongProp.b + +class RetainedSuper { + static get a() { log(); } +} +class RetainedSub extends RetainedSuper {} +RetainedSub.a; + +class RemovedSub extends RetainedSuper {} +RemovedSub.b; + +// class fields are not part of the prototype +class RemovedProtoValue { + a = true; +} +if (!RemovedProtoValue.prototype.a) log(); + +class DeoptProto { + a = true; +} +globalThis.unknown(DeoptProto.prototype); +if (!DeoptProto.prototype.a) log(); + +class DeoptComputed { + static get a() {} + static get [globalThis.unknown]() { log(); } +} +DeoptComputed.a; diff --git a/test/form/samples/side-effects-getters-and-setters/_expected.js b/test/form/samples/side-effects-getters-and-setters/_expected.js index 4100b6ca31b..31917077c8e 100644 --- a/test/form/samples/side-effects-getters-and-setters/_expected.js +++ b/test/form/samples/side-effects-getters-and-setters/_expected.js @@ -1,34 +1,36 @@ -const retained1a = { - get effect () { - console.log( 'effect' ); +const retained1 = { + get effect() { + console.log('effect'); }, - get noEffect () { + get noEffect() { const x = 1; return x; } }; -retained1a.effect; -retained1a[ 'eff' + 'ect' ]; + +//retained +retained1.effect; +retained1['eff' + 'ect']; const retained3 = { - set effect ( value ) { - console.log( value ); + set effect(value) { + console.log(value); } }; retained3.effect = 'retained'; const retained4 = { - set effect ( value ) { - console.log( value ); + set effect(value) { + console.log(value); } }; -retained4[ 'eff' + 'ect' ] = 'retained'; +retained4['eff' + 'ect'] = 'retained'; const retained7 = { foo: () => {}, - get foo () { + get foo() { return 1; } }; diff --git a/test/form/samples/side-effects-getters-and-setters/main.js b/test/form/samples/side-effects-getters-and-setters/main.js index b3ce3d4a9b6..66cb31f8451 100644 --- a/test/form/samples/side-effects-getters-and-setters/main.js +++ b/test/form/samples/side-effects-getters-and-setters/main.js @@ -1,68 +1,60 @@ -const retained1a = { - get effect () { - console.log( 'effect' ); +const retained1 = { + get effect() { + console.log('effect'); }, - get noEffect () { + get noEffect() { const x = 1; return x; } }; -const removed1 = retained1a.noEffect; -const retained1b = retained1a.effect; -const retained1c = retained1a[ 'eff' + 'ect' ]; +// removed +retained1.noEffect; -const removed2a = { - get shadowedEffect () { - console.log( 'effect' ); +//retained +retained1.effect; +retained1['eff' + 'ect']; + +const removed2 = { + get shadowedEffect() { + console.log('effect'); return 1; }, shadowedEffect: true, - set shadowedEffect ( value ) { - console.log( value ); + set shadowedEffect(value) { + console.log(value); } }; -const removed2b = removed2a.shadowedEffect; -const removed2c = removed2a.missingProp; +removed2.shadowedEffect; const retained3 = { - set effect ( value ) { - console.log( value ); + set effect(value) { + console.log(value); } }; retained3.effect = 'retained'; const retained4 = { - set effect ( value ) { - console.log( value ); + set effect(value) { + console.log(value); } }; -retained4[ 'eff' + 'ect' ] = 'retained'; +retained4['eff' + 'ect'] = 'retained'; const removed5 = { - set noEffect ( value ) { + set noEffect(value) { const x = value; } }; removed5.noEffect = 'removed'; -const removed6 = { - set shadowedEffect ( value ) { - console.log( value ); - }, - shadowedEffect: true -}; - -removed6.shadowedEffect = true; -removed6.missingProp = true; - const retained7 = { foo: () => {}, - get foo () { + get foo() { return 1; } }; diff --git a/test/form/samples/side-effects-object-literal-mutation/_config.js b/test/form/samples/side-effects-object-literal-mutation/_config.js index 2eaf74d0b0b..ffc52c259c1 100644 --- a/test/form/samples/side-effects-object-literal-mutation/_config.js +++ b/test/form/samples/side-effects-object-literal-mutation/_config.js @@ -1,4 +1,3 @@ module.exports = { - description: 'detects side-effects when mutating object literals', - options: { output: { name: 'bundle' } } + description: 'detects side-effects when mutating object literals' }; diff --git a/test/form/samples/side-effects-static-methods/_config.js b/test/form/samples/side-effects-static-methods/_config.js new file mode 100644 index 00000000000..129d0e1d6de --- /dev/null +++ b/test/form/samples/side-effects-static-methods/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'allow calls to pure static methods to be tree-shaken' +}; diff --git a/test/form/samples/side-effects-static-methods/_expected.js b/test/form/samples/side-effects-static-methods/_expected.js new file mode 100644 index 00000000000..3b12efac3e3 --- /dev/null +++ b/test/form/samples/side-effects-static-methods/_expected.js @@ -0,0 +1,38 @@ +class Effect { + static a() { log(); } +} +Effect.a(); + +class DeoptComputed { + static a() {} + static [foo]() { log(); } +} +DeoptComputed.a(); + +class DeoptGetter { + static a() {} + static get a() {} +} +DeoptGetter.a(); + +class DeoptAssign { + static a() {} +} +DeoptAssign.a = log; +DeoptAssign.a(); + +class DeoptFully { + static a() {} +} +unknown(DeoptFully); +DeoptFully.a(); + +class DeepAssign { + static a = {} + a = {} +} +DeepAssign.a.b = 1; +DeepAssign.prototype.a.b = 1; + +class DynamicAssign {} +DynamicAssign[foo()] = 1; diff --git a/test/form/samples/side-effects-static-methods/main.js b/test/form/samples/side-effects-static-methods/main.js new file mode 100644 index 00000000000..f2d3e1036b4 --- /dev/null +++ b/test/form/samples/side-effects-static-methods/main.js @@ -0,0 +1,43 @@ +class NoEffect { + static a() {} +} +NoEffect.a(); + +class Effect { + static a() { log(); } +} +Effect.a(); + +class DeoptComputed { + static a() {} + static [foo]() { log(); } +} +DeoptComputed.a(); + +class DeoptGetter { + static a() {} + static get a() {} +} +DeoptGetter.a(); + +class DeoptAssign { + static a() {} +} +DeoptAssign.a = log; +DeoptAssign.a(); + +class DeoptFully { + static a() {} +} +unknown(DeoptFully); +DeoptFully.a(); + +class DeepAssign { + static a = {} + a = {} +} +DeepAssign.a.b = 1; +DeepAssign.prototype.a.b = 1; + +class DynamicAssign {} +DynamicAssign[foo()] = 1; diff --git a/test/form/samples/skips-dead-branches/_expected/es.js b/test/form/samples/skips-dead-branches/_expected.js similarity index 100% rename from test/form/samples/skips-dead-branches/_expected/es.js rename to test/form/samples/skips-dead-branches/_expected.js diff --git a/test/form/samples/skips-dead-branches/_expected/amd.js b/test/form/samples/skips-dead-branches/_expected/amd.js deleted file mode 100644 index c192900373e..00000000000 --- a/test/form/samples/skips-dead-branches/_expected/amd.js +++ /dev/null @@ -1,9 +0,0 @@ -define(function () { 'use strict'; - - function bar () { - console.log( 'this should be included' ); - } - - bar(); - -}); diff --git a/test/form/samples/skips-dead-branches/_expected/cjs.js b/test/form/samples/skips-dead-branches/_expected/cjs.js deleted file mode 100644 index b3e974bb4e6..00000000000 --- a/test/form/samples/skips-dead-branches/_expected/cjs.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -function bar () { - console.log( 'this should be included' ); -} - -bar(); diff --git a/test/form/samples/skips-dead-branches/_expected/iife.js b/test/form/samples/skips-dead-branches/_expected/iife.js deleted file mode 100644 index 3b894d92a46..00000000000 --- a/test/form/samples/skips-dead-branches/_expected/iife.js +++ /dev/null @@ -1,10 +0,0 @@ -(function () { - 'use strict'; - - function bar () { - console.log( 'this should be included' ); - } - - bar(); - -}()); diff --git a/test/form/samples/skips-dead-branches/_expected/system.js b/test/form/samples/skips-dead-branches/_expected/system.js deleted file mode 100644 index 606d00fb15e..00000000000 --- a/test/form/samples/skips-dead-branches/_expected/system.js +++ /dev/null @@ -1,14 +0,0 @@ -System.register([], function () { - 'use strict'; - return { - execute: function () { - - function bar () { - console.log( 'this should be included' ); - } - - bar(); - - } - }; -}); diff --git a/test/form/samples/skips-dead-branches/_expected/umd.js b/test/form/samples/skips-dead-branches/_expected/umd.js deleted file mode 100644 index 06df11106d6..00000000000 --- a/test/form/samples/skips-dead-branches/_expected/umd.js +++ /dev/null @@ -1,12 +0,0 @@ -(function (factory) { - typeof define === 'function' && define.amd ? define(factory) : - factory(); -}((function () { 'use strict'; - - function bar () { - console.log( 'this should be included' ); - } - - bar(); - -}))); diff --git a/test/form/samples/static-class-property-calls/_config.js b/test/form/samples/static-class-property-calls/_config.js new file mode 100644 index 00000000000..2983907efde --- /dev/null +++ b/test/form/samples/static-class-property-calls/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles effects when calling static class properties' +}; diff --git a/test/form/samples/static-class-property-calls/_expected.js b/test/form/samples/static-class-property-calls/_expected.js new file mode 100644 index 00000000000..e2589637c16 --- /dev/null +++ b/test/form/samples/static-class-property-calls/_expected.js @@ -0,0 +1,21 @@ +class Foo { + static isTrue = () => true; + static noEffect = () => {}; + static effect = () => console.log('effect'); + static missing; +} + +console.log('retained'); +Foo.effect(); +Foo.missing(); + +class Bar { + static flag = false + static mutate = function() { + this.flag = true; + } +} + +Bar.mutate(); +if (Bar.flag) console.log('retained'); +else console.log('unimportant'); diff --git a/test/form/samples/static-class-property-calls/main.js b/test/form/samples/static-class-property-calls/main.js new file mode 100644 index 00000000000..4259adcdb71 --- /dev/null +++ b/test/form/samples/static-class-property-calls/main.js @@ -0,0 +1,23 @@ +class Foo { + static isTrue = () => true; + static noEffect = () => {}; + static effect = () => console.log('effect'); + static missing; +} + +if (Foo.isTrue()) console.log('retained'); +else console.log('removed'); +Foo.noEffect(); +Foo.effect(); +Foo.missing(); + +class Bar { + static flag = false + static mutate = function() { + this.flag = true; + } +} + +Bar.mutate(); +if (Bar.flag) console.log('retained'); +else console.log('unimportant'); diff --git a/test/form/samples/super-classes/super-class-prototype-access/_config.js b/test/form/samples/super-classes/super-class-prototype-access/_config.js new file mode 100644 index 00000000000..6bac45041bb --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-access/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'correctly resolves the prototype of the super class when accessing properties' +}; diff --git a/test/form/samples/super-classes/super-class-prototype-access/_expected.js b/test/form/samples/super-classes/super-class-prototype-access/_expected.js new file mode 100644 index 00000000000..936fab7c5a4 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-access/_expected.js @@ -0,0 +1,8 @@ +class SuperAccess { + prop = {}; + method() {} +} +class Access extends SuperAccess {} +Access.prototype.doesNoExist.throws; +Access.prototype.method.doesNoExist.throws; +Access.prototype.prop.throws; diff --git a/test/form/samples/super-classes/super-class-prototype-access/main.js b/test/form/samples/super-classes/super-class-prototype-access/main.js new file mode 100644 index 00000000000..fcf38d4ead6 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-access/main.js @@ -0,0 +1,11 @@ +class SuperAccess { + prop = {}; + method() {} +} +class Access extends SuperAccess {} + +Access.prototype.doesNoExist; +Access.prototype.doesNoExist.throws; +Access.prototype.method.doesNoExist; +Access.prototype.method.doesNoExist.throws; +Access.prototype.prop.throws; diff --git a/test/form/samples/super-classes/super-class-prototype-assignment/_config.js b/test/form/samples/super-classes/super-class-prototype-assignment/_config.js new file mode 100644 index 00000000000..dd974952e7f --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-assignment/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'correctly resolves the prototype of the super class when assigning properites' +}; diff --git a/test/form/samples/super-classes/super-class-prototype-assignment/_expected.js b/test/form/samples/super-classes/super-class-prototype-assignment/_expected.js new file mode 100644 index 00000000000..840f9a4d6c1 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-assignment/_expected.js @@ -0,0 +1,31 @@ +class SuperUsedAssign { + method() {} +} +class UsedAssign extends SuperUsedAssign {} +UsedAssign.prototype.doesNotExist = 1; +UsedAssign.prototype.method.doesNotExist = 1; +console.log(UsedAssign); + +class SuperAssign1 {} +class Assign1 extends SuperAssign1 {} +Assign1.prototype.doesNotExist.throws = 1; + +class SuperAssign2 { + prop = {}; +} +class Assign2 extends SuperAssign2 {} +Assign2.prototype.prop.throws = 1; + +class SuperAssign3 { + method() {} +} +class Assign3 extends SuperAssign3 {} +Assign3.prototype.method.doesNotExist.throws = 1; + +class SuperAssign4 { + set prop(v) { + console.log('effect', v); + } +} +class Assign4 extends SuperAssign4 {} +Assign4.prototype.prop = 1; diff --git a/test/form/samples/super-classes/super-class-prototype-assignment/main.js b/test/form/samples/super-classes/super-class-prototype-assignment/main.js new file mode 100644 index 00000000000..d47b557daf5 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-assignment/main.js @@ -0,0 +1,40 @@ +class SuperRemovedAssign { + method() {} + set prop(v) {} +} +class RemovedAssign extends SuperRemovedAssign {} +RemovedAssign.prototype.doesNotExist = 1; +RemovedAssign.prototype.method.doesNotExist = 1; +RemovedAssign.prototype.prop = 1; + +class SuperUsedAssign { + method() {} +} +class UsedAssign extends SuperUsedAssign {} +UsedAssign.prototype.doesNotExist = 1; +UsedAssign.prototype.method.doesNotExist = 1; +console.log(UsedAssign); + +class SuperAssign1 {} +class Assign1 extends SuperAssign1 {} +Assign1.prototype.doesNotExist.throws = 1; + +class SuperAssign2 { + prop = {}; +} +class Assign2 extends SuperAssign2 {} +Assign2.prototype.prop.throws = 1; + +class SuperAssign3 { + method() {} +} +class Assign3 extends SuperAssign3 {} +Assign3.prototype.method.doesNotExist.throws = 1; + +class SuperAssign4 { + set prop(v) { + console.log('effect', v); + } +} +class Assign4 extends SuperAssign4 {} +Assign4.prototype.prop = 1; diff --git a/test/form/samples/super-classes/super-class-prototype-calls/_config.js b/test/form/samples/super-classes/super-class-prototype-calls/_config.js new file mode 100644 index 00000000000..f0d4045a4f9 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-calls/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'correctly resolves the prototype of the super class when calling properties' +}; diff --git a/test/form/samples/super-classes/super-class-prototype-calls/_expected.js b/test/form/samples/super-classes/super-class-prototype-calls/_expected.js new file mode 100644 index 00000000000..478810e2b22 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-calls/_expected.js @@ -0,0 +1,23 @@ +class SuperValues { + get prop() { + return { + effect(used) { + console.log('effect', used); + }, + isTrue() { + return true; + } + }; + } + effect(used) { + console.log('effect', used); + } + isTrue() { + return true; + } +} +class Values extends SuperValues {} +console.log('retained'); +console.log('retained'); +Values.prototype.effect(); +Values.prototype.prop.effect(); diff --git a/test/form/samples/super-classes/super-class-prototype-calls/main.js b/test/form/samples/super-classes/super-class-prototype-calls/main.js new file mode 100644 index 00000000000..43c62658ce6 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-calls/main.js @@ -0,0 +1,25 @@ +class SuperValues { + get prop() { + return { + effect(used) { + console.log('effect', used); + }, + isTrue() { + return true; + } + }; + } + effect(used) { + console.log('effect', used); + } + isTrue() { + return true; + } +} +class Values extends SuperValues {} +if (Values.prototype.isTrue()) console.log('retained'); +else console.log('removed'); +if (Values.prototype.prop.isTrue()) console.log('retained'); +else console.log('removed'); +Values.prototype.effect(); +Values.prototype.prop.effect(); diff --git a/test/form/samples/super-classes/super-class-prototype-values/_config.js b/test/form/samples/super-classes/super-class-prototype-values/_config.js new file mode 100644 index 00000000000..0f3f274db77 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-values/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'correctly resolves the prototype of the super class when reading property values' +}; diff --git a/test/form/samples/super-classes/super-class-prototype-values/_expected.js b/test/form/samples/super-classes/super-class-prototype-values/_expected.js new file mode 100644 index 00000000000..09f0c0d87ce --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-values/_expected.js @@ -0,0 +1,15 @@ +console.log('retained'); +console.log('retained'); +// Note that isTrueProp is not part of the prototype +console.log('retained'); + +const prop = { isTrue: true }; +class SuperDeopt { + get prop() { + return prop; + } +} +class Deopt extends SuperDeopt {} +Deopt.prototype.prop.isTrue = false; +if (Deopt.prototype.prop.isTrue) console.log('unimportant'); +else console.log('retained'); diff --git a/test/form/samples/super-classes/super-class-prototype-values/main.js b/test/form/samples/super-classes/super-class-prototype-values/main.js new file mode 100644 index 00000000000..0fb92a2f737 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-values/main.js @@ -0,0 +1,28 @@ +class SuperValues { + isTrueProp = true; + get isTrue() { + return true; + } + get prop() { + return { isTrue: true }; + } +} +class Values extends SuperValues {} +if (Values.prototype.isTrue) console.log('retained'); +else console.log('removed'); +if (Values.prototype.prop.isTrue) console.log('retained'); +else console.log('removed'); +// Note that isTrueProp is not part of the prototype +if (Values.prototype.isTrueProp) console.log('removed'); +else console.log('retained'); + +const prop = { isTrue: true }; +class SuperDeopt { + get prop() { + return prop; + } +} +class Deopt extends SuperDeopt {} +Deopt.prototype.prop.isTrue = false; +if (Deopt.prototype.prop.isTrue) console.log('unimportant'); +else console.log('retained'); diff --git a/test/form/samples/this-in-class-body/_config.js b/test/form/samples/this-in-class-body/_config.js new file mode 100644 index 00000000000..1ade8977cfe --- /dev/null +++ b/test/form/samples/this-in-class-body/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'uses the correct "this" value in class properties' +}; diff --git a/test/form/samples/this-in-class-body/_expected.js b/test/form/samples/this-in-class-body/_expected.js new file mode 100644 index 00000000000..ff7c88e7839 --- /dev/null +++ b/test/form/samples/this-in-class-body/_expected.js @@ -0,0 +1,37 @@ +class Used { + static flag = false + static mutate = () => { + this.flag = true; + } +} +Used.mutate(); + +if (Used.flag) console.log('retained'); +else console.log('unimportant'); + +class InstanceMutation { + static flag = false + flag = false + mutate = () => { + this.flag = true; + } +} +(new InstanceMutation).mutate(); + +console.log('retained'); + +class UsedSuper { + static flag = false +} +class UsedWithSuper extends UsedSuper{ + static mutate = () => { + super.flag = true; + } +} +UsedWithSuper.mutate(); + +if (UsedWithSuper.flag) console.log('retained'); +else console.log('unimportant'); + +// Assignments via "super" do NOT mutate the super class! +console.log('retained'); diff --git a/test/form/samples/this-in-class-body/main.js b/test/form/samples/this-in-class-body/main.js new file mode 100644 index 00000000000..6fa4fc5ed65 --- /dev/null +++ b/test/form/samples/this-in-class-body/main.js @@ -0,0 +1,47 @@ +class Unused { + static flag = false + static mutate = () => { + this.flag = true; + } +} +Unused.mutate(); + +class Used { + static flag = false + static mutate = () => { + this.flag = true; + } +} +Used.mutate(); + +if (Used.flag) console.log('retained'); +else console.log('unimportant'); + +class InstanceMutation { + static flag = false + flag = false + mutate = () => { + this.flag = true; + } +} +(new InstanceMutation).mutate(); + +if (InstanceMutation.flag) console.log('removed'); +else console.log('retained'); + +class UsedSuper { + static flag = false +} +class UsedWithSuper extends UsedSuper{ + static mutate = () => { + super.flag = true; + } +} +UsedWithSuper.mutate(); + +if (UsedWithSuper.flag) console.log('retained'); +else console.log('unimportant'); + +// Assignments via "super" do NOT mutate the super class! +if (UsedSuper.flag) console.log('removed'); +else console.log('retained'); diff --git a/test/form/samples/undefined-properties/_config.js b/test/form/samples/undefined-properties/_config.js index a89e4af5ef0..29b3cc673cb 100644 --- a/test/form/samples/undefined-properties/_config.js +++ b/test/form/samples/undefined-properties/_config.js @@ -1,4 +1,3 @@ module.exports = { - description: 'detects undefined properties', - expectedWarnings: ['EMPTY_BUNDLE'] + description: 'detects undefined properties' }; diff --git a/test/form/samples/undefined-properties/_expected.js b/test/form/samples/undefined-properties/_expected.js index 8b137891791..f97408e6838 100644 --- a/test/form/samples/undefined-properties/_expected.js +++ b/test/form/samples/undefined-properties/_expected.js @@ -1 +1,10 @@ +var a = { + b: { + c: 'd' + } +}; +console.log('retained'); + +if (a.c.d) console.log('retained for effect'); +else console.log('retained for effect'); diff --git a/test/form/samples/undefined-properties/main.js b/test/form/samples/undefined-properties/main.js index e7df0af9514..bf2ad33721a 100644 --- a/test/form/samples/undefined-properties/main.js +++ b/test/form/samples/undefined-properties/main.js @@ -4,5 +4,8 @@ var a = { } }; -if (a.b.d) - console.log('yes'); \ No newline at end of file +if (a.b.d) console.log('removed'); +else console.log('retained'); + +if (a.c.d) console.log('retained for effect'); +else console.log('retained for effect'); \ No newline at end of file diff --git a/test/function/samples/access-instance-prop/_config.js b/test/function/samples/access-instance-prop/_config.js new file mode 100644 index 00000000000..98b5968822b --- /dev/null +++ b/test/function/samples/access-instance-prop/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'respects getters when accessing properties of an instance' +}; diff --git a/test/function/samples/access-instance-prop/main.js b/test/function/samples/access-instance-prop/main.js new file mode 100644 index 00000000000..69b8f8f7946 --- /dev/null +++ b/test/function/samples/access-instance-prop/main.js @@ -0,0 +1,13 @@ +let effect = false; + +var b = { + get a() { + effect = true; + } +}; + +function X() {} +X.prototype = b; +new X().a; + +assert.ok(effect); diff --git a/test/function/samples/array-getter/_config.js b/test/function/samples/array-getter/_config.js new file mode 100644 index 00000000000..e0437548252 --- /dev/null +++ b/test/function/samples/array-getter/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles getters defined on arrays' +}; diff --git a/test/function/samples/array-getter/main.js b/test/function/samples/array-getter/main.js new file mode 100644 index 00000000000..9ac16a7b6e6 --- /dev/null +++ b/test/function/samples/array-getter/main.js @@ -0,0 +1,14 @@ +let flag = false; +const array = []; + +Object.defineProperty(array, 'prop', { + get() { + flag = true; + } +}); + +array.prop; + +if (!flag) { + throw new Error('Mutation not detected'); +} diff --git a/test/function/samples/array-mutation/_config.js b/test/function/samples/array-mutation/_config.js new file mode 100644 index 00000000000..612a42d2e21 --- /dev/null +++ b/test/function/samples/array-mutation/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks array mutations' +}; diff --git a/test/function/samples/array-mutation/main.js b/test/function/samples/array-mutation/main.js new file mode 100644 index 00000000000..98363f0f469 --- /dev/null +++ b/test/function/samples/array-mutation/main.js @@ -0,0 +1,4 @@ +const array = []; +array.push(true); + +assert.strictEqual(array[0] || false, true); diff --git a/test/function/samples/conditionals-deoptimization/_config.js b/test/function/samples/conditionals-deoptimization/_config.js new file mode 100644 index 00000000000..48ed684f81d --- /dev/null +++ b/test/function/samples/conditionals-deoptimization/_config.js @@ -0,0 +1,8 @@ +const assert = require('assert'); + +module.exports = { + description: 'handles deoptimization of conditionals', + exports(exports) { + assert.deepStrictEqual(exports, { first: true, second: true, third: true, fourth: true }); + } +}; diff --git a/test/function/samples/conditionals-deoptimization/main.js b/test/function/samples/conditionals-deoptimization/main.js new file mode 100644 index 00000000000..324e03b344e --- /dev/null +++ b/test/function/samples/conditionals-deoptimization/main.js @@ -0,0 +1,21 @@ +export let first = false; +export let second = false; +export let third = false; +export let fourth = false; + +let flag = false; +checkConditional(); +checkLogical(); +flag = true; +checkConditional(); +checkLogical(); + +function checkConditional() { + if (flag ? true : false) first = true; + else second = true; +} + +function checkLogical() { + if (flag && true) third = true; + else fourth = true; +} diff --git a/test/function/samples/deoptimize-assumes-getters/_config.js b/test/function/samples/deoptimize-assumes-getters/_config.js new file mode 100644 index 00000000000..c04d33fbc9b --- /dev/null +++ b/test/function/samples/deoptimize-assumes-getters/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'assume full deoptimization may inject side-effectful getters' +}; diff --git a/test/function/samples/deoptimize-assumes-getters/main.js b/test/function/samples/deoptimize-assumes-getters/main.js new file mode 100644 index 00000000000..c11b529a8ff --- /dev/null +++ b/test/function/samples/deoptimize-assumes-getters/main.js @@ -0,0 +1,14 @@ +const obj = {}; +addGetter(obj); + +let mutated = false; +obj.prop; +assert.ok(mutated); + +function addGetter(o) { + Object.defineProperty(o, 'prop', { + get() { + mutated = true; + } + }); +} diff --git a/test/function/samples/deoptimize-class/_config.js b/test/function/samples/deoptimize-class/_config.js new file mode 100644 index 00000000000..14bcdb722ee --- /dev/null +++ b/test/function/samples/deoptimize-class/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles deoptimization of static class properties' +}; diff --git a/test/function/samples/deoptimize-class/main.js b/test/function/samples/deoptimize-class/main.js new file mode 100644 index 00000000000..f67b5d66413 --- /dev/null +++ b/test/function/samples/deoptimize-class/main.js @@ -0,0 +1,20 @@ +let trueProp = 'old'; +let falseProp = 'new'; + +function getClass() { + return class { + static [trueProp]() { + return true; + } + static [falseProp]() { + return false; + } + }; +} + +if (!getClass().old()) throw new Error('old missing'); +if (getClass().new()) throw new Error('new present'); +trueProp = 'new'; +falseProp = 'old'; +if (getClass().old()) throw new Error('old present'); +if (!getClass().new()) throw new Error('new missing'); diff --git a/test/function/samples/deoptimize-computed-class-keys/_config.js b/test/function/samples/deoptimize-computed-class-keys/_config.js new file mode 100644 index 00000000000..53e617bd7fe --- /dev/null +++ b/test/function/samples/deoptimize-computed-class-keys/_config.js @@ -0,0 +1,4 @@ +module.exports = { + description: 'deoptimizes computed class property keys', + minNodeVersion: 12 +}; diff --git a/test/function/samples/deoptimize-computed-class-keys/main.js b/test/function/samples/deoptimize-computed-class-keys/main.js new file mode 100644 index 00000000000..bad340b2305 --- /dev/null +++ b/test/function/samples/deoptimize-computed-class-keys/main.js @@ -0,0 +1,17 @@ +var key1 = 'x'; +var key2 = 'y'; + +changeKeys(); + +class Foo { + static [key1] = true; + static [key2] = false; +} + +assert.strictEqual(Foo.x, false); +assert.strictEqual(Foo.y, true); + +function changeKeys() { + key1 = 'y'; + key2 = 'x'; +} diff --git a/test/function/samples/deoptimize-computed-keys/_config.js b/test/function/samples/deoptimize-computed-keys/_config.js new file mode 100644 index 00000000000..2ac75a24810 --- /dev/null +++ b/test/function/samples/deoptimize-computed-keys/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'deoptimizes computed object property keys' +}; diff --git a/test/function/samples/deoptimize-computed-keys/main.js b/test/function/samples/deoptimize-computed-keys/main.js new file mode 100644 index 00000000000..273087a4e6a --- /dev/null +++ b/test/function/samples/deoptimize-computed-keys/main.js @@ -0,0 +1,14 @@ +var key1 = 'x' +var key2 = 'y' + +changeKeys(); + +var foo = { [key1]: true, [key2]: false }; + +assert.strictEqual(foo.x, false); +assert.strictEqual(foo.y, true); + +function changeKeys() { + key1 = 'y' + key2 = 'x' +} \ No newline at end of file diff --git a/test/function/samples/deoptimize-object/_config.js b/test/function/samples/deoptimize-object/_config.js new file mode 100644 index 00000000000..2906b6780d7 --- /dev/null +++ b/test/function/samples/deoptimize-object/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles deoptimization of object properties' +}; diff --git a/test/function/samples/deoptimize-object/main.js b/test/function/samples/deoptimize-object/main.js new file mode 100644 index 00000000000..bfd1b80873e --- /dev/null +++ b/test/function/samples/deoptimize-object/main.js @@ -0,0 +1,13 @@ +let prop = 'old'; + +function getObj() { + return { + [prop]: true + }; +} + +if (!getObj().old) throw new Error('old missing'); +if (getObj().new) throw new Error('new present'); +prop = 'new'; +if (getObj().old) throw new Error('old present'); +if (!getObj().new) throw new Error('new missing'); diff --git a/test/function/samples/deoptimize-this-parameters/_config.js b/test/function/samples/deoptimize-this-parameters/_config.js new file mode 100644 index 00000000000..518f65d290a --- /dev/null +++ b/test/function/samples/deoptimize-this-parameters/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles deoptimization of this parameters' +}; diff --git a/test/function/samples/deoptimize-this-parameters/main.js b/test/function/samples/deoptimize-this-parameters/main.js new file mode 100644 index 00000000000..89792dd4aea --- /dev/null +++ b/test/function/samples/deoptimize-this-parameters/main.js @@ -0,0 +1,21 @@ +const obj = { + flag: false, + otherFlag: false, + otherProp() { + this.otherFlag = true; + }, + prop() { + this.flag = true; + this.otherProp(); + } +}; + +const otherObj = { + prop: obj.prop, + otherProp: obj.otherProp +}; + +obj.prop(); +otherObj.prop(); +if (!obj.flag) throw new Error('first flag missing'); +if (!otherObj.flag) throw new Error('second flag missing'); diff --git a/test/function/samples/modify-object-via-this-d/_config.js b/test/function/samples/modify-object-via-this-d/_config.js new file mode 100644 index 00000000000..0c4423de923 --- /dev/null +++ b/test/function/samples/modify-object-via-this-d/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks object modification via "this"' +}; diff --git a/test/function/samples/modify-object-via-this-d/main.js b/test/function/samples/modify-object-via-this-d/main.js new file mode 100644 index 00000000000..55adf0b053b --- /dev/null +++ b/test/function/samples/modify-object-via-this-d/main.js @@ -0,0 +1,13 @@ +const obj = { + modify() {} +}; + +obj.modify = modify; + +function modify() { + this.modified = true; +} + +obj.modify(); + +assert.strictEqual(obj.modified ? 'MODIFIED' : 'BROKEN', 'MODIFIED'); diff --git a/test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/_config.js b/test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/_config.js new file mode 100644 index 00000000000..4b7c9b3ebb3 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles fully deoptimized objects', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/main.js b/test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/main.js new file mode 100644 index 00000000000..def94e30b12 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/main.js @@ -0,0 +1,17 @@ +import { unknown } from 'external'; + +const obj = { + flag: false +}; + +Object.defineProperty(obj, unknown, { + get() { + this.flag = true; + } +}); + +obj.prop; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/getter-in-assignment/_config.js b/test/function/samples/modify-this-via-getter/getter-in-assignment/_config.js new file mode 100644 index 00000000000..4f3496ac6bc --- /dev/null +++ b/test/function/samples/modify-this-via-getter/getter-in-assignment/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles known getters that are triggered via an assignment' +}; diff --git a/test/function/samples/modify-this-via-getter/getter-in-assignment/main.js b/test/function/samples/modify-this-via-getter/getter-in-assignment/main.js new file mode 100644 index 00000000000..4244e759d89 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/getter-in-assignment/main.js @@ -0,0 +1,14 @@ +const obj = { + flag: false, + get prop() { + this.flag = true; + return 1; + }, + set prop(v) {} +}; + +obj.prop += 1; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/getters-on-this/_config.js b/test/function/samples/modify-this-via-getter/getters-on-this/_config.js new file mode 100644 index 00000000000..6bd8c9213a8 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/getters-on-this/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles calling getters on "this"', +}; diff --git a/test/function/samples/modify-this-via-getter/getters-on-this/main.js b/test/function/samples/modify-this-via-getter/getters-on-this/main.js new file mode 100644 index 00000000000..24f3b72fdbd --- /dev/null +++ b/test/function/samples/modify-this-via-getter/getters-on-this/main.js @@ -0,0 +1,39 @@ +const obj = { + flag1: false, + flag2: false, + flag3: false, + flag4: false, + prop1() { + this.flag1 = true; + this.prop2(); + }, + prop2() { + this.flag2 = true; + this.prop3; + }, + get prop3() { + this.flag3 = true; + this.prop4 = true; + }, + set prop4(value) { + this.flag4 = value; + } +}; + +obj.prop1(); + +if (!obj.flag1) { + throw new Error('mutation 1 not detected'); +} + +if (!obj.flag2) { + throw new Error('mutation 2 not detected'); +} + +if (!obj.flag3) { + throw new Error('mutation 3 not detected'); +} + +if (!obj.flag4) { + throw new Error('mutation 4 not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/known-getter/_config.js b/test/function/samples/modify-this-via-getter/known-getter/_config.js new file mode 100644 index 00000000000..b608017f8e2 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/known-getter/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles known getters that modify "this"' +}; diff --git a/test/function/samples/modify-this-via-getter/known-getter/main.js b/test/function/samples/modify-this-via-getter/known-getter/main.js new file mode 100644 index 00000000000..9fa5e548b09 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/known-getter/main.js @@ -0,0 +1,12 @@ +const obj = { + flag: false, + get prop() { + this.flag = true; + } +}; + +obj.prop; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/known-super-prop/_config.js b/test/function/samples/modify-this-via-getter/known-super-prop/_config.js new file mode 100644 index 00000000000..52280f050d4 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/known-super-prop/_config.js @@ -0,0 +1,4 @@ +module.exports = { + description: 'handles getters that modify "this" on prototypes for known properties', + minNodeVersion: 12 +}; diff --git a/test/function/samples/modify-this-via-getter/known-super-prop/main.js b/test/function/samples/modify-this-via-getter/known-super-prop/main.js new file mode 100644 index 00000000000..ca4cd5a954c --- /dev/null +++ b/test/function/samples/modify-this-via-getter/known-super-prop/main.js @@ -0,0 +1,14 @@ +class proto { + static flag = false; + static get prop() { + this.flag = true; + } +} + +class obj extends proto {} + +obj.prop; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/unknown-prop-getter/_config.js b/test/function/samples/modify-this-via-getter/unknown-prop-getter/_config.js new file mode 100644 index 00000000000..4af96c05a70 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-prop-getter/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles unknown getters that modify "this"', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-getter/unknown-prop-getter/main.js b/test/function/samples/modify-this-via-getter/unknown-prop-getter/main.js new file mode 100644 index 00000000000..c8829e86915 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-prop-getter/main.js @@ -0,0 +1,14 @@ +import { unknown } from 'external'; + +const obj = { + get [unknown]() { + this.flag = true; + }, + flag: false +}; + +obj.prop; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/_config.js b/test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/_config.js new file mode 100644 index 00000000000..884460d181b --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles unknown getters that modify "this" for unknown property access', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/main.js b/test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/main.js new file mode 100644 index 00000000000..40846145270 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/main.js @@ -0,0 +1,14 @@ +import { unknown } from 'external'; + +const obj = { + get [unknown]() { + this.flag = true; + }, + flag: false +}; + +obj[unknown]; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/unknown-property-access/_config.js b/test/function/samples/modify-this-via-getter/unknown-property-access/_config.js new file mode 100644 index 00000000000..98690a2ad8d --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-property-access/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles getters that modify "this" for unknown property access', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-getter/unknown-property-access/main.js b/test/function/samples/modify-this-via-getter/unknown-property-access/main.js new file mode 100644 index 00000000000..0c790f0347f --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-property-access/main.js @@ -0,0 +1,14 @@ +import { unknown } from 'external'; + +const obj = { + flag: false, + get prop() { + this.flag = true; + } +}; + +obj[unknown]; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/unknown-super-prop/_config.js b/test/function/samples/modify-this-via-getter/unknown-super-prop/_config.js new file mode 100644 index 00000000000..7e4188cbd13 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-super-prop/_config.js @@ -0,0 +1,12 @@ +module.exports = { + description: 'handles getters that modify "this" on prototypes for unknown properties', + minNodeVersion: 12, + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-getter/unknown-super-prop/main.js b/test/function/samples/modify-this-via-getter/unknown-super-prop/main.js new file mode 100644 index 00000000000..9530c344172 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-super-prop/main.js @@ -0,0 +1,16 @@ +import { unknown } from 'external'; + +class proto { + static flag = false; + static get prop() { + this.flag = true; + } +} + +class obj extends proto {} + +obj[unknown]; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/_config.js b/test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/_config.js new file mode 100644 index 00000000000..4b7c9b3ebb3 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles fully deoptimized objects', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/main.js b/test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/main.js new file mode 100644 index 00000000000..73c7d3a1af3 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/main.js @@ -0,0 +1,17 @@ +import { unknown } from 'external'; + +const obj = { + flag: false +}; + +Object.defineProperty(obj, unknown, { + set(value) { + this.flag = value; + } +}); + +obj.prop = true; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-setter/known-setter/_config.js b/test/function/samples/modify-this-via-setter/known-setter/_config.js new file mode 100644 index 00000000000..449c895ba6d --- /dev/null +++ b/test/function/samples/modify-this-via-setter/known-setter/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles known setters that modify "this"' +}; diff --git a/test/function/samples/modify-this-via-setter/known-setter/main.js b/test/function/samples/modify-this-via-setter/known-setter/main.js new file mode 100644 index 00000000000..74930dba6fb --- /dev/null +++ b/test/function/samples/modify-this-via-setter/known-setter/main.js @@ -0,0 +1,12 @@ +const obj = { + flag: false, + set prop(value) { + this.flag = value; + } +}; + +obj.prop = true; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-setter/known-super-prop/_config.js b/test/function/samples/modify-this-via-setter/known-super-prop/_config.js new file mode 100644 index 00000000000..1a2b7ea0df4 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/known-super-prop/_config.js @@ -0,0 +1,4 @@ +module.exports = { + description: 'handles setters that modify "this" on prototypes for known properties', + minNodeVersion: 12 +}; diff --git a/test/function/samples/modify-this-via-setter/known-super-prop/main.js b/test/function/samples/modify-this-via-setter/known-super-prop/main.js new file mode 100644 index 00000000000..d6f72d34575 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/known-super-prop/main.js @@ -0,0 +1,14 @@ +class proto { + static flag = false; + static set prop(value) { + this.flag = value; + } +} + +class obj extends proto {} + +obj.prop = true; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-setter/unknown-prop-setter/_config.js b/test/function/samples/modify-this-via-setter/unknown-prop-setter/_config.js new file mode 100644 index 00000000000..d6305f6c204 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/unknown-prop-setter/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles unknown setters that modify "this"', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-setter/unknown-prop-setter/main.js b/test/function/samples/modify-this-via-setter/unknown-prop-setter/main.js new file mode 100644 index 00000000000..4285ed33781 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/unknown-prop-setter/main.js @@ -0,0 +1,14 @@ +import { unknown } from 'external'; + +const obj = { + set [unknown](value) { + this.flag = value; + }, + flag: false +}; + +obj.prop = true; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/_config.js b/test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/_config.js new file mode 100644 index 00000000000..5b67fd6ea0e --- /dev/null +++ b/test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles unknown setters that modify "this" for unknown property access', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/main.js b/test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/main.js new file mode 100644 index 00000000000..536e3c7fa79 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/main.js @@ -0,0 +1,14 @@ +import { unknown } from 'external'; + +const obj = { + set [unknown](value) { + this.flag = value; + }, + flag: false +}; + +obj[unknown] = true; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-setter/unknown-super-prop/_config.js b/test/function/samples/modify-this-via-setter/unknown-super-prop/_config.js new file mode 100644 index 00000000000..c68617f4906 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/unknown-super-prop/_config.js @@ -0,0 +1,12 @@ +module.exports = { + description: 'handles setters that modify "this" on prototypes for unknown properties', + minNodeVersion: 12, + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-setter/unknown-super-prop/main.js b/test/function/samples/modify-this-via-setter/unknown-super-prop/main.js new file mode 100644 index 00000000000..f1e106b4b70 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/unknown-super-prop/main.js @@ -0,0 +1,16 @@ +import { unknown } from 'external'; + +class proto { + static flag = false; + static set prop(value) { + this.flag = value; + } +} + +class obj extends proto {} + +obj[unknown] = true; + +if (!obj.flag) { + throw new Error('mutation not detected'); +}