diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index a3bd422fef6..e7e76fcfec4 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -17,6 +17,11 @@ export default class CallExpression extends Node { node => node.bindCallAtPath( path, callOptions ) ); } + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { + this.callee.forEachReturnExpressionWhenCalledAtPath( [], this._callOptions, + node => node.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback ) ); + } + bindNode () { if ( this.callee.type === 'Identifier' ) { const variable = this.scope.findVariable( this.callee.name ); @@ -65,6 +70,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(); } diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index 8aac8cf248c..db0f1b7ad02 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -7,18 +7,22 @@ const PROPERTY_KINDS_WRITE = [ 'init', 'set' ]; export default class ObjectExpression extends Node { bindAssignmentAtPath ( path, expression ) { if ( path.length === 0 ) return; - this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_WRITE ).properties.forEach( property => - property.bindAssignmentAtPath( path.slice( 1 ), expression ) ); + + this._getPossiblePropertiesWithName( + path[ 0 ], path.length === 1 ? PROPERTY_KINDS_WRITE : PROPERTY_KINDS_READ ) + .properties.forEach( property => property.bindAssignmentAtPath( path.slice( 1 ), expression ) ); } bindCallAtPath ( path, callOptions ) { if ( path.length === 0 ) return; + this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_READ ).properties.forEach( property => property.bindCallAtPath( path.slice( 1 ), callOptions ) ); } forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { if ( path.length === 0 ) return; + this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_READ ).properties.forEach( property => property.forEachReturnExpressionWhenCalledAtPath( path.slice( 1 ), callOptions, callback ) ); } @@ -45,43 +49,35 @@ export default class ObjectExpression extends Node { } hasEffectsWhenAccessedAtPath ( path, options ) { - if ( path.length === 0 ) { - return false; - } - const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_READ ); + if ( path.length === 0 ) return false; + const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_READ ); return (path.length > 1 && !hasCertainHit) || properties.some( property => property.hasEffectsWhenAccessedAtPath( path.slice( 1 ), options ) ); } hasEffectsWhenAssignedAtPath ( path, options ) { - if ( path.length === 0 ) { - return false; - } - const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ], - path.length === 1 ? PROPERTY_KINDS_WRITE : PROPERTY_KINDS_READ ); + if ( path.length === 0 ) return false; + const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( + path[ 0 ], path.length === 1 ? PROPERTY_KINDS_WRITE : PROPERTY_KINDS_READ ); return (path.length > 1 && !hasCertainHit) || properties.some( property => (path.length > 1 || property.kind === 'set') && property.hasEffectsWhenAssignedAtPath( path.slice( 1 ), options ) ); } hasEffectsWhenCalledAtPath ( path, callOptions, options ) { - if ( path.length === 0 ) { - return true; - } - const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_READ ); + if ( path.length === 0 ) return true; + const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_READ ); return !hasCertainHit || properties.some( property => property.hasEffectsWhenCalledAtPath( path.slice( 1 ), callOptions, options ) ); } someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { - if ( path.length === 0 ) { - return true; - } - const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_READ ); + if ( path.length === 0 ) return true; + const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_READ ); return !hasCertainHit || properties.some( property => property.someReturnExpressionWhenCalledAtPath( path.slice( 1 ), callOptions, predicateFunction, options ) ); } diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index 078b8840903..d844018096d 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -4,15 +4,32 @@ import { UNKNOWN_ASSIGNMENT } from '../values'; export default class Property extends Node { bindAssignmentAtPath ( path, expression ) { - this.value.bindAssignmentAtPath( path, expression ); + if ( this.kind === 'get' ) { + path.length > 0 && this.value.forEachReturnExpressionWhenCalledAtPath( [], this._accessorCallOptions, + node => node.bindAssignmentAtPath( path, expression ) ); + } else if ( this.kind === 'set' ) { + path.length === 0 && this.value.bindCallAtPath( [], CallOptions.create( { withNew: false, args: [ expression ] } ) ); + } else { + this.value.bindAssignmentAtPath( path, expression ); + } } bindCallAtPath ( path, callOptions ) { - this.value.bindCallAtPath( path, callOptions ); + if ( this.kind === 'get' ) { + this.value.forEachReturnExpressionWhenCalledAtPath( [], this._accessorCallOptions, + node => node.bindCallAtPath( path, callOptions ) ); + } else { + this.value.bindCallAtPath( path, callOptions ); + } } forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { - this.value.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback ); + if ( this.kind === 'get' ) { + this.value.forEachReturnExpressionWhenCalledAtPath( [], this._accessorCallOptions, + node => node.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback ) ); + } else { + this.value.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback ); + } } hasEffects ( options ) { @@ -22,9 +39,9 @@ export default class Property extends Node { hasEffectsWhenAccessedAtPath ( path, options ) { if ( this.kind === 'get' ) { - return this.value.hasEffectsWhenCalledAtPath( [], this._getterCallOptions, options.getHasEffectsWhenCalledOptions() ) + return this.value.hasEffectsWhenCalledAtPath( [], this._accessorCallOptions, options.getHasEffectsWhenCalledOptions() ) || (!options.hasReturnExpressionBeenAccessedAtPath( path, this ) - && this.value.someReturnExpressionWhenCalledAtPath( [], this._getterCallOptions, innerOptions => node => + && this.value.someReturnExpressionWhenCalledAtPath( [], this._accessorCallOptions, innerOptions => node => node.hasEffectsWhenAccessedAtPath( path, innerOptions.addAccessedReturnExpressionAtPath( path, this ) ), options )); } return this.value.hasEffectsWhenAccessedAtPath( path, options ); @@ -33,16 +50,21 @@ export default class Property extends Node { hasEffectsWhenAssignedAtPath ( path, options ) { if ( this.kind === 'set' ) { return path.length > 0 - || this.value.hasEffectsWhenCalledAtPath( [], this._getterCallOptions, options.getHasEffectsWhenCalledOptions() ); + || 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 ); } return this.value.hasEffectsWhenAssignedAtPath( path, options ); } hasEffectsWhenCalledAtPath ( path, callOptions, options ) { if ( this.kind === 'get' ) { - return this.value.hasEffectsWhenCalledAtPath( [], this._getterCallOptions, options.getHasEffectsWhenCalledOptions() ) + return this.value.hasEffectsWhenCalledAtPath( [], this._accessorCallOptions, options.getHasEffectsWhenCalledOptions() ) || (!options.hasReturnExpressionBeenCalledAtPath( path, this ) - && this.value.someReturnExpressionWhenCalledAtPath( [], this._getterCallOptions, innerOptions => node => + && this.value.someReturnExpressionWhenCalledAtPath( [], this._accessorCallOptions, innerOptions => node => node.hasEffectsWhenCalledAtPath( path, callOptions, innerOptions.addCalledReturnExpressionAtPath( path, this ) ), options )); } return this.value.hasEffectsWhenCalledAtPath( path, callOptions, options ); @@ -56,7 +78,7 @@ export default class Property extends Node { } initialiseNode () { - this._getterCallOptions = CallOptions.create( { withNew: false } ); + this._accessorCallOptions = CallOptions.create( { withNew: false } ); } render ( code, es ) { @@ -68,8 +90,8 @@ export default class Property extends Node { someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { if ( this.kind === 'get' ) { - return this.value.hasEffectsWhenCalledAtPath( [], this._getterCallOptions, options.getHasEffectsWhenCalledOptions() ) - || this.value.someReturnExpressionWhenCalledAtPath( [], this._getterCallOptions, innerOptions => node => + return this.value.hasEffectsWhenCalledAtPath( [], this._accessorCallOptions, options.getHasEffectsWhenCalledOptions() ) + || this.value.someReturnExpressionWhenCalledAtPath( [], this._accessorCallOptions, innerOptions => node => node.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, innerOptions ), options ); } return this.value.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ); diff --git a/test/function/samples/associate-function-return-values-3/_config.js b/test/function/samples/associate-function-return-values-3/_config.js new file mode 100644 index 00000000000..02522cee30c --- /dev/null +++ b/test/function/samples/associate-function-return-values-3/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'Associates function return values of returned functions', + exports: function ( exports ) { + assert.equal( exports.bar, 'present' ); + } +}; diff --git a/test/function/samples/associate-function-return-values-3/main.js b/test/function/samples/associate-function-return-values-3/main.js new file mode 100644 index 00000000000..c9c75678ca2 --- /dev/null +++ b/test/function/samples/associate-function-return-values-3/main.js @@ -0,0 +1,11 @@ +const foo = { mightBeExported: {} }; +const exported = {}; + +function getGetFoo () { + return () => foo; +} + +getGetFoo()().mightBeExported = exported; +foo.mightBeExported.bar = 'present'; + +export default exported; diff --git a/test/function/samples/associate-getter-return-values-2/_config.js b/test/function/samples/associate-getter-return-values-2/_config.js new file mode 100644 index 00000000000..d07ce3fd79b --- /dev/null +++ b/test/function/samples/associate-getter-return-values-2/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'Associates getter return values with regard to calls', + exports: function ( exports ) { + assert.equal( exports.bar, 'present' ); + } +}; diff --git a/test/function/samples/associate-getter-return-values-2/main.js b/test/function/samples/associate-getter-return-values-2/main.js new file mode 100644 index 00000000000..f22481cbd2e --- /dev/null +++ b/test/function/samples/associate-getter-return-values-2/main.js @@ -0,0 +1,8 @@ +const foo = { mightBeExported: {} }; +const exported = {}; +const fooContainer = { get getFoo () {return () => foo;} }; + +fooContainer.getFoo().mightBeExported = exported; +foo.mightBeExported.bar = 'present'; + +export default exported; diff --git a/test/function/samples/associate-getter-return-values/_config.js b/test/function/samples/associate-getter-return-values/_config.js new file mode 100644 index 00000000000..bf83751e6d1 --- /dev/null +++ b/test/function/samples/associate-getter-return-values/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'Associates getter return values with regard to mutations', + exports: function ( exports ) { + assert.equal( exports.bar, 'present' ); + } +}; diff --git a/test/function/samples/associate-getter-return-values/main.js b/test/function/samples/associate-getter-return-values/main.js new file mode 100644 index 00000000000..50c56684467 --- /dev/null +++ b/test/function/samples/associate-getter-return-values/main.js @@ -0,0 +1,8 @@ +const foo = { mightBeExported: {} }; +const exported = {}; +const fooContainer = { get foo () {return foo;} }; + +fooContainer.foo.mightBeExported = exported; +foo.mightBeExported.bar = 'present'; + +export default exported; diff --git a/test/function/samples/associate-setter-parameters/_config.js b/test/function/samples/associate-setter-parameters/_config.js new file mode 100644 index 00000000000..bf83751e6d1 --- /dev/null +++ b/test/function/samples/associate-setter-parameters/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'Associates getter return values with regard to mutations', + exports: function ( exports ) { + assert.equal( exports.bar, 'present' ); + } +}; diff --git a/test/function/samples/associate-setter-parameters/main.js b/test/function/samples/associate-setter-parameters/main.js new file mode 100644 index 00000000000..95b1c9d3c9b --- /dev/null +++ b/test/function/samples/associate-setter-parameters/main.js @@ -0,0 +1,12 @@ +const foo = { mightBeExported: {} }; +const exported = {}; +const fooContainer = { + set addMightBeExported ( obj ) { + obj.mightBeExported = exported; + } +}; + +fooContainer.addMightBeExported = foo; +foo.mightBeExported.bar = 'present'; + +export default exported;