diff --git a/src/ast/CallOptions.js b/src/ast/CallOptions.js index d01381d28f2..dc6f1179c23 100644 --- a/src/ast/CallOptions.js +++ b/src/ast/CallOptions.js @@ -1,29 +1,15 @@ -import Immutable from 'immutable'; - -const RESULT_KEY = {}; - export default class CallOptions { static create ( callOptions ) { - return new this( callOptions, Immutable.Map() ); + return new this( callOptions ); } - constructor ( { withNew = false, args = [] } = {}, nodesCalledAtPath ) { + constructor ( { withNew = false, args = [], caller } = {} ) { this.withNew = withNew; this.args = args; - this._nodesCalledAtPath = nodesCalledAtPath; - } - - addCalledNodeAtPath ( path, node ) { - return new this.constructor( this, this._nodesCalledAtPath.setIn( [ node, ...path, RESULT_KEY ], true ) ); + this.caller = caller; } equals ( callOptions ) { - return this.withNew === callOptions.withNew - && this.args.length === callOptions.args.length - && this.args.every( ( parameter, index ) => parameter === callOptions.args[ index ] ); - } - - hasNodeBeenCalledAtPath ( path, node ) { - return this._nodesCalledAtPath.getIn( [ node, ...path, RESULT_KEY ] ); + return callOptions && this.caller === callOptions.caller; } } diff --git a/src/ast/Node.js b/src/ast/Node.js index 26ba86833d0..6a87b511032 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -19,19 +19,6 @@ export default class Node { this.bindNode(); } - /** - * Override this to bind assignments to variables and do any initialisations that - * require the scopes to be populated with variables. - */ - bindNode () {} - - /** - * Override to control on which children "bind" is called. - */ - bindChildren () { - this.eachChild( child => child.bind() ); - } - /** * Bind an expression as an assignment to a node given a path. * E.g., node.bindAssignmentAtPath(['x', 'y'], otherNode) is called when otherNode @@ -40,16 +27,31 @@ export default class Node { * always returns true for this node. Otherwise it should be overridden. * @param {String[]} path * @param {Node} expression + * @param {ExecutionPathOptions} options */ - bindAssignmentAtPath ( path, expression ) {} + bindAssignmentAtPath ( path, expression, options ) {} /** * Binds the arguments a node is called with to this node and possibly its parameters. * Should usually be overridden together with hasEffectsWhenCalled. * @param {String[]} path * @param {CallOptions} callOptions + * @param {ExecutionPathOptions} options */ - bindCallAtPath ( path, callOptions ) {} + bindCallAtPath ( path, callOptions, options ) {} + + /** + * Override to control on which children "bind" is called. + */ + bindChildren () { + this.eachChild( child => child.bind() ); + } + + /** + * Override this to bind assignments to variables and do any initialisations that + * require the scopes to be populated with variables. + */ + bindNode () {} eachChild ( callback ) { this.keys.forEach( key => { @@ -69,8 +71,9 @@ export default class Node { * @param {String[]} path * @param {CallOptions} callOptions * @param {Function} callback + * @param {ExecutionPathOptions} options */ - forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) {} + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) {} getValue () { return UNKNOWN_VALUE; diff --git a/src/ast/nodes/ArrayPattern.js b/src/ast/nodes/ArrayPattern.js index 5891f7bccb4..8c6437e4d01 100644 --- a/src/ast/nodes/ArrayPattern.js +++ b/src/ast/nodes/ArrayPattern.js @@ -2,15 +2,14 @@ import Node from '../Node.js'; import { UNKNOWN_ASSIGNMENT } from '../values'; export default class ArrayPattern extends Node { - bindAssignmentAtPath () { - this.eachChild( child => child.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT ) ); + bindAssignmentAtPath ( path, expression, options ) { + path.length === 0 + && this.eachChild( child => child.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT, options ) ); } hasEffectsWhenAssignedAtPath ( path, options ) { - if ( path.length > 0 ) { - return true; - } - return this.someChild( child => child.hasEffectsWhenAssignedAtPath( [], options ) ); + return path.length > 0 + || this.someChild( child => child.hasEffectsWhenAssignedAtPath( [], options ) ); } initialiseAndDeclare ( parentScope, kind ) { diff --git a/src/ast/nodes/ArrowFunctionExpression.js b/src/ast/nodes/ArrowFunctionExpression.js index aca439ccb6f..43f77b5174a 100644 --- a/src/ast/nodes/ArrowFunctionExpression.js +++ b/src/ast/nodes/ArrowFunctionExpression.js @@ -4,9 +4,8 @@ import ReturnValueScope from '../scopes/ReturnValueScope'; export default class ArrowFunctionExpression extends Node { bindCallAtPath ( path, { args } ) { - if ( path.length === 0 ) { - this.scope.bindCallArguments( args ); - } + path.length === 0 + && this.scope.bindCallArguments( args ); } bindNode () { @@ -15,10 +14,9 @@ export default class ArrowFunctionExpression extends Node { : this.scope.addReturnExpression( this.body ); } - forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { - if ( path.length === 0 ) { - this.scope.forEachReturnExpressionWhenCalled( callback ); - } + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) { + path.length === 0 + && this.scope.forEachReturnExpressionWhenCalled( callOptions, callback, options ); } hasEffects () { diff --git a/src/ast/nodes/AssignmentExpression.js b/src/ast/nodes/AssignmentExpression.js index 5db827c55de..6cab3439f4e 100644 --- a/src/ast/nodes/AssignmentExpression.js +++ b/src/ast/nodes/AssignmentExpression.js @@ -1,10 +1,11 @@ import Node from '../Node.js'; import disallowIllegalReassignment from './shared/disallowIllegalReassignment.js'; +import ExecutionPathOptions from '../ExecutionPathOptions'; export default class AssignmentExpression extends Node { bindNode () { disallowIllegalReassignment( this.scope, this.left ); - this.left.bindAssignmentAtPath( [], this.right ); + this.left.bindAssignmentAtPath( [], this.right, ExecutionPathOptions.create() ); } hasEffects ( options ) { diff --git a/src/ast/nodes/AssignmentPattern.js b/src/ast/nodes/AssignmentPattern.js index df02e252140..eb3df0e338e 100644 --- a/src/ast/nodes/AssignmentPattern.js +++ b/src/ast/nodes/AssignmentPattern.js @@ -1,19 +1,19 @@ import Node from '../Node.js'; +import ExecutionPathOptions from '../ExecutionPathOptions'; export default class AssignmentPattern extends Node { bindNode () { - this.left.bindAssignmentAtPath( [], this.right ); + this.left.bindAssignmentAtPath( [], this.right, ExecutionPathOptions.create() ); } - bindAssignmentAtPath ( path, expression ) { - this.left.bindAssignmentAtPath( path, expression ); + bindAssignmentAtPath ( path, expression, options ) { + path.length === 0 + && this.left.bindAssignmentAtPath( path, expression, options ); } hasEffectsWhenAssignedAtPath ( path, options ) { - if ( path.length > 0 ) { - return true; - } - return this.left.hasEffectsWhenAssignedAtPath( [], options ); + return path.length > 0 + || this.left.hasEffectsWhenAssignedAtPath( [], options ); } initialiseAndDeclare ( parentScope, kind, init ) { diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index e7e76fcfec4..189c5f7dde7 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -1,25 +1,18 @@ import Node from '../Node.js'; import CallOptions from '../CallOptions'; -import StructuredAssignmentTracker from '../variables/StructuredAssignmentTracker'; +import ExecutionPathOptions from '../ExecutionPathOptions'; export default class CallExpression extends Node { - bindAssignmentAtPath ( path, expression ) { - if ( this._boundExpressions.hasAtPath( path, expression ) ) return; - this._boundExpressions.addAtPath( path, expression ); - this.callee.forEachReturnExpressionWhenCalledAtPath( [], this._callOptions, - node => node.bindAssignmentAtPath( path, expression ) ); + bindAssignmentAtPath ( path, expression, options ) { + !options.hasReturnExpressionBeenAssignedAtPath( path, this ) + && this.callee.forEachReturnExpressionWhenCalledAtPath( [], this._callOptions, innerOptions => node => + node.bindAssignmentAtPath( path, expression, innerOptions.addAssignedReturnExpressionAtPath( path, this ) ), options ); } - bindCallAtPath ( path, callOptions ) { - if ( this._boundCalls.hasAtPath( path, callOptions ) ) return; - this._boundCalls.addAtPath( path, callOptions ); - this.callee.forEachReturnExpressionWhenCalledAtPath( [], this._callOptions, - node => node.bindCallAtPath( path, callOptions ) ); - } - - forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { - this.callee.forEachReturnExpressionWhenCalledAtPath( [], this._callOptions, - node => node.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback ) ); + bindCallAtPath ( path, callOptions, options ) { + !options.hasReturnExpressionBeenCalledAtPath( path, this ) + && this.callee.forEachReturnExpressionWhenCalledAtPath( [], this._callOptions, innerOptions => node => + node.bindCallAtPath( path, callOptions, innerOptions.addCalledReturnExpressionAtPath( path, this ) ), options ); } bindNode () { @@ -41,7 +34,12 @@ export default class CallExpression extends Node { }, this.start ); } } - this.callee.bindCallAtPath( [], this._callOptions ); + this.callee.bindCallAtPath( [], this._callOptions, ExecutionPathOptions.create() ); + } + + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) { + this.callee.forEachReturnExpressionWhenCalledAtPath( [], this._callOptions, innerOptions => node => + node.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback, innerOptions ), options ); } hasEffects ( options ) { @@ -69,10 +67,7 @@ export default class CallExpression extends Node { } initialiseNode () { - this._callOptions = CallOptions.create( { withNew: false, args: this.arguments } ); - // To avoid infinite recursions when dealing with recursive functions - this._boundExpressions = new StructuredAssignmentTracker(); - this._boundCalls = new StructuredAssignmentTracker(); + this._callOptions = CallOptions.create( { withNew: false, args: this.arguments, caller: this } ); } someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { diff --git a/src/ast/nodes/ClassBody.js b/src/ast/nodes/ClassBody.js index d1e6e49ef16..6ece0dd9644 100644 --- a/src/ast/nodes/ClassBody.js +++ b/src/ast/nodes/ClassBody.js @@ -1,20 +1,18 @@ import Node from '../Node'; export default class ClassBody extends Node { - bindCallAtPath ( path, callOptions ) { - if ( path.length === 0 && this.classConstructor ) { - this.classConstructor.bindCallAtPath( path, callOptions ); - } + bindCallAtPath ( path, callOptions, options ) { + path.length === 0 + && this.classConstructor + && this.classConstructor.bindCallAtPath( path, callOptions, options ); } hasEffectsWhenCalledAtPath ( path, callOptions, options ) { if ( path.length > 0 ) { return true; } - if ( this.classConstructor ) { - return this.classConstructor.hasEffectsWhenCalledAtPath( [], callOptions, options ); - } - return false; + return this.classConstructor + && this.classConstructor.hasEffectsWhenCalledAtPath( [], callOptions, options ); } initialiseNode () { diff --git a/src/ast/nodes/ConditionalExpression.js b/src/ast/nodes/ConditionalExpression.js index 085e65aed1d..1dc2b3af395 100644 --- a/src/ast/nodes/ConditionalExpression.js +++ b/src/ast/nodes/ConditionalExpression.js @@ -2,18 +2,17 @@ import Node from '../Node.js'; import { UNKNOWN_VALUE } from '../values.js'; export default class ConditionalExpression extends Node { - bindAssignmentAtPath ( path, expression ) { - if ( path.length > 0 ) { - this._forEachRelevantBranch( node => node.bindAssignmentAtPath( path, expression ) ); - } + bindAssignmentAtPath ( path, expression, options ) { + path.length > 0 + && this._forEachRelevantBranch( node => node.bindAssignmentAtPath( path, expression, options ) ); } - bindCallAtPath ( path, callOptions ) { - this._forEachRelevantBranch( node => node.bindCallAtPath( path, callOptions ) ); + bindCallAtPath ( path, callOptions, options ) { + this._forEachRelevantBranch( node => node.bindCallAtPath( path, callOptions, options ) ); } - forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { - this._forEachRelevantBranch( node => node.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback ) ); + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) { + this._forEachRelevantBranch( node => node.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback, options ) ); } getValue () { @@ -32,18 +31,16 @@ export default class ConditionalExpression extends Node { hasEffectsWhenAccessedAtPath ( path, options ) { return path.length > 0 - && this._someRelevantBranch( node => - node.hasEffectsWhenAccessedAtPath( path, options ) ); + && this._someRelevantBranch( node => node.hasEffectsWhenAccessedAtPath( path, options ) ); } hasEffectsWhenAssignedAtPath ( path, options ) { - return this._someRelevantBranch( node => - node.hasEffectsWhenAssignedAtPath( path, options ) ); + return path.length === 0 + || this._someRelevantBranch( node => node.hasEffectsWhenAssignedAtPath( path, options ) ); } hasEffectsWhenCalledAtPath ( path, callOptions, options ) { - return this._someRelevantBranch( node => - node.hasEffectsWhenCalledAtPath( path, callOptions, options ) ); + return this._someRelevantBranch( node => node.hasEffectsWhenCalledAtPath( path, callOptions, options ) ); } initialiseChildren ( parentScope ) { diff --git a/src/ast/nodes/ForOfStatement.js b/src/ast/nodes/ForOfStatement.js index 5445647ba99..683d8a3882a 100644 --- a/src/ast/nodes/ForOfStatement.js +++ b/src/ast/nodes/ForOfStatement.js @@ -1,10 +1,11 @@ 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 ); + this.left.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT, ExecutionPathOptions.create() ); } hasEffects ( options ) { diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index afe3c18f7dc..b256d56ba6f 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -3,18 +3,16 @@ import isReference from 'is-reference'; import { UNKNOWN_ASSIGNMENT } from '../values'; export default class Identifier extends Node { - bindAssignmentAtPath ( path, expression ) { + bindAssignmentAtPath ( path, expression, options ) { this._bindVariableIfMissing(); - if ( this.variable ) { - this.variable.bindAssignmentAtPath( path, expression ); - } + this.variable + && this.variable.bindAssignmentAtPath( path, expression, options ); } - bindCallAtPath ( path, callOptions ) { + bindCallAtPath ( path, callOptions, options ) { this._bindVariableIfMissing(); - if ( this.variable ) { - this.variable.bindCallAtPath( path, callOptions ); - } + this.variable + && this.variable.bindCallAtPath( path, callOptions, options ); } bindNode () { @@ -28,11 +26,10 @@ export default class Identifier extends Node { } } - forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) { this._bindVariableIfMissing(); - if ( this.variable ) { - this.variable.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback ); - } + this.variable + && this.variable.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback, options ); } hasEffectsWhenAccessedAtPath ( path, options ) { diff --git a/src/ast/nodes/LogicalExpression.js b/src/ast/nodes/LogicalExpression.js index feead3c619c..fd66e15e6de 100644 --- a/src/ast/nodes/LogicalExpression.js +++ b/src/ast/nodes/LogicalExpression.js @@ -2,18 +2,17 @@ import Node from '../Node.js'; import { UNKNOWN_VALUE } from '../values.js'; export default class LogicalExpression extends Node { - bindAssignmentAtPath ( path, expression ) { - if ( path.length > 0 ) { - this._forEachRelevantBranch( node => node.bindAssignmentAtPath( path, expression ) ); - } + bindAssignmentAtPath ( path, expression, options ) { + path.length > 0 + && this._forEachRelevantBranch( node => node.bindAssignmentAtPath( path, expression, options ) ); } - bindCallAtPath ( path, callOptions ) { - this._forEachRelevantBranch( node => node.bindCallAtPath( path, callOptions ) ); + bindCallAtPath ( path, callOptions, options ) { + this._forEachRelevantBranch( node => node.bindCallAtPath( path, callOptions, options ) ); } - forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { - this._forEachRelevantBranch( node => node.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback ) ); + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) { + this._forEachRelevantBranch( node => node.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback, options ) ); } getValue () { @@ -43,8 +42,8 @@ export default class LogicalExpression extends Node { } hasEffectsWhenAssignedAtPath ( path, options ) { - return this._someRelevantBranch( node => - node.hasEffectsWhenAssignedAtPath( path, options ) ); + return path.length === 0 + || this._someRelevantBranch( node => node.hasEffectsWhenAssignedAtPath( path, options ) ); } hasEffectsWhenCalledAtPath ( path, callOptions, options ) { diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index bfba8c2bad2..a781f75f60a 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -76,30 +76,30 @@ export default class MemberExpression extends Node { } } - bindAssignmentAtPath ( path, expression ) { + bindAssignmentAtPath ( path, expression, options ) { if ( !this._bound ) this.bind(); if ( this.variable ) { - this.variable.bindAssignmentAtPath( path, expression ); + this.variable.bindAssignmentAtPath( path, expression, options ); } else { - this.object.bindAssignmentAtPath( [ this._getPathSegment(), ...path ], expression ); + this.object.bindAssignmentAtPath( [ this._getPathSegment(), ...path ], expression, options ); } } - bindCallAtPath ( path, callOptions ) { + bindCallAtPath ( path, callOptions, options ) { if ( !this._bound ) this.bind(); if ( this.variable ) { - this.variable.bindCallAtPath( path, callOptions ); + this.variable.bindCallAtPath( path, callOptions, options ); } else { - this.object.bindCallAtPath( [ this._getPathSegment(), ...path ], callOptions ); + this.object.bindCallAtPath( [ this._getPathSegment(), ...path ], callOptions, options ); } } - forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) { if ( !this._bound ) this.bind(); if ( this.variable ) { - this.variable.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback ); + this.variable.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback, options ); } else { - this.object.forEachReturnExpressionWhenCalledAtPath( [ this._getPathSegment(), ...path ], callOptions, callback ); + this.object.forEachReturnExpressionWhenCalledAtPath( [ this._getPathSegment(), ...path ], callOptions, callback, options ); } } diff --git a/src/ast/nodes/MethodDefinition.js b/src/ast/nodes/MethodDefinition.js index a856621797a..f192f1f1b66 100644 --- a/src/ast/nodes/MethodDefinition.js +++ b/src/ast/nodes/MethodDefinition.js @@ -1,10 +1,9 @@ import Node from '../Node'; export default class MethodDefinition extends Node { - bindCallAtPath ( path, callOptions ) { - if ( path.length === 0 ) { - this.value.bindCallAtPath( path, callOptions ); - } + bindCallAtPath ( path, callOptions, options ) { + path.length === 0 + && this.value.bindCallAtPath( path, callOptions, options ); } hasEffects ( options ) { diff --git a/src/ast/nodes/NewExpression.js b/src/ast/nodes/NewExpression.js index e538abcd36a..4717bca4213 100644 --- a/src/ast/nodes/NewExpression.js +++ b/src/ast/nodes/NewExpression.js @@ -1,9 +1,10 @@ import Node from '../Node.js'; import CallOptions from '../CallOptions'; +import ExecutionPathOptions from '../ExecutionPathOptions'; export default class NewExpression extends Node { bindNode () { - this.callee.bindCallAtPath( [], this._callOptions ); + this.callee.bindCallAtPath( [], this._callOptions, ExecutionPathOptions.create() ); } hasEffects ( options ) { @@ -16,6 +17,6 @@ export default class NewExpression extends Node { } initialiseNode () { - this._callOptions = CallOptions.create( { withNew: true, args: this.arguments } ); + this._callOptions = CallOptions.create( { withNew: true, args: this.arguments, caller: this } ); } } diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index db0f1b7ad02..901b307b011 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -5,26 +5,30 @@ const PROPERTY_KINDS_READ = [ 'init', 'get' ]; const PROPERTY_KINDS_WRITE = [ 'init', 'set' ]; export default class ObjectExpression extends Node { - bindAssignmentAtPath ( path, expression ) { + bindAssignmentAtPath ( path, expression, options ) { if ( path.length === 0 ) return; - this._getPossiblePropertiesWithName( - path[ 0 ], path.length === 1 ? PROPERTY_KINDS_WRITE : PROPERTY_KINDS_READ ) - .properties.forEach( property => property.bindAssignmentAtPath( path.slice( 1 ), expression ) ); + 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 ) ); } - bindCallAtPath ( path, callOptions ) { + bindCallAtPath ( path, callOptions, options ) { if ( path.length === 0 ) return; - this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_READ ).properties.forEach( property => - property.bindCallAtPath( path.slice( 1 ), callOptions ) ); + const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_READ ); + hasCertainHit && properties.forEach( property => + property.bindCallAtPath( path.slice( 1 ), callOptions, options ) ); } - forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) { if ( path.length === 0 ) return; - this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_READ ).properties.forEach( property => - property.forEachReturnExpressionWhenCalledAtPath( path.slice( 1 ), callOptions, callback ) ); + const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_READ ); + hasCertainHit && properties.forEach( property => + property.forEachReturnExpressionWhenCalledAtPath( path.slice( 1 ), callOptions, callback, options ) ); } _getPossiblePropertiesWithName ( name, kinds ) { diff --git a/src/ast/nodes/ObjectPattern.js b/src/ast/nodes/ObjectPattern.js index 0ce95596ea1..2aea456d45c 100644 --- a/src/ast/nodes/ObjectPattern.js +++ b/src/ast/nodes/ObjectPattern.js @@ -1,15 +1,14 @@ import Node from '../Node.js'; export default class ObjectPattern extends Node { - bindAssignmentAtPath ( path, expression ) { - this.properties.forEach( child => child.bindAssignmentAtPath( path, expression ) ); + bindAssignmentAtPath ( path, expression, options ) { + path.length === 0 + && this.properties.forEach( child => child.bindAssignmentAtPath( path, expression, options ) ); } hasEffectsWhenAssignedAtPath ( path, options ) { - if ( path.length > 0 ) { - return true; - } - return this.someChild( child => child.hasEffectsWhenAssignedAtPath( [], options ) ); + return path.length > 0 + || this.someChild( child => child.hasEffectsWhenAssignedAtPath( [], options ) ); } initialiseAndDeclare ( parentScope, kind, init ) { diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index d844018096d..af37a8c341e 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -3,32 +3,37 @@ import CallOptions from '../CallOptions'; import { UNKNOWN_ASSIGNMENT } from '../values'; export default class Property extends Node { - bindAssignmentAtPath ( path, expression ) { + bindAssignmentAtPath ( path, expression, options ) { if ( this.kind === 'get' ) { - path.length > 0 && this.value.forEachReturnExpressionWhenCalledAtPath( [], this._accessorCallOptions, - node => node.bindAssignmentAtPath( path, expression ) ); + path.length > 0 + && this.value.forEachReturnExpressionWhenCalledAtPath( [], this._accessorCallOptions, innerOptions => node => + node.bindAssignmentAtPath( path, expression, innerOptions.addAssignedReturnExpressionAtPath( path, this ) ), options ); } else if ( this.kind === 'set' ) { - path.length === 0 && this.value.bindCallAtPath( [], CallOptions.create( { withNew: false, args: [ expression ] } ) ); + path.length === 0 + && this.value.bindCallAtPath( [], CallOptions.create( { withNew: false, args: [ expression ], caller: this } ), options ); } else { - this.value.bindAssignmentAtPath( path, expression ); + this.value.bindAssignmentAtPath( path, expression, options ); } } - bindCallAtPath ( path, callOptions ) { + bindCallAtPath ( path, callOptions, options ) { if ( this.kind === 'get' ) { - this.value.forEachReturnExpressionWhenCalledAtPath( [], this._accessorCallOptions, - node => node.bindCallAtPath( path, callOptions ) ); + this.value.bindCallAtPath( [], this._accessorCallOptions, options ); + !options.hasReturnExpressionBeenCalledAtPath( path, this ) + && this.value.forEachReturnExpressionWhenCalledAtPath( [], this._accessorCallOptions, innerOptions => node => + node.bindCallAtPath( path, callOptions, innerOptions.addCalledReturnExpressionAtPath( path, this ) ), options ); } else { - this.value.bindCallAtPath( path, callOptions ); + this.value.bindCallAtPath( path, callOptions, options ); } } - forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) { if ( this.kind === 'get' ) { - this.value.forEachReturnExpressionWhenCalledAtPath( [], this._accessorCallOptions, - node => node.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback ) ); + this.value.bindCallAtPath( [], this._accessorCallOptions, options ); + this.value.forEachReturnExpressionWhenCalledAtPath( [], this._accessorCallOptions, innerOptions => node => + node.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback, innerOptions ), options ); } else { - this.value.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback ); + this.value.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback, options ); } } @@ -48,15 +53,15 @@ export default class Property extends Node { } hasEffectsWhenAssignedAtPath ( path, options ) { - if ( this.kind === 'set' ) { - return path.length > 0 - || this.value.hasEffectsWhenCalledAtPath( [], this._accessorCallOptions, options.getHasEffectsWhenCalledOptions() ); - } if ( this.kind === 'get' ) { return path.length === 0 || this.value.someReturnExpressionWhenCalledAtPath( [], this._accessorCallOptions, innerOptions => node => node.hasEffectsWhenAssignedAtPath( path, innerOptions.addAssignedReturnExpressionAtPath( path, this ) ), options ); } + if ( this.kind === 'set' ) { + return path.length > 0 + || this.value.hasEffectsWhenCalledAtPath( [], this._accessorCallOptions, options.getHasEffectsWhenCalledOptions() ); + } return this.value.hasEffectsWhenAssignedAtPath( path, options ); } @@ -78,7 +83,7 @@ export default class Property extends Node { } initialiseNode () { - this._accessorCallOptions = CallOptions.create( { withNew: false } ); + this._accessorCallOptions = CallOptions.create( { withNew: false, caller: this } ); } render ( code, es ) { diff --git a/src/ast/nodes/RestElement.js b/src/ast/nodes/RestElement.js index 65db3d87f1c..c7cd16e12f7 100644 --- a/src/ast/nodes/RestElement.js +++ b/src/ast/nodes/RestElement.js @@ -2,12 +2,14 @@ import Node from '../Node.js'; import { UNKNOWN_ASSIGNMENT } from '../values'; export default class RestElement extends Node { - bindAssignmentAtPath () { - this.argument.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT ); + bindAssignmentAtPath ( path, expression, options ) { + path.length === 0 + && this.argument.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT, options ); } hasEffectsWhenAssignedAtPath ( path, options ) { - return this.argument.hasEffectsWhenAssignedAtPath( path, options ); + return path.length > 0 + || this.argument.hasEffectsWhenAssignedAtPath( [], options ); } initialiseAndDeclare ( parentScope, kind ) { diff --git a/src/ast/nodes/TaggedTemplateExpression.js b/src/ast/nodes/TaggedTemplateExpression.js index 5fd4b6a6a91..88b165a2b7f 100644 --- a/src/ast/nodes/TaggedTemplateExpression.js +++ b/src/ast/nodes/TaggedTemplateExpression.js @@ -29,6 +29,6 @@ export default class TaggedTemplateExpression extends Node { } initialiseNode () { - this._callOptions = CallOptions.create( { withNew: false } ); + this._callOptions = CallOptions.create( { withNew: false, caller: this } ); } } diff --git a/src/ast/nodes/UnaryExpression.js b/src/ast/nodes/UnaryExpression.js index 2542ca5438e..4e50e2b875b 100644 --- a/src/ast/nodes/UnaryExpression.js +++ b/src/ast/nodes/UnaryExpression.js @@ -1,6 +1,7 @@ import Node from '../Node.js'; import { UNKNOWN_VALUE } from '../values'; import UndefinedIdentifier from './shared/UndefinedIdentifier'; +import ExecutionPathOptions from '../ExecutionPathOptions'; const operators = { '-': value => -value, @@ -15,7 +16,7 @@ const operators = { export default class UnaryExpression extends Node { bindNode () { if ( this.operator === 'delete' ) { - this.argument.bindAssignmentAtPath( [], new UndefinedIdentifier() ); + this.argument.bindAssignmentAtPath( [], new UndefinedIdentifier(), ExecutionPathOptions.create() ); } } diff --git a/src/ast/nodes/UpdateExpression.js b/src/ast/nodes/UpdateExpression.js index 61e641f03aa..f0ba5d97bd7 100644 --- a/src/ast/nodes/UpdateExpression.js +++ b/src/ast/nodes/UpdateExpression.js @@ -1,11 +1,12 @@ 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() ); + this.argument.bindAssignmentAtPath( [], new VirtualNumberLiteral(), 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 b1eccfef516..676a4820c5e 100644 --- a/src/ast/nodes/VariableDeclaration.js +++ b/src/ast/nodes/VariableDeclaration.js @@ -1,6 +1,7 @@ import Node from '../Node.js'; import extractNames from '../utils/extractNames.js'; import { UNKNOWN_ASSIGNMENT } from '../values'; +import ExecutionPathOptions from '../ExecutionPathOptions'; function getSeparator ( code, start ) { let c = start; @@ -19,7 +20,7 @@ const forStatement = /^For(?:Of|In)?Statement/; export default class VariableDeclaration extends Node { bindAssignmentAtPath () { - this.eachChild( child => child.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT ) ); + this.eachChild( child => child.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT, ExecutionPathOptions.create() ) ); } hasEffectsWhenAssignedAtPath () { diff --git a/src/ast/nodes/VariableDeclarator.js b/src/ast/nodes/VariableDeclarator.js index dae2d2e432e..8a7f913f587 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 ) { - this.id.bindAssignmentAtPath( path, expression ); + bindAssignmentAtPath ( path, expression, options ) { + this.id.bindAssignmentAtPath( path, expression, options ); } initialiseDeclarator ( parentScope, kind ) { diff --git a/src/ast/nodes/shared/ClassNode.js b/src/ast/nodes/shared/ClassNode.js index e6efd63c6c9..e61848ad841 100644 --- a/src/ast/nodes/shared/ClassNode.js +++ b/src/ast/nodes/shared/ClassNode.js @@ -2,9 +2,9 @@ import Node from '../../Node.js'; import Scope from '../../scopes/Scope'; export default class ClassNode extends Node { - bindCallAtPath ( path, callOptions ) { - this.body.bindCallAtPath( path, callOptions ); - this.superClass && this.superClass.bindCallAtPath( path, callOptions ); + bindCallAtPath ( path, callOptions, options ) { + this.body.bindCallAtPath( path, callOptions, options ); + this.superClass && this.superClass.bindCallAtPath( path, callOptions, options ); } hasEffectsWhenAccessedAtPath ( path ) { diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index 4244cd7da74..b3116c07d8a 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -13,10 +13,9 @@ export default class FunctionNode extends Node { this.body.bindImplicitReturnExpressionToScope(); } - forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { - if ( path.length === 0 ) { - this.scope.forEachReturnExpressionWhenCalled( callback ); - } + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) { + path.length === 0 + && this.scope.forEachReturnExpressionWhenCalled( callOptions, callback, options ); } hasEffects ( options ) { diff --git a/src/ast/scopes/ReturnValueScope.js b/src/ast/scopes/ReturnValueScope.js index 99c34ffabd8..b628c063e3b 100644 --- a/src/ast/scopes/ReturnValueScope.js +++ b/src/ast/scopes/ReturnValueScope.js @@ -10,8 +10,9 @@ export default class ReturnValueScope extends ParameterScope { this._returnExpressions.add( expression ); } - forEachReturnExpressionWhenCalled ( callback ) { - this._returnExpressions.forEach( exp => callback( exp ) ); + forEachReturnExpressionWhenCalled ( callOptions, callback, options ) { + const innerOptions = this.getOptionsWithReplacedParameters( callOptions.args, options ); + this._returnExpressions.forEach( callback( innerOptions ) ); } someReturnExpressionWhenCalled ( callOptions, predicateFunction, options ) { diff --git a/src/ast/scopes/Scope.js b/src/ast/scopes/Scope.js index 56e3263c4dc..28e9a409fcf 100644 --- a/src/ast/scopes/Scope.js +++ b/src/ast/scopes/Scope.js @@ -1,6 +1,5 @@ import { blank, keys } from '../../utils/object.js'; import LocalVariable from '../variables/LocalVariable'; -import ParameterVariable from '../variables/ParameterVariable'; import ExportDefaultVariable from '../variables/ExportDefaultVariable'; import UndefinedIdentifier from '../nodes/shared/UndefinedIdentifier'; diff --git a/src/ast/variables/ArgumentsVariable.js b/src/ast/variables/ArgumentsVariable.js index 4212a47c7a4..4199623a79d 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 ) { + bindAssignmentAtPath ( path, expression, options ) { if ( path.length > 0 ) { if ( path[ 0 ] >= 0 && this._parameters[ path[ 0 ] ] ) { - this._parameters[ path[ 0 ] ].bindAssignmentAtPath( path.slice( 1 ), expression ); + this._parameters[ path[ 0 ] ].bindAssignmentAtPath( path.slice( 1 ), expression, options ); } } } diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index 7e848987e3c..ceda4c8a3c9 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -19,37 +19,36 @@ export default class LocalVariable extends Variable { this.declarations.add( identifier ); } - bindAssignmentAtPath ( path, expression ) { + bindAssignmentAtPath ( path, expression, options ) { if ( path.length > MAX_PATH_LENGTH || this.boundExpressions.hasAtPath( path, expression ) ) return; this.boundExpressions.addAtPath( path, expression ); this.boundExpressions.forEachAssignedToPath( path, ( subPath, node ) => { - if ( subPath.length > 0 ) { - expression.bindAssignmentAtPath( 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 ) ); + node.bindAssignmentAtPath( [ ...relativePath, ...path.slice( -1 ) ], expression, options ) ); } else { this.isReassigned = true; } this.boundCalls.forEachAtPath( path, ( relativePath, callOptions ) => - expression.bindCallAtPath( relativePath, callOptions ) ); + expression.bindCallAtPath( relativePath, callOptions, options ) ); } - bindCallAtPath ( path, callOptions ) { + bindCallAtPath ( path, callOptions, options ) { if ( path.length > MAX_PATH_LENGTH || this.boundCalls.hasAtPath( path, callOptions ) ) return; this.boundCalls.addAtPath( path, callOptions ); this.boundExpressions.forEachAtPath( path, ( relativePath, node ) => - node.bindCallAtPath( relativePath, callOptions ) ); + node.bindCallAtPath( relativePath, callOptions, options ) ); } - forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) { if ( path.length > MAX_PATH_LENGTH ) return; this.boundExpressions.forEachAtPath( path, ( relativePath, node ) => - !callOptions.hasNodeBeenCalledAtPath( relativePath, node ) - && node.forEachReturnExpressionWhenCalledAtPath( relativePath, callOptions - .addCalledNodeAtPath( relativePath, node ), callback ) ); + !options.hasNodeBeenCalledAtPathWithOptions( relativePath, node, callOptions ) && node + .forEachReturnExpressionWhenCalledAtPath( relativePath, callOptions, callback, + options.addCalledNodeAtPathWithOptions( relativePath, node, callOptions ) ) ); } getName ( es ) { @@ -62,9 +61,9 @@ export default class LocalVariable extends Variable { hasEffectsWhenAccessedAtPath ( path, options ) { return path.length > MAX_PATH_LENGTH || this.boundExpressions.someAtPath( path, ( relativePath, node ) => - !options.hasNodeBeenAccessedAtPath( relativePath, node ) - && node.hasEffectsWhenAccessedAtPath( relativePath, options - .addAccessedNodeAtPath( relativePath, node ) ) ); + !options.hasNodeBeenAccessedAtPath( relativePath, node ) && node + .hasEffectsWhenAccessedAtPath( relativePath, + options.addAccessedNodeAtPath( relativePath, node ) ) ); } hasEffectsWhenAssignedAtPath ( path, options ) { @@ -72,35 +71,34 @@ export default class LocalVariable extends Variable { || path.length > MAX_PATH_LENGTH || this.boundExpressions.someAtPath( path, ( relativePath, node ) => relativePath.length > 0 - && !options.hasNodeBeenAssignedAtPath( relativePath, node ) - && node.hasEffectsWhenAssignedAtPath( relativePath, options - .addAssignedNodeAtPath( relativePath, node ) ) ); + && !options.hasNodeBeenAssignedAtPath( relativePath, node ) && node + .hasEffectsWhenAssignedAtPath( relativePath, + options.addAssignedNodeAtPath( relativePath, node ) ) ); } hasEffectsWhenCalledAtPath ( path, callOptions, options ) { return path.length > MAX_PATH_LENGTH || (this.included && path.length > 0) || this.boundExpressions.someAtPath( path, ( relativePath, node ) => - !options.hasNodeBeenCalledAtPathWithOptions( relativePath, node, callOptions ) - && node.hasEffectsWhenCalledAtPath( relativePath, callOptions, options - .addCalledNodeAtPathWithOptions( relativePath, node, callOptions ) ) + !options.hasNodeBeenCalledAtPathWithOptions( relativePath, node, callOptions ) && node + .hasEffectsWhenCalledAtPath( relativePath, callOptions, + options.addCalledNodeAtPathWithOptions( relativePath, node, callOptions ) ) ); } includeVariable () { - if ( !super.includeVariable() ) { - return false; - } + if ( !super.includeVariable() ) return false; this.declarations.forEach( identifier => identifier.includeInBundle() ); return true; } someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { return path.length > MAX_PATH_LENGTH + || (this.included && path.length > 0) || this.boundExpressions.someAtPath( path, ( relativePath, node ) => - !callOptions.hasNodeBeenCalledAtPath( relativePath, node ) - && node.someReturnExpressionWhenCalledAtPath( relativePath, callOptions - .addCalledNodeAtPath( relativePath, node ), predicateFunction, options ) ); + !options.hasNodeBeenCalledAtPathWithOptions( relativePath, node, callOptions ) && node + .someReturnExpressionWhenCalledAtPath( relativePath, callOptions, predicateFunction, + options.addCalledNodeAtPathWithOptions( relativePath, node, callOptions ) ) ); } toString () { diff --git a/src/ast/variables/StructuredAssignmentTracker.js b/src/ast/variables/StructuredAssignmentTracker.js index d591dc9865e..d1b2500460f 100644 --- a/src/ast/variables/StructuredAssignmentTracker.js +++ b/src/ast/variables/StructuredAssignmentTracker.js @@ -69,6 +69,18 @@ export default class StructuredAssignmentTracker { } } + hasEqualAtPath ( path, equalAssignment ) { + if ( path.length === 0 ) { + return Array.from( this._assignments.get( SET_KEY ) ).find( assignment => equalAssignment.equals( assignment ) ); + } else { + const [ nextPath, ...remainingPath ] = path; + if ( !this._assignments.has( nextPath ) ) { + return false; + } + return this._assignments.get( nextPath ).hasAtPath( remainingPath, equalAssignment ); + } + } + someAtPath ( path, predicateFunction ) { const [ nextPath, ...remainingPath ] = path; return Array.from( this._assignments.get( SET_KEY ) ).some( assignment => predicateFunction( path, assignment ) ) diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index 90c4f5a112b..27f98d89b0a 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -19,24 +19,26 @@ export default class Variable { * e.g. an object path is called or mutated. * @param {String[]} path * @param {Node} expression + * @param {ExecutionPathOptions} options */ - bindAssignmentAtPath ( path, expression ) {} + bindAssignmentAtPath ( path, expression, options ) {} /** * Binds a call to this node. Necessary to be able to bind arguments * to parameters. * @param {String[]} path * @param {CallOptions} callOptions + * @param {ExecutionPathOptions} options */ - bindCallAtPath ( path, callOptions ) {} + bindCallAtPath ( path, callOptions, options ) {} /** * @param {String[]} path * @param {CallOptions} callOptions * @param {Function} callback - * @returns {*} + * @param {ExecutionPathOptions} options */ - forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) {} + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) {} /** * @returns {String} diff --git a/test/form/samples/side-effects-es6-class-declarations/_config.js b/test/form/samples/side-effects-es6-class-declarations/_config.js index 097de149111..29ad84ec965 100644 --- a/test/form/samples/side-effects-es6-class-declarations/_config.js +++ b/test/form/samples/side-effects-es6-class-declarations/_config.js @@ -1,3 +1,3 @@ module.exports = { - description: 'determine side effects in ES6 class declarations', + description: 'determine side effects in ES6 class declarations' }; diff --git a/test/function/samples/build-promise-chain/_config.js b/test/function/samples/build-promise-chain/_config.js new file mode 100644 index 00000000000..89a2db852c9 --- /dev/null +++ b/test/function/samples/build-promise-chain/_config.js @@ -0,0 +1,5 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'Does not fail when iteratively chaining promises' +}; diff --git a/test/function/samples/build-promise-chain/main.js b/test/function/samples/build-promise-chain/main.js new file mode 100644 index 00000000000..933f7793db5 --- /dev/null +++ b/test/function/samples/build-promise-chain/main.js @@ -0,0 +1,4 @@ +let promise = Promise.resolve(); + +promise = promise.then( () => {} ); +promise.then( () => {} );