Skip to content

Commit

Permalink
Bind return values.
Browse files Browse the repository at this point in the history
  • Loading branch information
lukastaegert committed Nov 8, 2017
1 parent 63e4453 commit dcfe5d4
Show file tree
Hide file tree
Showing 24 changed files with 192 additions and 17 deletions.
10 changes: 9 additions & 1 deletion src/ast/Node.js
Expand Up @@ -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 ) {}

Expand All @@ -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;
}
Expand Down
6 changes: 6 additions & 0 deletions src/ast/nodes/ArrowFunctionExpression.js
Expand Up @@ -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;
}
Expand Down
17 changes: 17 additions & 0 deletions 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 );
Expand Down Expand Up @@ -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 ) {
Expand Down
4 changes: 4 additions & 0 deletions src/ast/nodes/ConditionalExpression.js
Expand Up @@ -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;
Expand Down
4 changes: 0 additions & 4 deletions src/ast/nodes/ExportDefaultDeclaration.js
Expand Up @@ -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();
Expand Down
7 changes: 7 additions & 0 deletions src/ast/nodes/Identifier.js
Expand Up @@ -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 );
Expand Down
4 changes: 4 additions & 0 deletions src/ast/nodes/LogicalExpression.js
Expand Up @@ -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;
Expand Down
17 changes: 11 additions & 6 deletions src/ast/nodes/MemberExpression.js
Expand Up @@ -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 {
Expand All @@ -88,16 +86,23 @@ 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 {
this.object.bindCallAtPath( [ this._getPathSegment(), ...path ], callOptions );
}
}

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 );
Expand Down
14 changes: 8 additions & 6 deletions src/ast/nodes/ObjectExpression.js
Expand Up @@ -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 };
Expand Down
4 changes: 4 additions & 0 deletions src/ast/nodes/Property.js
Expand Up @@ -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 );
Expand Down
6 changes: 6 additions & 0 deletions src/ast/nodes/shared/FunctionNode.js
Expand Up @@ -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 );
}
Expand Down
4 changes: 4 additions & 0 deletions src/ast/scopes/ReturnValueScope.js
Expand Up @@ -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 ) );
Expand Down
8 changes: 8 additions & 0 deletions src/ast/variables/LocalVariable.js
Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions src/ast/variables/Variable.js
Expand Up @@ -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}
*/
Expand Down
@@ -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' );
}
};
@@ -0,0 +1,9 @@
const foo = { mightBeExported: {} };
const exported = {};

const getFoo = () => foo;

getFoo().mightBeExported = exported;
foo.mightBeExported.bar = 'present';

export default exported;
@@ -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' );
}
};
13 changes: 13 additions & 0 deletions 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;
@@ -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' );
}
};
@@ -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;
@@ -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' );
}
};
11 changes: 11 additions & 0 deletions 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;
@@ -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' );
}
};
@@ -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;

0 comments on commit dcfe5d4

Please sign in to comment.