From 690510f83587cf05736632d9618a6dae79e27454 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Sat, 18 Nov 2017 08:29:36 +0100 Subject: [PATCH] Simplify assignment -> reassignment tracking --- src/ast/Node.js | 6 +- src/ast/nodes/ArrayPattern.js | 4 +- src/ast/nodes/AssignmentExpression.js | 2 +- src/ast/nodes/AssignmentPattern.js | 6 +- src/ast/nodes/CallExpression.js | 4 +- src/ast/nodes/ConditionalExpression.js | 4 +- src/ast/nodes/ForOfStatement.js | 3 +- src/ast/nodes/Identifier.js | 4 +- src/ast/nodes/LogicalExpression.js | 4 +- src/ast/nodes/MemberExpression.js | 8 +- src/ast/nodes/ObjectExpression.js | 6 +- src/ast/nodes/ObjectPattern.js | 4 +- src/ast/nodes/Property.js | 6 +- src/ast/nodes/RestElement.js | 4 +- src/ast/nodes/UnaryExpression.js | 4 +- src/ast/nodes/UpdateExpression.js | 3 +- src/ast/nodes/VariableDeclaration.js | 5 +- src/ast/nodes/VariableDeclarator.js | 4 +- src/ast/nodes/shared/VirtualNumberLiteral.js | 15 --- src/ast/scopes/Scope.js | 3 +- src/ast/values.js | 4 +- src/ast/variables/ArgumentsVariable.js | 4 +- src/ast/variables/LocalVariable.js | 29 ++---- src/ast/variables/Variable.js | 5 +- .../variables/VariableReassignmentTracker.js | 79 +++++++++++++++ src/ast/variables/VariableShapeTracker.js | 96 ------------------- .../_expected/amd.js | 1 - .../_expected/cjs.js | 1 - .../_expected/es.js | 1 - .../_expected/iife.js | 1 - .../_expected/umd.js | 1 - .../conditional-expression-paths/main.js | 2 - .../cyclic-assignments/_expected/amd.js | 9 ++ .../cyclic-assignments/_expected/cjs.js | 10 ++ .../cyclic-assignments/_expected/es.js | 9 ++ .../cyclic-assignments/_expected/iife.js | 9 ++ .../cyclic-assignments/_expected/umd.js | 9 ++ .../side-effects-object-literal-calls/main.js | 1 - .../main.js | 7 +- .../_expected/amd.js | 6 +- .../_expected/cjs.js | 6 +- .../side-effects-reassignment/_expected/es.js | 6 +- .../_expected/iife.js | 6 +- .../_expected/umd.js | 6 +- .../samples/side-effects-reassignment/main.js | 12 +-- 45 files changed, 198 insertions(+), 221 deletions(-) delete mode 100644 src/ast/nodes/shared/VirtualNumberLiteral.js create mode 100644 src/ast/variables/VariableReassignmentTracker.js delete mode 100644 src/ast/variables/VariableShapeTracker.js diff --git a/src/ast/Node.js b/src/ast/Node.js index caf3fbead86..29d918ad15e 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -20,8 +20,8 @@ export default class Node { } /** - * Bind an expression as an assignment to a node given a path. - * E.g., node.bindAssignmentAtPath(['x', 'y'], otherNode) is called when otherNode + * Reassign a given path of an object. + * E.g., node.reassignPath(['x', 'y']) is called when something * is assigned to node.x.y. * The default noop implementation is ok as long as hasEffectsWhenAssignedAtPath * always returns true for this node. Otherwise it should be overridden. @@ -29,7 +29,7 @@ export default class Node { * @param {Node} expression * @param {ExecutionPathOptions} options */ - bindAssignmentAtPath ( path, expression, options ) {} + reassignPath ( path, options ) {} /** * Override to control on which children "bind" is called. diff --git a/src/ast/nodes/ArrayPattern.js b/src/ast/nodes/ArrayPattern.js index 8c6437e4d01..b79b1bb1578 100644 --- a/src/ast/nodes/ArrayPattern.js +++ b/src/ast/nodes/ArrayPattern.js @@ -2,9 +2,9 @@ import Node from '../Node.js'; import { UNKNOWN_ASSIGNMENT } from '../values'; export default class ArrayPattern extends Node { - bindAssignmentAtPath ( path, expression, options ) { + reassignPath ( path, options ) { path.length === 0 - && this.eachChild( child => child.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT, options ) ); + && this.eachChild( child => child.reassignPath( [], options ) ); } hasEffectsWhenAssignedAtPath ( path, options ) { diff --git a/src/ast/nodes/AssignmentExpression.js b/src/ast/nodes/AssignmentExpression.js index 6cab3439f4e..9ec54c8d7cb 100644 --- a/src/ast/nodes/AssignmentExpression.js +++ b/src/ast/nodes/AssignmentExpression.js @@ -5,7 +5,7 @@ import ExecutionPathOptions from '../ExecutionPathOptions'; export default class AssignmentExpression extends Node { bindNode () { disallowIllegalReassignment( this.scope, this.left ); - this.left.bindAssignmentAtPath( [], this.right, ExecutionPathOptions.create() ); + this.left.reassignPath( [], ExecutionPathOptions.create() ); } hasEffects ( options ) { diff --git a/src/ast/nodes/AssignmentPattern.js b/src/ast/nodes/AssignmentPattern.js index eb3df0e338e..876b06f77ca 100644 --- a/src/ast/nodes/AssignmentPattern.js +++ b/src/ast/nodes/AssignmentPattern.js @@ -3,12 +3,12 @@ import ExecutionPathOptions from '../ExecutionPathOptions'; export default class AssignmentPattern extends Node { bindNode () { - this.left.bindAssignmentAtPath( [], this.right, ExecutionPathOptions.create() ); + this.left.reassignPath( [], ExecutionPathOptions.create() ); } - bindAssignmentAtPath ( path, expression, options ) { + reassignPath ( path, options ) { path.length === 0 - && this.left.bindAssignmentAtPath( path, expression, options ); + && this.left.reassignPath( path, options ); } hasEffectsWhenAssignedAtPath ( path, options ) { diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index b1ab507a2a5..dcd1d3130e9 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -2,10 +2,10 @@ import Node from '../Node.js'; import CallOptions from '../CallOptions'; export default class CallExpression extends Node { - bindAssignmentAtPath ( path, expression, options ) { + reassignPath ( path, options ) { !options.hasReturnExpressionBeenAssignedAtPath( path, this ) && this.callee.forEachReturnExpressionWhenCalledAtPath( [], this._callOptions, innerOptions => node => - node.bindAssignmentAtPath( path, expression, innerOptions.addAssignedReturnExpressionAtPath( path, this ) ), options ); + node.reassignPath( path, innerOptions.addAssignedReturnExpressionAtPath( path, this ) ), options ); } bindNode () { diff --git a/src/ast/nodes/ConditionalExpression.js b/src/ast/nodes/ConditionalExpression.js index 25092c3df1a..4aabe5f0354 100644 --- a/src/ast/nodes/ConditionalExpression.js +++ b/src/ast/nodes/ConditionalExpression.js @@ -2,9 +2,9 @@ import Node from '../Node.js'; import { UNKNOWN_VALUE } from '../values.js'; export default class ConditionalExpression extends Node { - bindAssignmentAtPath ( path, expression, options ) { + reassignPath ( path, options ) { path.length > 0 - && this._forEachRelevantBranch( node => node.bindAssignmentAtPath( path, expression, options ) ); + && this._forEachRelevantBranch( node => node.reassignPath( path, options ) ); } forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) { diff --git a/src/ast/nodes/ForOfStatement.js b/src/ast/nodes/ForOfStatement.js index 683d8a3882a..f7963d8545f 100644 --- a/src/ast/nodes/ForOfStatement.js +++ b/src/ast/nodes/ForOfStatement.js @@ -1,11 +1,10 @@ import Statement from './shared/Statement.js'; import BlockScope from '../scopes/BlockScope'; -import { UNKNOWN_ASSIGNMENT } from '../values'; import ExecutionPathOptions from '../ExecutionPathOptions'; export default class ForOfStatement extends Statement { bindNode () { - this.left.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT, ExecutionPathOptions.create() ); + this.left.reassignPath( [], ExecutionPathOptions.create() ); } hasEffects ( options ) { diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index 153b4c0e3f4..525de18c1fe 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -3,10 +3,10 @@ import isReference from 'is-reference'; import { UNKNOWN_ASSIGNMENT } from '../values'; export default class Identifier extends Node { - bindAssignmentAtPath ( path, expression, options ) { + reassignPath ( path, options ) { this._bindVariableIfMissing(); this.variable - && this.variable.bindAssignmentAtPath( path, expression, options ); + && this.variable.reassignPath( path, options ); } bindNode () { diff --git a/src/ast/nodes/LogicalExpression.js b/src/ast/nodes/LogicalExpression.js index 3048df12d88..2e0b3bb05ab 100644 --- a/src/ast/nodes/LogicalExpression.js +++ b/src/ast/nodes/LogicalExpression.js @@ -2,9 +2,9 @@ import Node from '../Node.js'; import { UNKNOWN_VALUE } from '../values.js'; export default class LogicalExpression extends Node { - bindAssignmentAtPath ( path, expression, options ) { + reassignPath ( path, options ) { path.length > 0 - && this._forEachRelevantBranch( node => node.bindAssignmentAtPath( path, expression, options ) ); + && this._forEachRelevantBranch( node => node.reassignPath( path, options ) ); } forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) { diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index 3fc704f1ef7..a03b0934616 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -1,6 +1,6 @@ import relativeId from '../../utils/relativeId.js'; import Node from '../Node.js'; -import { UNKNOWN_KEY } from '../variables/VariableShapeTracker'; +import { UNKNOWN_KEY } from '../variables/VariableReassignmentTracker'; const validProp = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; @@ -76,12 +76,12 @@ export default class MemberExpression extends Node { } } - bindAssignmentAtPath ( path, expression, options ) { + reassignPath ( path, options ) { if ( !this._bound ) this.bind(); if ( this.variable ) { - this.variable.bindAssignmentAtPath( path, expression, options ); + this.variable.reassignPath( path, options ); } else { - this.object.bindAssignmentAtPath( [ this._getPathSegment(), ...path ], expression, options ); + this.object.reassignPath( [ this._getPathSegment(), ...path ], options ); } } diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index a662d1b8eec..7d7406b0d94 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -1,18 +1,18 @@ import Node from '../Node.js'; -import { UNKNOWN_KEY } from '../variables/VariableShapeTracker'; +import { UNKNOWN_KEY } from '../variables/VariableReassignmentTracker'; const PROPERTY_KINDS_READ = [ 'init', 'get' ]; const PROPERTY_KINDS_WRITE = [ 'init', 'set' ]; export default class ObjectExpression extends Node { - bindAssignmentAtPath ( path, expression, options ) { + reassignPath ( path, options ) { if ( path.length === 0 ) return; const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ], path.length === 1 ? PROPERTY_KINDS_WRITE : PROPERTY_KINDS_READ ); (path.length === 1 || hasCertainHit) && properties.forEach( property => (path.length > 1 || property.kind === 'set') - && property.bindAssignmentAtPath( path.slice( 1 ), expression, options ) ); + && property.reassignPath( path.slice( 1 ), options ) ); } forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) { diff --git a/src/ast/nodes/ObjectPattern.js b/src/ast/nodes/ObjectPattern.js index 2aea456d45c..788547ee733 100644 --- a/src/ast/nodes/ObjectPattern.js +++ b/src/ast/nodes/ObjectPattern.js @@ -1,9 +1,9 @@ import Node from '../Node.js'; export default class ObjectPattern extends Node { - bindAssignmentAtPath ( path, expression, options ) { + reassignPath ( path, options ) { path.length === 0 - && this.properties.forEach( child => child.bindAssignmentAtPath( path, expression, options ) ); + && this.properties.forEach( child => child.reassignPath( path, options ) ); } hasEffectsWhenAssignedAtPath ( path, options ) { diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index d71b688e8c0..0b5ba59a6b9 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -3,13 +3,13 @@ import CallOptions from '../CallOptions'; import { UNKNOWN_ASSIGNMENT } from '../values'; export default class Property extends Node { - bindAssignmentAtPath ( path, expression, options ) { + reassignPath ( path, options ) { if ( this.kind === 'get' ) { path.length > 0 && this.value.forEachReturnExpressionWhenCalledAtPath( [], this._accessorCallOptions, innerOptions => node => - node.bindAssignmentAtPath( path, expression, innerOptions.addAssignedReturnExpressionAtPath( path, this ) ), options ); + node.reassignPath( path, innerOptions.addAssignedReturnExpressionAtPath( path, this ) ), options ); } else if ( this.kind !== 'set' ) { - this.value.bindAssignmentAtPath( path, expression, options ); + this.value.reassignPath( path, options ); } } diff --git a/src/ast/nodes/RestElement.js b/src/ast/nodes/RestElement.js index c7cd16e12f7..77b6eb89dc4 100644 --- a/src/ast/nodes/RestElement.js +++ b/src/ast/nodes/RestElement.js @@ -2,9 +2,9 @@ import Node from '../Node.js'; import { UNKNOWN_ASSIGNMENT } from '../values'; export default class RestElement extends Node { - bindAssignmentAtPath ( path, expression, options ) { + reassignPath ( path, options ) { path.length === 0 - && this.argument.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT, options ); + && this.argument.reassignPath( [], options ); } hasEffectsWhenAssignedAtPath ( path, options ) { diff --git a/src/ast/nodes/UnaryExpression.js b/src/ast/nodes/UnaryExpression.js index de72d77c3ed..942c670f8c2 100644 --- a/src/ast/nodes/UnaryExpression.js +++ b/src/ast/nodes/UnaryExpression.js @@ -1,5 +1,5 @@ import Node from '../Node.js'; -import { UNDEFINED_ASSIGNMENT, UNKNOWN_VALUE } from '../values'; +import { UNKNOWN_VALUE } from '../values'; import ExecutionPathOptions from '../ExecutionPathOptions'; const operators = { @@ -15,7 +15,7 @@ const operators = { export default class UnaryExpression extends Node { bindNode () { if ( this.operator === 'delete' ) { - this.argument.bindAssignmentAtPath( [], UNDEFINED_ASSIGNMENT, ExecutionPathOptions.create() ); + this.argument.reassignPath( [], ExecutionPathOptions.create() ); } } diff --git a/src/ast/nodes/UpdateExpression.js b/src/ast/nodes/UpdateExpression.js index f0ba5d97bd7..283637b02fa 100644 --- a/src/ast/nodes/UpdateExpression.js +++ b/src/ast/nodes/UpdateExpression.js @@ -1,12 +1,11 @@ import Node from '../Node.js'; import disallowIllegalReassignment from './shared/disallowIllegalReassignment.js'; -import VirtualNumberLiteral from './shared/VirtualNumberLiteral'; import ExecutionPathOptions from '../ExecutionPathOptions'; export default class UpdateExpression extends Node { bindNode () { disallowIllegalReassignment( this.scope, this.argument ); - this.argument.bindAssignmentAtPath( [], new VirtualNumberLiteral(), ExecutionPathOptions.create() ); + this.argument.reassignPath( [], ExecutionPathOptions.create() ); if ( this.argument.type === 'Identifier' ) { const variable = this.scope.findVariable( this.argument.name ); variable.isReassigned = true; diff --git a/src/ast/nodes/VariableDeclaration.js b/src/ast/nodes/VariableDeclaration.js index 676a4820c5e..47b837ca6c9 100644 --- a/src/ast/nodes/VariableDeclaration.js +++ b/src/ast/nodes/VariableDeclaration.js @@ -1,6 +1,5 @@ import Node from '../Node.js'; import extractNames from '../utils/extractNames.js'; -import { UNKNOWN_ASSIGNMENT } from '../values'; import ExecutionPathOptions from '../ExecutionPathOptions'; function getSeparator ( code, start ) { @@ -19,8 +18,8 @@ function getSeparator ( code, start ) { const forStatement = /^For(?:Of|In)?Statement/; export default class VariableDeclaration extends Node { - bindAssignmentAtPath () { - this.eachChild( child => child.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT, ExecutionPathOptions.create() ) ); + reassignPath () { + this.eachChild( child => child.reassignPath( [], ExecutionPathOptions.create() ) ); } hasEffectsWhenAssignedAtPath () { diff --git a/src/ast/nodes/VariableDeclarator.js b/src/ast/nodes/VariableDeclarator.js index 8a7f913f587..7656a0535f0 100644 --- a/src/ast/nodes/VariableDeclarator.js +++ b/src/ast/nodes/VariableDeclarator.js @@ -2,8 +2,8 @@ import Node from '../Node.js'; import extractNames from '../utils/extractNames.js'; export default class VariableDeclarator extends Node { - bindAssignmentAtPath ( path, expression, options ) { - this.id.bindAssignmentAtPath( path, expression, options ); + reassignPath ( path, options ) { + this.id.reassignPath( path, options ); } initialiseDeclarator ( parentScope, kind ) { diff --git a/src/ast/nodes/shared/VirtualNumberLiteral.js b/src/ast/nodes/shared/VirtualNumberLiteral.js deleted file mode 100644 index cd04544aeb6..00000000000 --- a/src/ast/nodes/shared/VirtualNumberLiteral.js +++ /dev/null @@ -1,15 +0,0 @@ -import Node from '../../Node'; - -export default class VirtualNumberLiteral extends Node { - hasEffectsWhenAccessedAtPath ( path ) { - return path.length > 1; - } - - hasEffectsWhenAssignedAtPath ( path ) { - return path.length > 1; - } - - toString () { - return '[[VIRTUAL NUMBER]]'; - } -} diff --git a/src/ast/scopes/Scope.js b/src/ast/scopes/Scope.js index df470df17c2..7ea90001b63 100644 --- a/src/ast/scopes/Scope.js +++ b/src/ast/scopes/Scope.js @@ -2,6 +2,7 @@ import { blank, keys } from '../../utils/object.js'; import LocalVariable from '../variables/LocalVariable'; import ExportDefaultVariable from '../variables/ExportDefaultVariable'; import { UNDEFINED_ASSIGNMENT } from '../values'; +import ExecutionPathOptions from '../ExecutionPathOptions'; export default class Scope { constructor ( options = {} ) { @@ -26,7 +27,7 @@ export default class Scope { if ( this.variables[ name ] ) { const variable = this.variables[ name ]; variable.addDeclaration( identifier ); - options.init && variable.bindAssignmentAtPath( [], options.init ); + variable.reassignPath( [], ExecutionPathOptions.create() ); } else { this.variables[ name ] = new LocalVariable( identifier.name, identifier, options.init || UNDEFINED_ASSIGNMENT ); } diff --git a/src/ast/values.js b/src/ast/values.js index 2815eb227a4..c685d829306 100644 --- a/src/ast/values.js +++ b/src/ast/values.js @@ -2,7 +2,7 @@ export const UNKNOWN_VALUE = { toString: () => '[[UNKNOWN]]' }; export const UNKNOWN_ASSIGNMENT = { type: 'UNKNOWN', - bindAssignmentAtPath: () => {}, + reassignPath: () => {}, forEachReturnExpressionWhenCalledAtPath: () => {}, hasEffectsWhenAccessedAtPath: path => path.length > 0, hasEffectsWhenAssignedAtPath: path => path.length > 0, @@ -13,7 +13,7 @@ export const UNKNOWN_ASSIGNMENT = { export const UNDEFINED_ASSIGNMENT = { type: 'UNDEFINED', - bindAssignmentAtPath: () => {}, + reassignPath: () => {}, forEachReturnExpressionWhenCalledAtPath: () => {}, hasEffectsWhenAccessedAtPath: path => path.length > 0, hasEffectsWhenAssignedAtPath: path => path.length > 0, diff --git a/src/ast/variables/ArgumentsVariable.js b/src/ast/variables/ArgumentsVariable.js index 44a4f3b1a03..d006ef670b2 100644 --- a/src/ast/variables/ArgumentsVariable.js +++ b/src/ast/variables/ArgumentsVariable.js @@ -11,10 +11,10 @@ export default class ArgumentsVariable extends LocalVariable { this._parameters = parameters; } - bindAssignmentAtPath ( path, expression, options ) { + reassignPath ( path, options ) { if ( path.length > 0 ) { if ( path[ 0 ] >= 0 && this._parameters[ path[ 0 ] ] ) { - this._parameters[ path[ 0 ] ].bindAssignmentAtPath( path.slice( 1 ), expression, options ); + this._parameters[ path[ 0 ] ].reassignPath( path.slice( 1 ), options ); } } } diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index 32a98f74a23..ac2ce0020b4 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -1,8 +1,8 @@ import Variable from './Variable'; -import VariableShapeTracker from './VariableShapeTracker'; +import VariableReassignmentTracker from './VariableReassignmentTracker'; -// To avoid exponential performance degradation for complex object manipulations -const MAX_PATH_LENGTH = 2; +// To avoid infinite recursions +const MAX_PATH_LENGTH = 6; export default class LocalVariable extends Variable { constructor ( name, declarator, init ) { @@ -10,30 +10,21 @@ export default class LocalVariable extends Variable { this.isReassigned = false; this.exportName = null; this.declarations = new Set( declarator ? [ declarator ] : null ); - this.boundExpressions = new VariableShapeTracker(); - init && this.boundExpressions.addAtPath( [], init ); + this.boundExpressions = new VariableReassignmentTracker( init ); } addDeclaration ( identifier ) { this.declarations.add( identifier ); } - bindAssignmentAtPath ( path, expression, options ) { - if ( expression.variable ) { - expression = expression.variable; - } - if ( path.length > MAX_PATH_LENGTH || expression === this || this.boundExpressions.hasAtPath( path, expression ) ) return; - this.boundExpressions.addAtPath( path, expression ); - this.boundExpressions.forEachAssignedToPath( path, ( subPath, node ) => { - subPath.length > 0 - && expression.bindAssignmentAtPath( subPath, node, options ); - } ); - if ( path.length > 0 ) { - this.boundExpressions.forEachAtPath( path.slice( 0, -1 ), ( relativePath, node ) => - node.bindAssignmentAtPath( [ ...relativePath, ...path.slice( -1 ) ], expression, options ) ); - } else { + reassignPath ( path, options ) { + if ( path.length > MAX_PATH_LENGTH ) return; + if ( path.length === 0 ) { this.isReassigned = true; } + if ( !options.hasNodeBeenAssignedAtPath( path, this ) ) { + this.boundExpressions.reassignPath( path, options.addAssignedNodeAtPath( path, this ) ); + } } forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) { diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index 1c443a494eb..14bc230d5e4 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -15,13 +15,10 @@ export default class Variable { addReference ( identifier ) {} /** - * This enables variables to know which nodes need to be checked for side-effects when - * e.g. an object path is called or mutated. * @param {String[]} path - * @param {Node} expression * @param {ExecutionPathOptions} options */ - bindAssignmentAtPath ( path, expression, options ) {} + reassignPath ( path, options ) {} /** * @param {String[]} path diff --git a/src/ast/variables/VariableReassignmentTracker.js b/src/ast/variables/VariableReassignmentTracker.js new file mode 100644 index 00000000000..c075dac5783 --- /dev/null +++ b/src/ast/variables/VariableReassignmentTracker.js @@ -0,0 +1,79 @@ +import { UNKNOWN_ASSIGNMENT } from '../values'; + +export const UNKNOWN_KEY = { type: 'UNKNOWN_KEY' }; + +class ReassignedPathTracker { + constructor () { + this._reassigned = false; + this._unknownReassignedSubPath = false; + this._subPaths = new Map(); + } + + isReassigned ( path ) { + if ( path.length === 0 ) { + return this._reassigned; + } + const [ subPath, ...remainingPath ] = path; + return this._unknownReassignedSubPath || ( + this._subPaths.has( subPath ) && this._subPaths.get( subPath ).isReassigned( remainingPath ) + ); + } + + reassignPath ( path ) { + if ( this._reassigned ) return; + if ( path.length === 0 ) { + this._reassigned = true; + } else { + this._reassignSubPath( path ); + } + } + + _reassignSubPath ( path ) { + if ( this._unknownReassignedSubPath ) return; + const [ subPath, ...remainingPath ] = path; + if ( subPath === UNKNOWN_KEY ) { + this._unknownReassignedSubPath = true; + } else { + if ( !this._subPaths.has( subPath ) ) { + this._subPaths.set( subPath, new ReassignedPathTracker() ); + } + this._subPaths.get( subPath ).reassignPath( remainingPath ); + } + } + + someReassignedPath ( path, callback ) { + return this._reassigned + ? callback( path, UNKNOWN_ASSIGNMENT ) + : path.length >= 1 && this._onSubPathIfReassigned( path, callback ); + } + + _onSubPathIfReassigned ( path, callback ) { + const [ subPath, ...remainingPath ] = path; + return this._unknownReassignedSubPath || subPath === UNKNOWN_KEY + ? callback( remainingPath, UNKNOWN_ASSIGNMENT ) + : this._subPaths.has( subPath ) && this._subPaths.get( subPath ).someReassignedPath( remainingPath, callback ); + } +} + +export default class VariableReassignmentTracker { + constructor ( initialExpression ) { + this._initialExpression = initialExpression; + this._reassignedPathTracker = new ReassignedPathTracker(); + } + + reassignPath ( path, options ) { + if ( path.length > 0 ) { + this._initialExpression && this._initialExpression.reassignPath( path, options ); + } + this._reassignedPathTracker.reassignPath( path, options ); + } + + forEachAtPath ( path, callback ) { + this._initialExpression && callback( path, this._initialExpression ); + } + + someAtPath ( path, predicateFunction ) { + return this._reassignedPathTracker.someReassignedPath( path, predicateFunction ) + || (this._initialExpression && predicateFunction( path, this._initialExpression )); + } +} diff --git a/src/ast/variables/VariableShapeTracker.js b/src/ast/variables/VariableShapeTracker.js deleted file mode 100644 index 84e3b8951b3..00000000000 --- a/src/ast/variables/VariableShapeTracker.js +++ /dev/null @@ -1,96 +0,0 @@ -import { UNKNOWN_ASSIGNMENT } from '../values'; - -const SET_KEY = { type: 'SET_KEY' }; -export const UNKNOWN_KEY = { type: 'UNKNOWN_KEY' }; - -const UNKNOWN_ASSIGNMENTS = new Map( [ [ SET_KEY, new Set( [ UNKNOWN_ASSIGNMENT ] ) ] ] ); -const UNKNOWN_KEY_ASSIGNMENT = [ UNKNOWN_KEY, { toString: ( path = '' ) => path + '[[UNKNOWN_KEY]]' } ]; - -export default class VariableShapeTracker { - constructor () { - this._assignments = new Map( [ [ SET_KEY, new Set() ] ] ); - } - - addAtPath ( path, assignment ) { - if ( this._assignments === UNKNOWN_ASSIGNMENTS - || (path.length > 0 && this._assignments.has( UNKNOWN_KEY ) ) ) return; - if ( path.length === 0 && assignment === UNKNOWN_ASSIGNMENT ) { - this._assignments = UNKNOWN_ASSIGNMENTS; - } else if ( path[ 0 ] === UNKNOWN_KEY ) { - this._assignments = new Map( [ [ SET_KEY, this._assignments.get( SET_KEY ) ], UNKNOWN_KEY_ASSIGNMENT ] ); - } else if ( path.length === 0 ) { - this._assignments.get( SET_KEY ).add( assignment ); - } else { - const [ nextPath, ...remainingPath ] = path; - if ( !this._assignments.has( nextPath ) ) { - this._assignments.set( nextPath, new VariableShapeTracker() ); - } - this._assignments.get( nextPath ).addAtPath( remainingPath, assignment ); - } - } - - forEachAtPath ( path, callback ) { - const [ nextPath, ...remainingPath ] = path; - this._assignments.get( SET_KEY ).forEach( assignment => callback( path, assignment ) ); - if ( path.length > 0 - && nextPath !== UNKNOWN_KEY - && !this._assignments.has( UNKNOWN_KEY ) - && this._assignments.has( nextPath ) ) { - this._assignments.get( nextPath ).forEachAtPath( remainingPath, callback ); - } - } - - forEachAssignedToPath ( path, callback ) { - if ( this._assignments === UNKNOWN_ASSIGNMENTS || this._assignments.has( UNKNOWN_KEY ) ) return; - if ( path.length > 0 ) { - const [ nextPath, ...remainingPath ] = path; - if ( nextPath === UNKNOWN_KEY || this._assignments.has( UNKNOWN_KEY ) ) return; - if ( this._assignments.has( nextPath ) ) { - this._assignments.get( nextPath ).forEachAssignedToPath( remainingPath, callback ); - } - } else { - this._assignments.get( SET_KEY ).forEach( assignment => callback( [], assignment ) ); - this._assignments.forEach( ( assignment, subPath ) => { - if ( subPath !== SET_KEY ) { - assignment.forEachAssignedToPath( [], - ( relativePath, assignment ) => callback( [ subPath, ...relativePath ], assignment ) ); - } - } ); - } - } - - hasAtPath ( path, assignment ) { - if ( this._assignments === UNKNOWN_ASSIGNMENTS ) return true; - if ( path.length === 0 ) { - return this._assignments.get( SET_KEY ).has( assignment ); - } else { - if ( this._assignments.has( UNKNOWN_KEY ) ) return true; - const [ nextPath, ...remainingPath ] = path; - if ( !this._assignments.has( nextPath ) ) { - return false; - } - return this._assignments.get( nextPath ).hasAtPath( remainingPath, assignment ); - } - } - - someAtPath ( path, predicateFunction ) { - const [ nextPath, ...remainingPath ] = path; - return Array.from( this._assignments.get( SET_KEY ) ).some( assignment => predicateFunction( path, assignment ) ) - || ( - path.length > 0 - && ( - (nextPath === UNKNOWN_KEY || this._assignments.has( UNKNOWN_KEY ) - ? predicateFunction( remainingPath, UNKNOWN_ASSIGNMENT ) - : this._assignments.has( nextPath ) - && this._assignments.get( nextPath ).someAtPath( remainingPath, predicateFunction )) - ) - ); - } - - // For debugging purposes - toString ( pathString = '/' ) { - return Array.from( this._assignments ).map( ( [ subPath, subAssignment ] ) => subPath === SET_KEY - ? Array.from( subAssignment ).map( assignment => pathString + assignment.toString() ).join( '\n' ) - : subAssignment.toString( pathString + subPath + ': ' ) ).join( '\n' ); - } -} diff --git a/test/form/samples/conditional-expression-paths/_expected/amd.js b/test/form/samples/conditional-expression-paths/_expected/amd.js index d9eda46ad22..08c33da808a 100644 --- a/test/form/samples/conditional-expression-paths/_expected/amd.js +++ b/test/form/samples/conditional-expression-paths/_expected/amd.js @@ -7,7 +7,6 @@ define(function () { 'use strict'; var bar2 = { x: () => console.log( 'effect' ) }; var a2 = (unknownValue ? foo2 : bar2).x.y.z; var b2 = (unknownValue ? foo2 : bar2).x(); - var c2 = (unknownValue ? foo2 : bar2).x = () => console.log( 'effect' ); foo2.x(); bar2.x(); diff --git a/test/form/samples/conditional-expression-paths/_expected/cjs.js b/test/form/samples/conditional-expression-paths/_expected/cjs.js index 9c6e7fb0c25..22e029faef2 100644 --- a/test/form/samples/conditional-expression-paths/_expected/cjs.js +++ b/test/form/samples/conditional-expression-paths/_expected/cjs.js @@ -7,7 +7,6 @@ var foo2 = { x: () => {} }; var bar2 = { x: () => console.log( 'effect' ) }; var a2 = (unknownValue ? foo2 : bar2).x.y.z; var b2 = (unknownValue ? foo2 : bar2).x(); -var c2 = (unknownValue ? foo2 : bar2).x = () => console.log( 'effect' ); foo2.x(); bar2.x(); diff --git a/test/form/samples/conditional-expression-paths/_expected/es.js b/test/form/samples/conditional-expression-paths/_expected/es.js index 2863cebd2b0..849aba2e815 100644 --- a/test/form/samples/conditional-expression-paths/_expected/es.js +++ b/test/form/samples/conditional-expression-paths/_expected/es.js @@ -5,7 +5,6 @@ var foo2 = { x: () => {} }; var bar2 = { x: () => console.log( 'effect' ) }; var a2 = (unknownValue ? foo2 : bar2).x.y.z; var b2 = (unknownValue ? foo2 : bar2).x(); -var c2 = (unknownValue ? foo2 : bar2).x = () => console.log( 'effect' ); foo2.x(); bar2.x(); diff --git a/test/form/samples/conditional-expression-paths/_expected/iife.js b/test/form/samples/conditional-expression-paths/_expected/iife.js index d82e880c3de..010662155e5 100644 --- a/test/form/samples/conditional-expression-paths/_expected/iife.js +++ b/test/form/samples/conditional-expression-paths/_expected/iife.js @@ -8,7 +8,6 @@ var bar2 = { x: () => console.log( 'effect' ) }; var a2 = (unknownValue ? foo2 : bar2).x.y.z; var b2 = (unknownValue ? foo2 : bar2).x(); - var c2 = (unknownValue ? foo2 : bar2).x = () => console.log( 'effect' ); foo2.x(); bar2.x(); diff --git a/test/form/samples/conditional-expression-paths/_expected/umd.js b/test/form/samples/conditional-expression-paths/_expected/umd.js index c5d7483058c..0e29c610ba8 100644 --- a/test/form/samples/conditional-expression-paths/_expected/umd.js +++ b/test/form/samples/conditional-expression-paths/_expected/umd.js @@ -11,7 +11,6 @@ var bar2 = { x: () => console.log( 'effect' ) }; var a2 = (unknownValue ? foo2 : bar2).x.y.z; var b2 = (unknownValue ? foo2 : bar2).x(); - var c2 = (unknownValue ? foo2 : bar2).x = () => console.log( 'effect' ); foo2.x(); bar2.x(); diff --git a/test/form/samples/conditional-expression-paths/main.js b/test/form/samples/conditional-expression-paths/main.js index ef824ad3f96..0215e2d07e2 100644 --- a/test/form/samples/conditional-expression-paths/main.js +++ b/test/form/samples/conditional-expression-paths/main.js @@ -5,7 +5,6 @@ var foo1 = { x: () => {} }; var bar1 = { x: () => {} }; var a1 = (unknownValue ? foo1 : bar1).x.y; var b1 = (unknownValue ? foo1 : bar1).x(); -var c1 = (unknownValue ? foo1 : bar1).x = () => {}; foo1.x(); bar1.x(); @@ -14,7 +13,6 @@ var foo2 = { x: () => {} }; var bar2 = { x: () => console.log( 'effect' ) }; var a2 = (unknownValue ? foo2 : bar2).x.y.z; var b2 = (unknownValue ? foo2 : bar2).x(); -var c2 = (unknownValue ? foo2 : bar2).x = () => console.log( 'effect' ); foo2.x(); bar2.x(); diff --git a/test/form/samples/cyclic-assignments/_expected/amd.js b/test/form/samples/cyclic-assignments/_expected/amd.js index f9f8229aa40..6838b8b1a0b 100644 --- a/test/form/samples/cyclic-assignments/_expected/amd.js +++ b/test/form/samples/cyclic-assignments/_expected/amd.js @@ -1,5 +1,14 @@ define(function () { 'use strict'; + let a = { foo: () => {}, bar: () => () => {} }; + let b = a; + a = b; + a.foo = () => {}; + a.foo(); + b = b; + b.bar = () => () => {}; + + b.bar()(); }); diff --git a/test/form/samples/cyclic-assignments/_expected/cjs.js b/test/form/samples/cyclic-assignments/_expected/cjs.js index eb109abbed0..7540cd3825e 100644 --- a/test/form/samples/cyclic-assignments/_expected/cjs.js +++ b/test/form/samples/cyclic-assignments/_expected/cjs.js @@ -1,2 +1,12 @@ 'use strict'; +let a = { foo: () => {}, bar: () => () => {} }; +let b = a; +a = b; +a.foo = () => {}; +a.foo(); + +b = b; +b.bar = () => () => {}; + +b.bar()(); diff --git a/test/form/samples/cyclic-assignments/_expected/es.js b/test/form/samples/cyclic-assignments/_expected/es.js index 8b137891791..943b5a6f9ab 100644 --- a/test/form/samples/cyclic-assignments/_expected/es.js +++ b/test/form/samples/cyclic-assignments/_expected/es.js @@ -1 +1,10 @@ +let a = { foo: () => {}, bar: () => () => {} }; +let b = a; +a = b; +a.foo = () => {}; +a.foo(); +b = b; +b.bar = () => () => {}; + +b.bar()(); diff --git a/test/form/samples/cyclic-assignments/_expected/iife.js b/test/form/samples/cyclic-assignments/_expected/iife.js index 43ef5426880..062a0cea0e2 100644 --- a/test/form/samples/cyclic-assignments/_expected/iife.js +++ b/test/form/samples/cyclic-assignments/_expected/iife.js @@ -1,6 +1,15 @@ (function () { 'use strict'; + let a = { foo: () => {}, bar: () => () => {} }; + let b = a; + a = b; + a.foo = () => {}; + a.foo(); + b = b; + b.bar = () => () => {}; + + b.bar()(); }()); diff --git a/test/form/samples/cyclic-assignments/_expected/umd.js b/test/form/samples/cyclic-assignments/_expected/umd.js index 07ce27e42f1..4882a13c2c7 100644 --- a/test/form/samples/cyclic-assignments/_expected/umd.js +++ b/test/form/samples/cyclic-assignments/_expected/umd.js @@ -4,6 +4,15 @@ (factory()); }(this, (function () { 'use strict'; + let a = { foo: () => {}, bar: () => () => {} }; + let b = a; + a = b; + a.foo = () => {}; + a.foo(); + b = b; + b.bar = () => () => {}; + + b.bar()(); }))); diff --git a/test/form/samples/side-effects-object-literal-calls/main.js b/test/form/samples/side-effects-object-literal-calls/main.js index 82c05d7d7ad..5f7c1d32a3d 100644 --- a/test/form/samples/side-effects-object-literal-calls/main.js +++ b/test/form/samples/side-effects-object-literal-calls/main.js @@ -2,7 +2,6 @@ const removed1 = { x: () => {} }; removed1.x(); const removed2 = { x: { y: () => {} } }; -removed2.x.y = function () {}; removed2.x.y(); const retained1 = { x: () => {} }; diff --git a/test/form/samples/side-effects-object-literal-mutation/main.js b/test/form/samples/side-effects-object-literal-mutation/main.js index d8017609bf5..115a8386993 100644 --- a/test/form/samples/side-effects-object-literal-mutation/main.js +++ b/test/form/samples/side-effects-object-literal-mutation/main.js @@ -2,6 +2,9 @@ const removed1 = { x: {} }; removed1.y = 1; removed1.x.y = 2; +const removed2 = { x: 99 }; +removed2.x.y = 2; + export const retained1 = { x: {} }; retained1.y = 1; retained1.x.y = 2; @@ -21,10 +24,6 @@ const retained5 = { x: {} }; retained5.x = null; retained5.x.y = 2; -const removed2 = { x: {} }; -removed2.x = 99; -removed2.x.y = 2; - const retained6 = { x: { y: {} } }; retained6.x = {}; retained6.x.y.z = 3; diff --git a/test/form/samples/side-effects-reassignment/_expected/amd.js b/test/form/samples/side-effects-reassignment/_expected/amd.js index 8413149572d..1056781e7d0 100644 --- a/test/form/samples/side-effects-reassignment/_expected/amd.js +++ b/test/form/samples/side-effects-reassignment/_expected/amd.js @@ -1,12 +1,10 @@ define(function () { 'use strict'; - var effect = () => {}; - effect = function() { + var effect = function() { console.log('effect'); }; - var alsoEffect = () => {}; - alsoEffect = effect; + var alsoEffect = effect; alsoEffect(); }); diff --git a/test/form/samples/side-effects-reassignment/_expected/cjs.js b/test/form/samples/side-effects-reassignment/_expected/cjs.js index e246be25b57..a6efcd3596e 100644 --- a/test/form/samples/side-effects-reassignment/_expected/cjs.js +++ b/test/form/samples/side-effects-reassignment/_expected/cjs.js @@ -1,10 +1,8 @@ 'use strict'; -var effect = () => {}; -effect = function() { +var effect = function() { console.log('effect'); }; -var alsoEffect = () => {}; -alsoEffect = effect; +var alsoEffect = effect; alsoEffect(); diff --git a/test/form/samples/side-effects-reassignment/_expected/es.js b/test/form/samples/side-effects-reassignment/_expected/es.js index 634177515de..eb567ddc6ab 100644 --- a/test/form/samples/side-effects-reassignment/_expected/es.js +++ b/test/form/samples/side-effects-reassignment/_expected/es.js @@ -1,8 +1,6 @@ -var effect = () => {}; -effect = function() { +var effect = function() { console.log('effect'); }; -var alsoEffect = () => {}; -alsoEffect = effect; +var alsoEffect = effect; alsoEffect(); diff --git a/test/form/samples/side-effects-reassignment/_expected/iife.js b/test/form/samples/side-effects-reassignment/_expected/iife.js index e09f12b1360..2d21989b418 100644 --- a/test/form/samples/side-effects-reassignment/_expected/iife.js +++ b/test/form/samples/side-effects-reassignment/_expected/iife.js @@ -1,13 +1,11 @@ (function () { 'use strict'; - var effect = () => {}; - effect = function() { + var effect = function() { console.log('effect'); }; - var alsoEffect = () => {}; - alsoEffect = effect; + var alsoEffect = effect; alsoEffect(); }()); diff --git a/test/form/samples/side-effects-reassignment/_expected/umd.js b/test/form/samples/side-effects-reassignment/_expected/umd.js index f6450848e1f..da0c3fe9ef8 100644 --- a/test/form/samples/side-effects-reassignment/_expected/umd.js +++ b/test/form/samples/side-effects-reassignment/_expected/umd.js @@ -4,13 +4,11 @@ (factory()); }(this, (function () { 'use strict'; - var effect = () => {}; - effect = function() { + var effect = function() { console.log('effect'); }; - var alsoEffect = () => {}; - alsoEffect = effect; + var alsoEffect = effect; alsoEffect(); }))); diff --git a/test/form/samples/side-effects-reassignment/main.js b/test/form/samples/side-effects-reassignment/main.js index 1013e127833..602206f9ba5 100644 --- a/test/form/samples/side-effects-reassignment/main.js +++ b/test/form/samples/side-effects-reassignment/main.js @@ -7,20 +7,16 @@ foo = function () { foo = ['foo']; foo = undefined; -var noEffect = () => {}; -noEffect = function(a) { +var noEffect = function(a) { a = 'reassigned parameter'; }; -var stillNoEffect = () => {}; -stillNoEffect = noEffect; +var stillNoEffect = noEffect; stillNoEffect(); -var effect = () => {}; -effect = function() { +var effect = function() { console.log('effect'); }; -var alsoEffect = () => {}; -alsoEffect = effect; +var alsoEffect = effect; alsoEffect();