From dcfe5d4bd79b9588cc2c0576ec98c107f5e7434a Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Thu, 2 Nov 2017 17:15:47 +0100 Subject: [PATCH] Bind return values. --- src/ast/Node.js | 10 +++++++++- src/ast/nodes/ArrowFunctionExpression.js | 6 ++++++ src/ast/nodes/CallExpression.js | 17 +++++++++++++++++ src/ast/nodes/ConditionalExpression.js | 4 ++++ src/ast/nodes/ExportDefaultDeclaration.js | 4 ---- src/ast/nodes/Identifier.js | 7 +++++++ src/ast/nodes/LogicalExpression.js | 4 ++++ src/ast/nodes/MemberExpression.js | 17 +++++++++++------ src/ast/nodes/ObjectExpression.js | 14 ++++++++------ src/ast/nodes/Property.js | 4 ++++ src/ast/nodes/shared/FunctionNode.js | 6 ++++++ src/ast/scopes/ReturnValueScope.js | 4 ++++ src/ast/variables/LocalVariable.js | 8 ++++++++ src/ast/variables/Variable.js | 8 ++++++++ .../_config.js | 8 ++++++++ .../main.js | 9 +++++++++ .../_config.js | 8 ++++++++ .../associate-function-return-values-2/main.js | 13 +++++++++++++ .../_config.js | 8 ++++++++ .../main.js | 11 +++++++++++ .../associate-function-return-values/_config.js | 8 ++++++++ .../associate-function-return-values/main.js | 11 +++++++++++ .../_config.js | 8 ++++++++ .../main.js | 12 ++++++++++++ 24 files changed, 192 insertions(+), 17 deletions(-) create mode 100644 test/function/samples/associate-arrow-function-return-values/_config.js create mode 100644 test/function/samples/associate-arrow-function-return-values/main.js create mode 100644 test/function/samples/associate-function-return-values-2/_config.js create mode 100644 test/function/samples/associate-function-return-values-2/main.js create mode 100644 test/function/samples/associate-function-return-values-across-other-expressions/_config.js create mode 100644 test/function/samples/associate-function-return-values-across-other-expressions/main.js create mode 100644 test/function/samples/associate-function-return-values/_config.js create mode 100644 test/function/samples/associate-function-return-values/main.js create mode 100644 test/function/samples/associate-object-expression-return-values/_config.js create mode 100644 test/function/samples/associate-object-expression-return-values/main.js diff --git a/src/ast/Node.js b/src/ast/Node.js index 4adc160e3f4..26ba86833d0 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -47,7 +47,7 @@ export default class Node { * 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 + * @param {CallOptions} callOptions */ bindCallAtPath ( path, callOptions ) {} @@ -64,6 +64,14 @@ export default class Node { } ); } + /** + * Executes the callback on each possible return expression when calling this node. + * @param {String[]} path + * @param {CallOptions} callOptions + * @param {Function} callback + */ + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) {} + getValue () { return UNKNOWN_VALUE; } diff --git a/src/ast/nodes/ArrowFunctionExpression.js b/src/ast/nodes/ArrowFunctionExpression.js index 3bbd88a14ba..aca439ccb6f 100644 --- a/src/ast/nodes/ArrowFunctionExpression.js +++ b/src/ast/nodes/ArrowFunctionExpression.js @@ -15,6 +15,12 @@ export default class ArrowFunctionExpression extends Node { : this.scope.addReturnExpression( this.body ); } + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { + if ( path.length === 0 ) { + this.scope.forEachReturnExpressionWhenCalled( callback ); + } + } + hasEffects () { return false; } diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index b4aeae34380..a3bd422fef6 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -1,7 +1,22 @@ import Node from '../Node.js'; import CallOptions from '../CallOptions'; +import StructuredAssignmentTracker from '../variables/StructuredAssignmentTracker'; 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 ) ); + } + + 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 ) ); + } + bindNode () { if ( this.callee.type === 'Identifier' ) { const variable = this.scope.findVariable( this.callee.name ); @@ -50,6 +65,8 @@ export default class CallExpression extends Node { initialiseNode () { this._callOptions = CallOptions.create( { withNew: false, args: this.arguments } ); + this._boundExpressions = new StructuredAssignmentTracker(); + this._boundCalls = new StructuredAssignmentTracker(); } someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { diff --git a/src/ast/nodes/ConditionalExpression.js b/src/ast/nodes/ConditionalExpression.js index 23db5feb950..085e65aed1d 100644 --- a/src/ast/nodes/ConditionalExpression.js +++ b/src/ast/nodes/ConditionalExpression.js @@ -12,6 +12,10 @@ export default class ConditionalExpression extends Node { this._forEachRelevantBranch( node => node.bindCallAtPath( path, callOptions ) ); } + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { + this._forEachRelevantBranch( node => node.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback ) ); + } + getValue () { const testValue = this.test.getValue(); if ( testValue === UNKNOWN_VALUE ) return UNKNOWN_VALUE; diff --git a/src/ast/nodes/ExportDefaultDeclaration.js b/src/ast/nodes/ExportDefaultDeclaration.js index 6b006d1451d..e896d16b07a 100644 --- a/src/ast/nodes/ExportDefaultDeclaration.js +++ b/src/ast/nodes/ExportDefaultDeclaration.js @@ -10,10 +10,6 @@ export default class ExportDefaultDeclaration extends Node { } } - hasEffectsWhenCalledAtPath ( path, callOptions, options ) { - return this.declaration.hasEffectsWhenCalledAtPath( path, callOptions, options ); - } - includeDefaultExport () { this.included = true; this.declaration.includeInBundle(); diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index 6356d64c560..afe3c18f7dc 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -28,6 +28,13 @@ export default class Identifier extends Node { } } + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { + this._bindVariableIfMissing(); + if ( this.variable ) { + this.variable.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback ); + } + } + hasEffectsWhenAccessedAtPath ( path, options ) { return this.variable && this.variable.hasEffectsWhenAccessedAtPath( path, options ); diff --git a/src/ast/nodes/LogicalExpression.js b/src/ast/nodes/LogicalExpression.js index 6aadf848426..feead3c619c 100644 --- a/src/ast/nodes/LogicalExpression.js +++ b/src/ast/nodes/LogicalExpression.js @@ -12,6 +12,10 @@ export default class LogicalExpression extends Node { this._forEachRelevantBranch( node => node.bindCallAtPath( path, callOptions ) ); } + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { + this._forEachRelevantBranch( node => node.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback ) ); + } + getValue () { const leftValue = this.left.getValue(); if ( leftValue === UNKNOWN_VALUE ) return UNKNOWN_VALUE; diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index f04685ba516..bfba8c2bad2 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -77,9 +77,7 @@ export default class MemberExpression extends Node { } bindAssignmentAtPath ( path, expression ) { - if ( !this._bound ) { - this.bind(); - } + if ( !this._bound ) this.bind(); if ( this.variable ) { this.variable.bindAssignmentAtPath( path, expression ); } else { @@ -88,9 +86,7 @@ export default class MemberExpression extends Node { } bindCallAtPath ( path, callOptions ) { - if ( !this._bound ) { - this.bind(); - } + if ( !this._bound ) this.bind(); if ( this.variable ) { this.variable.bindCallAtPath( path, callOptions ); } else { @@ -98,6 +94,15 @@ export default class MemberExpression extends Node { } } + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { + if ( !this._bound ) this.bind(); + if ( this.variable ) { + this.variable.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback ); + } else { + this.object.forEachReturnExpressionWhenCalledAtPath( [ this._getPathSegment(), ...path ], callOptions, callback ); + } + } + hasEffects ( options ) { return super.hasEffects( options ) || this.object.hasEffectsWhenAccessedAtPath( [ this._getPathSegment() ], options ); diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index b6146c57dfd..8aac8cf248c 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -6,21 +6,23 @@ const PROPERTY_KINDS_WRITE = [ 'init', 'set' ]; export default class ObjectExpression extends Node { bindAssignmentAtPath ( path, expression ) { - if ( path.length === 0 ) { - return; - } + if ( path.length === 0 ) return; this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_WRITE ).properties.forEach( property => property.bindAssignmentAtPath( path.slice( 1 ), expression ) ); } bindCallAtPath ( path, callOptions ) { - if ( path.length === 0 ) { - return; - } + 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 ) ); + } + _getPossiblePropertiesWithName ( name, kinds ) { if ( name === UNKNOWN_KEY ) { return { properties: this.properties, hasCertainHit: false }; diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index 34f92de1cb0..078b8840903 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -11,6 +11,10 @@ export default class Property extends Node { this.value.bindCallAtPath( path, callOptions ); } + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { + this.value.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback ); + } + hasEffects ( options ) { return this.key.hasEffects( options ) || this.value.hasEffects( options ); diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index 201c7896480..4244cd7da74 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -13,6 +13,12 @@ export default class FunctionNode extends Node { this.body.bindImplicitReturnExpressionToScope(); } + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { + if ( path.length === 0 ) { + this.scope.forEachReturnExpressionWhenCalled( callback ); + } + } + hasEffects ( options ) { return this.id && this.id.hasEffects( options ); } diff --git a/src/ast/scopes/ReturnValueScope.js b/src/ast/scopes/ReturnValueScope.js index 18fef9ea1ba..99c34ffabd8 100644 --- a/src/ast/scopes/ReturnValueScope.js +++ b/src/ast/scopes/ReturnValueScope.js @@ -10,6 +10,10 @@ export default class ReturnValueScope extends ParameterScope { this._returnExpressions.add( expression ); } + forEachReturnExpressionWhenCalled ( callback ) { + this._returnExpressions.forEach( exp => callback( exp ) ); + } + someReturnExpressionWhenCalled ( callOptions, predicateFunction, options ) { const innerOptions = this.getOptionsWithReplacedParameters( callOptions.args, options ); return Array.from( this._returnExpressions ).some( predicateFunction( innerOptions ) ); diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index 49e8af18830..7e848987e3c 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -44,6 +44,14 @@ export default class LocalVariable extends Variable { node.bindCallAtPath( relativePath, callOptions ) ); } + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) { + 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 ) ); + } + getName ( es ) { if ( es ) return this.name; if ( !this.isReassigned || !this.exportName ) return this.name; diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index 8c9dc7c2f1b..90c4f5a112b 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -30,6 +30,14 @@ export default class Variable { */ bindCallAtPath ( path, callOptions ) {} + /** + * @param {String[]} path + * @param {CallOptions} callOptions + * @param {Function} callback + * @returns {*} + */ + forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback ) {} + /** * @returns {String} */ diff --git a/test/function/samples/associate-arrow-function-return-values/_config.js b/test/function/samples/associate-arrow-function-return-values/_config.js new file mode 100644 index 00000000000..98eedfe77cf --- /dev/null +++ b/test/function/samples/associate-arrow-function-return-values/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'Associates function return values with regard to mutations', + exports: function ( exports ) { + assert.equal( exports.bar, 'present' ); + } +}; diff --git a/test/function/samples/associate-arrow-function-return-values/main.js b/test/function/samples/associate-arrow-function-return-values/main.js new file mode 100644 index 00000000000..ad815070e56 --- /dev/null +++ b/test/function/samples/associate-arrow-function-return-values/main.js @@ -0,0 +1,9 @@ +const foo = { mightBeExported: {} }; +const exported = {}; + +const getFoo = () => foo; + +getFoo().mightBeExported = exported; +foo.mightBeExported.bar = 'present'; + +export default exported; diff --git a/test/function/samples/associate-function-return-values-2/_config.js b/test/function/samples/associate-function-return-values-2/_config.js new file mode 100644 index 00000000000..7dc454a0645 --- /dev/null +++ b/test/function/samples/associate-function-return-values-2/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'Associates function return values with regard to calls', + exports: function ( exports ) { + assert.equal( exports.bar, 'present' ); + } +}; diff --git a/test/function/samples/associate-function-return-values-2/main.js b/test/function/samples/associate-function-return-values-2/main.js new file mode 100644 index 00000000000..671c09c7667 --- /dev/null +++ b/test/function/samples/associate-function-return-values-2/main.js @@ -0,0 +1,13 @@ +const foo = { mightBeExported: {} }; +const exported = {}; + +function getAssignExported () { + return function assignExported ( obj ) { + obj.mightBeExported = exported; + }; +} + +getAssignExported()( foo ); +foo.mightBeExported.bar = 'present'; + +export default exported; diff --git a/test/function/samples/associate-function-return-values-across-other-expressions/_config.js b/test/function/samples/associate-function-return-values-across-other-expressions/_config.js new file mode 100644 index 00000000000..98eedfe77cf --- /dev/null +++ b/test/function/samples/associate-function-return-values-across-other-expressions/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'Associates function return values with regard to mutations', + exports: function ( exports ) { + assert.equal( exports.bar, 'present' ); + } +}; diff --git a/test/function/samples/associate-function-return-values-across-other-expressions/main.js b/test/function/samples/associate-function-return-values-across-other-expressions/main.js new file mode 100644 index 00000000000..86f78a3bcc0 --- /dev/null +++ b/test/function/samples/associate-function-return-values-across-other-expressions/main.js @@ -0,0 +1,11 @@ +const foo = { mightBeExported: {} }; +const exported = {}; + +function getFoo () { + return foo; +} + +(Math.random() < 0.5 ? true && getFoo : false || getFoo)().mightBeExported = exported; +foo.mightBeExported.bar = 'present'; + +export default exported; diff --git a/test/function/samples/associate-function-return-values/_config.js b/test/function/samples/associate-function-return-values/_config.js new file mode 100644 index 00000000000..98eedfe77cf --- /dev/null +++ b/test/function/samples/associate-function-return-values/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'Associates function return values with regard to mutations', + exports: function ( exports ) { + assert.equal( exports.bar, 'present' ); + } +}; diff --git a/test/function/samples/associate-function-return-values/main.js b/test/function/samples/associate-function-return-values/main.js new file mode 100644 index 00000000000..b41e9a70e95 --- /dev/null +++ b/test/function/samples/associate-function-return-values/main.js @@ -0,0 +1,11 @@ +const foo = { mightBeExported: {} }; +const exported = {}; + +function getFoo () { + return foo; +} + +getFoo().mightBeExported = exported; +foo.mightBeExported.bar = 'present'; + +export default exported; diff --git a/test/function/samples/associate-object-expression-return-values/_config.js b/test/function/samples/associate-object-expression-return-values/_config.js new file mode 100644 index 00000000000..9d29f4c142b --- /dev/null +++ b/test/function/samples/associate-object-expression-return-values/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'Associates object expression member parameters with their call arguments', + exports: function ( exports ) { + assert.equal( exports.bar, 'present' ); + } +}; diff --git a/test/function/samples/associate-object-expression-return-values/main.js b/test/function/samples/associate-object-expression-return-values/main.js new file mode 100644 index 00000000000..d334bc94552 --- /dev/null +++ b/test/function/samples/associate-object-expression-return-values/main.js @@ -0,0 +1,12 @@ +const foo = { mightBeExported: {} }; +const exported = {}; +const assigner = { + getFoo () { + return foo; + } +}; + +assigner.getFoo().mightBeExported = exported; +foo.mightBeExported.bar = 'present'; + +export default exported;