From cc01fd5b262f2ee457ec0ef324a99246a0f7c8be Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Fri, 15 Sep 2017 10:31:30 +0200 Subject: [PATCH 01/76] Add implicit undefined assignments to variable declarations without initialisation. This will make tree-shaking slightly worse but will not swallow runtime errors due to uninitialized variables. Though this might not look as an important use case, it will become more important when assigning implicit undefined values to members of objects. --- src/ast/nodes/Property.js | 4 ++-- src/ast/scopes/Scope.js | 3 ++- .../implicit-undefined-assignments/_config.js | 5 +++++ .../_expected/amd.js | 15 +++++++++++++++ .../_expected/cjs.js | 13 +++++++++++++ .../_expected/es.js | 11 +++++++++++ .../_expected/iife.js | 16 ++++++++++++++++ .../_expected/umd.js | 19 +++++++++++++++++++ .../implicit-undefined-assignments/main.js | 11 +++++++++++ 9 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 test/form/samples/implicit-undefined-assignments/_config.js create mode 100644 test/form/samples/implicit-undefined-assignments/_expected/amd.js create mode 100644 test/form/samples/implicit-undefined-assignments/_expected/cjs.js create mode 100644 test/form/samples/implicit-undefined-assignments/_expected/es.js create mode 100644 test/form/samples/implicit-undefined-assignments/_expected/iife.js create mode 100644 test/form/samples/implicit-undefined-assignments/_expected/umd.js create mode 100644 test/form/samples/implicit-undefined-assignments/main.js diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index 0a3a497ea22..aee99c4ed4d 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -10,10 +10,10 @@ export default class Property extends Node { return this.value.hasEffectsWhenAssigned( options ); } - initialiseAndDeclare ( parentScope, kind, init ) { + initialiseAndDeclare ( parentScope, kind ) { this.initialiseScope( parentScope ); this.key.initialise( parentScope ); - this.value.initialiseAndDeclare( parentScope, kind, init && UNKNOWN_ASSIGNMENT ); + this.value.initialiseAndDeclare( parentScope, kind, UNKNOWN_ASSIGNMENT ); } render ( code, es ) { diff --git a/src/ast/scopes/Scope.js b/src/ast/scopes/Scope.js index 3730f14b110..8bb99910193 100644 --- a/src/ast/scopes/Scope.js +++ b/src/ast/scopes/Scope.js @@ -1,6 +1,7 @@ import { blank, keys } from '../../utils/object.js'; import LocalVariable from '../variables/LocalVariable'; import ParameterVariable from '../variables/ParameterVariable'; +import { UNDEFINED_ASSIGNMENT } from '../values'; export default class Scope { constructor ( options = {} ) { @@ -20,7 +21,7 @@ export default class Scope { variable.addDeclaration( identifier ); init && variable.assignExpression( init ); } else { - this.variables[ name ] = new LocalVariable( identifier.name, identifier, init ); + this.variables[ name ] = new LocalVariable( identifier.name, identifier, init || UNDEFINED_ASSIGNMENT ); } } diff --git a/test/form/samples/implicit-undefined-assignments/_config.js b/test/form/samples/implicit-undefined-assignments/_config.js new file mode 100644 index 00000000000..03b89af6a1e --- /dev/null +++ b/test/form/samples/implicit-undefined-assignments/_config.js @@ -0,0 +1,5 @@ +var path = require('path'); + +module.exports = { + description: 'Make sure implicit undefined assignments in declarations are not ignored' +}; diff --git a/test/form/samples/implicit-undefined-assignments/_expected/amd.js b/test/form/samples/implicit-undefined-assignments/_expected/amd.js new file mode 100644 index 00000000000..db9113757df --- /dev/null +++ b/test/form/samples/implicit-undefined-assignments/_expected/amd.js @@ -0,0 +1,15 @@ +define(function () { 'use strict'; + + let a; + a(); + + let b; + b.foo = 'bar'; + + let { c } = {}; + c(); + + let { d } = {}; + d.foo = 'bar'; + +}); diff --git a/test/form/samples/implicit-undefined-assignments/_expected/cjs.js b/test/form/samples/implicit-undefined-assignments/_expected/cjs.js new file mode 100644 index 00000000000..83836f0ba55 --- /dev/null +++ b/test/form/samples/implicit-undefined-assignments/_expected/cjs.js @@ -0,0 +1,13 @@ +'use strict'; + +let a; +a(); + +let b; +b.foo = 'bar'; + +let { c } = {}; +c(); + +let { d } = {}; +d.foo = 'bar'; diff --git a/test/form/samples/implicit-undefined-assignments/_expected/es.js b/test/form/samples/implicit-undefined-assignments/_expected/es.js new file mode 100644 index 00000000000..8a0689db84d --- /dev/null +++ b/test/form/samples/implicit-undefined-assignments/_expected/es.js @@ -0,0 +1,11 @@ +let a; +a(); + +let b; +b.foo = 'bar'; + +let { c } = {}; +c(); + +let { d } = {}; +d.foo = 'bar'; diff --git a/test/form/samples/implicit-undefined-assignments/_expected/iife.js b/test/form/samples/implicit-undefined-assignments/_expected/iife.js new file mode 100644 index 00000000000..be372d793fa --- /dev/null +++ b/test/form/samples/implicit-undefined-assignments/_expected/iife.js @@ -0,0 +1,16 @@ +(function () { + 'use strict'; + + let a; + a(); + + let b; + b.foo = 'bar'; + + let { c } = {}; + c(); + + let { d } = {}; + d.foo = 'bar'; + +}()); diff --git a/test/form/samples/implicit-undefined-assignments/_expected/umd.js b/test/form/samples/implicit-undefined-assignments/_expected/umd.js new file mode 100644 index 00000000000..fa13e0b1720 --- /dev/null +++ b/test/form/samples/implicit-undefined-assignments/_expected/umd.js @@ -0,0 +1,19 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + let a; + a(); + + let b; + b.foo = 'bar'; + + let { c } = {}; + c(); + + let { d } = {}; + d.foo = 'bar'; + +}))); diff --git a/test/form/samples/implicit-undefined-assignments/main.js b/test/form/samples/implicit-undefined-assignments/main.js new file mode 100644 index 00000000000..8a0689db84d --- /dev/null +++ b/test/form/samples/implicit-undefined-assignments/main.js @@ -0,0 +1,11 @@ +let a; +a(); + +let b; +b.foo = 'bar'; + +let { c } = {}; +c(); + +let { d } = {}; +d.foo = 'bar'; From 30cb87e4d847f733135da8f8cfa6ac0680a688f1 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Fri, 15 Sep 2017 10:52:39 +0200 Subject: [PATCH 02/76] Refactor Scope.addDeclaration to receive an options object --- src/ast/nodes/Identifier.js | 4 ++-- src/ast/scopes/BlockScope.js | 8 ++++---- src/ast/scopes/Scope.js | 12 +++++++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index 962b656a336..8590c598c82 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -67,12 +67,12 @@ export default class Identifier extends Node { switch ( kind ) { case 'var': case 'function': - this.scope.addDeclaration( this, true, init ); + this.scope.addDeclaration( this, { isHoisted: true, init } ); break; case 'let': case 'const': case 'class': - this.scope.addDeclaration( this, false, init ); + this.scope.addDeclaration( this, { init } ); break; case 'parameter': this.scope.addParameterDeclaration( this ); diff --git a/src/ast/scopes/BlockScope.js b/src/ast/scopes/BlockScope.js index cb573dc487f..47beb8de142 100644 --- a/src/ast/scopes/BlockScope.js +++ b/src/ast/scopes/BlockScope.js @@ -1,11 +1,11 @@ import Scope from './Scope'; export default class BlockScope extends Scope { - addDeclaration ( identifier, isHoisted, init ) { - if ( isHoisted ) { - this.parent.addDeclaration( identifier, isHoisted, init ); + addDeclaration ( identifier, options = {} ) { + if ( options.isHoisted ) { + this.parent.addDeclaration( identifier, options ); } else { - super.addDeclaration( identifier, false, init ); + super.addDeclaration( identifier, options ); } } } diff --git a/src/ast/scopes/Scope.js b/src/ast/scopes/Scope.js index 8bb99910193..c8146198431 100644 --- a/src/ast/scopes/Scope.js +++ b/src/ast/scopes/Scope.js @@ -14,14 +14,20 @@ export default class Scope { this.variables = blank(); } - addDeclaration ( identifier, isHoisted, init ) { + /** + * @param identifier + * @param {Object} [options] - valid options are + * {(Node|null)} init + * {boolean} isHoisted + */ + addDeclaration ( identifier, options = {} ) { const name = identifier.name; if ( this.variables[ name ] ) { const variable = this.variables[ name ]; variable.addDeclaration( identifier ); - init && variable.assignExpression( init ); + options.init && variable.assignExpression( options.init ); } else { - this.variables[ name ] = new LocalVariable( identifier.name, identifier, init || UNDEFINED_ASSIGNMENT ); + this.variables[ name ] = new LocalVariable( identifier.name, identifier, options.init || UNDEFINED_ASSIGNMENT ); } } From aa9a311bbdf0880ebe5198c2ed0800d2fb5d8f52 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Fri, 15 Sep 2017 11:18:42 +0200 Subject: [PATCH 03/76] Refactor bindAssignment -> bindAssignmentAtPath --- src/ast/Node.js | 7 +++++-- src/ast/nodes/ArrayPattern.js | 4 ++-- src/ast/nodes/AssignmentExpression.js | 2 +- src/ast/nodes/AssignmentPattern.js | 6 +++--- src/ast/nodes/ForOfStatement.js | 2 +- src/ast/nodes/Identifier.js | 4 ++-- src/ast/nodes/ObjectPattern.js | 4 ++-- src/ast/nodes/Property.js | 4 ++-- src/ast/nodes/RestElement.js | 4 ++-- src/ast/nodes/VariableDeclaration.js | 4 ++-- src/ast/nodes/VariableDeclarator.js | 4 ++-- 11 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/ast/Node.js b/src/ast/Node.js index 8f6db8b969b..b96c7e73367 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -12,12 +12,15 @@ export default class Node { } /** - * Bind an expression as an assignment to a node. + * Bind an expression as an assignment to a node given an optional path. + * E.g., node.bindAssignmentAtPath(['x', 'y'], otherNode) is called if otherNode + * is assigned to node.x.y. * The default noop implementation is ok as long as hasEffectsWhenAssigned * always returns true for this node. Otherwise it should be overridden. + * @param {String[]} path * @param {Node} expression */ - bindAssignment () {} + bindAssignmentAtPath () {} /** * Binds ways a node is called to a node. Current options are: diff --git a/src/ast/nodes/ArrayPattern.js b/src/ast/nodes/ArrayPattern.js index 291b58df434..e0deb53cc32 100644 --- a/src/ast/nodes/ArrayPattern.js +++ b/src/ast/nodes/ArrayPattern.js @@ -2,8 +2,8 @@ import Node from '../Node.js'; import { UNKNOWN_ASSIGNMENT } from '../values'; export default class ArrayPattern extends Node { - bindAssignment () { - this.eachChild( child => child.bindAssignment( UNKNOWN_ASSIGNMENT ) ); + bindAssignmentAtPath () { + this.eachChild( child => child.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT ) ); } hasEffectsWhenAssigned ( options ) { diff --git a/src/ast/nodes/AssignmentExpression.js b/src/ast/nodes/AssignmentExpression.js index f8d48e150e3..4ba1317417a 100644 --- a/src/ast/nodes/AssignmentExpression.js +++ b/src/ast/nodes/AssignmentExpression.js @@ -5,7 +5,7 @@ export default class AssignmentExpression extends Node { bind () { super.bind(); disallowIllegalReassignment( this.scope, this.left ); - this.left.bindAssignment( this.right ); + this.left.bindAssignmentAtPath( [], this.right ); } hasEffects ( options ) { diff --git a/src/ast/nodes/AssignmentPattern.js b/src/ast/nodes/AssignmentPattern.js index 7f36eb694d0..78dba7bfeb4 100644 --- a/src/ast/nodes/AssignmentPattern.js +++ b/src/ast/nodes/AssignmentPattern.js @@ -3,11 +3,11 @@ import Node from '../Node.js'; export default class AssignmentPattern extends Node { bind () { super.bind(); - this.left.bindAssignment( this.right ); + this.left.bindAssignmentAtPath( [], this.right ); } - bindAssignment ( expression ) { - this.left.bindAssignment( expression ); + bindAssignmentAtPath ( path, expression ) { + this.left.bindAssignmentAtPath( path, expression ); } hasEffectsWhenAssigned ( options ) { diff --git a/src/ast/nodes/ForOfStatement.js b/src/ast/nodes/ForOfStatement.js index 5117bf4d3a4..61799d3027e 100644 --- a/src/ast/nodes/ForOfStatement.js +++ b/src/ast/nodes/ForOfStatement.js @@ -5,7 +5,7 @@ import { UNKNOWN_ASSIGNMENT } from '../values'; export default class ForOfStatement extends Statement { bind () { super.bind(); - this.left.bindAssignment( UNKNOWN_ASSIGNMENT ); + this.left.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT ); } hasEffects ( options ) { diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index 8590c598c82..8b7e5264fb3 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -24,8 +24,8 @@ export default class Identifier extends Node { } } - bindAssignment ( expression ) { - if ( this.variable ) { + bindAssignmentAtPath ( path, expression ) { + if ( this.variable && path.length === 0 ) { this.variable.assignExpression( expression ); } } diff --git a/src/ast/nodes/ObjectPattern.js b/src/ast/nodes/ObjectPattern.js index 4c36e48e767..6fab170abe3 100644 --- a/src/ast/nodes/ObjectPattern.js +++ b/src/ast/nodes/ObjectPattern.js @@ -1,8 +1,8 @@ import Node from '../Node.js'; export default class ObjectPattern extends Node { - bindAssignment ( expression ) { - this.properties.forEach( child => child.bindAssignment( expression ) ); + bindAssignmentAtPath ( path, expression ) { + this.properties.forEach( child => child.bindAssignmentAtPath( path, expression ) ); } hasEffectsWhenAssigned ( options ) { diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index aee99c4ed4d..af8fbefa2f4 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -2,8 +2,8 @@ import Node from '../Node.js'; import { UNKNOWN_ASSIGNMENT } from '../values'; export default class Property extends Node { - bindAssignment () { - this.value.bindAssignment( UNKNOWN_ASSIGNMENT ); + bindAssignmentAtPath () { + this.value.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT ); } hasEffectsWhenAssigned ( options ) { diff --git a/src/ast/nodes/RestElement.js b/src/ast/nodes/RestElement.js index 81f6bd443a3..85fee0aa27d 100644 --- a/src/ast/nodes/RestElement.js +++ b/src/ast/nodes/RestElement.js @@ -2,8 +2,8 @@ import Node from '../Node.js'; import { UNKNOWN_ASSIGNMENT } from '../values'; export default class RestElement extends Node { - bindAssignment () { - this.argument.bindAssignment( UNKNOWN_ASSIGNMENT ); + bindAssignmentAtPath () { + this.argument.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT ); } hasEffectsWhenAssigned ( options ) { diff --git a/src/ast/nodes/VariableDeclaration.js b/src/ast/nodes/VariableDeclaration.js index 760e5df1c13..20a0084e94d 100644 --- a/src/ast/nodes/VariableDeclaration.js +++ b/src/ast/nodes/VariableDeclaration.js @@ -18,8 +18,8 @@ function getSeparator ( code, start ) { const forStatement = /^For(?:Of|In)?Statement/; export default class VariableDeclaration extends Node { - bindAssignment () { - this.eachChild( child => child.bindAssignment( UNKNOWN_ASSIGNMENT ) ); + bindAssignmentAtPath () { + this.eachChild( child => child.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT ) ); } hasEffectsWhenAssigned () { diff --git a/src/ast/nodes/VariableDeclarator.js b/src/ast/nodes/VariableDeclarator.js index 1c52d08f027..dae2d2e432e 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 { - bindAssignment ( expression ) { - this.id.bindAssignment( expression ); + bindAssignmentAtPath ( path, expression ) { + this.id.bindAssignmentAtPath( path, expression ); } initialiseDeclarator ( parentScope, kind ) { From efef49f59acaa268f7e6d7f7cc7094b2a3c5fe69 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Fri, 15 Sep 2017 12:09:06 +0200 Subject: [PATCH 04/76] Refactor assignExpression -> assignExpressionAtPath --- src/ast/nodes/Identifier.js | 4 ++-- src/ast/nodes/shared/FunctionNode.js | 4 ++-- src/ast/scopes/Scope.js | 2 +- src/ast/variables/GlobalVariable.js | 2 -- src/ast/variables/LocalVariable.js | 12 ++++++------ src/ast/variables/NamespaceVariable.js | 2 -- src/ast/variables/Variable.js | 4 ++++ 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index 8b7e5264fb3..24a3152e325 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -25,8 +25,8 @@ export default class Identifier extends Node { } bindAssignmentAtPath ( path, expression ) { - if ( this.variable && path.length === 0 ) { - this.variable.assignExpression( expression ); + if ( this.variable ) { + this.variable.assignExpressionAtPath( path, expression ); } } diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index 95831ac6f60..ef0bdc5c9ef 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -7,9 +7,9 @@ export default class FunctionNode extends Node { const thisVariable = this.scope.findVariable( 'this' ); if ( withNew ) { - thisVariable.assignExpression( UNKNOWN_OBJECT_LITERAL ); + thisVariable.assignExpressionAtPath( [], UNKNOWN_OBJECT_LITERAL ); } else { - thisVariable.assignExpression( UNKNOWN_ASSIGNMENT ); + thisVariable.assignExpressionAtPath( [], UNKNOWN_ASSIGNMENT ); } } diff --git a/src/ast/scopes/Scope.js b/src/ast/scopes/Scope.js index c8146198431..d64c91854db 100644 --- a/src/ast/scopes/Scope.js +++ b/src/ast/scopes/Scope.js @@ -25,7 +25,7 @@ export default class Scope { if ( this.variables[ name ] ) { const variable = this.variables[ name ]; variable.addDeclaration( identifier ); - options.init && variable.assignExpression( options.init ); + options.init && variable.assignExpressionAtPath( [], options.init ); } else { this.variables[ name ] = new LocalVariable( identifier.name, identifier, options.init || UNDEFINED_ASSIGNMENT ); } diff --git a/src/ast/variables/GlobalVariable.js b/src/ast/variables/GlobalVariable.js index 9c516d80cf8..6a568605bed 100644 --- a/src/ast/variables/GlobalVariable.js +++ b/src/ast/variables/GlobalVariable.js @@ -14,8 +14,6 @@ export default class GlobalVariable extends Variable { if ( reference.isReassignment ) this.isReassigned = true; } - assignExpression () {} - hasEffectsWhenCalled () { return !pureFunctions[ this.name ]; } diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index e5291985582..cd28321fe64 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -21,12 +21,12 @@ export default class LocalVariable extends Variable { Array.from( this.assignedExpressions ).forEach( expression => expression.bindCall( callOptions ) ); } - addReference () {} - - assignExpression ( expression ) { - this.assignedExpressions.add( expression ); - this.isReassigned = true; - Array.from( this.calls ).forEach( callOptions => expression.bindCall( callOptions ) ); + assignExpressionAtPath ( path, expression ) { + if ( path.length === 0 ) { + this.assignedExpressions.add( expression ); + this.isReassigned = true; + Array.from( this.calls ).forEach( callOptions => expression.bindCall( callOptions ) ); + } } getName ( es ) { diff --git a/src/ast/variables/NamespaceVariable.js b/src/ast/variables/NamespaceVariable.js index 19fb6663b77..194cf4f6b63 100644 --- a/src/ast/variables/NamespaceVariable.js +++ b/src/ast/variables/NamespaceVariable.js @@ -19,8 +19,6 @@ export default class NamespaceVariable extends Variable { this.name = node.name; } - assignExpression () {} - includeVariable () { const hasBeenIncluded = super.includeVariable(); if ( hasBeenIncluded ) { diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index 893ce18eb7d..94c180761f1 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -5,6 +5,10 @@ export default class Variable { addCall () {} + addReference () {} + + assignExpressionAtPath () {} + getName () { return this.name; } From c309a4986abb83a6e9aaf28646e9cbd504af97d1 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Fri, 15 Sep 2017 15:01:47 +0200 Subject: [PATCH 05/76] Refactor hasEffectsWhenAssigned -> hasEffectsWhenAssignedAtPath --- src/ast/Node.js | 5 +++-- src/ast/nodes/ArrayPattern.js | 7 +++++-- src/ast/nodes/AssignmentExpression.js | 2 +- src/ast/nodes/AssignmentPattern.js | 7 +++++-- src/ast/nodes/ForInStatement.js | 2 +- src/ast/nodes/ForOfStatement.js | 2 +- src/ast/nodes/Identifier.js | 7 +++++-- src/ast/nodes/MemberExpression.js | 2 +- src/ast/nodes/ObjectPattern.js | 7 +++++-- src/ast/nodes/Property.js | 7 +++++-- src/ast/nodes/RestElement.js | 4 ++-- src/ast/nodes/ThisExpression.js | 7 +++++++ src/ast/nodes/UpdateExpression.js | 2 +- src/ast/nodes/VariableDeclaration.js | 2 +- 14 files changed, 43 insertions(+), 20 deletions(-) diff --git a/src/ast/Node.js b/src/ast/Node.js index b96c7e73367..c8bd2fc08da 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -15,7 +15,7 @@ export default class Node { * Bind an expression as an assignment to a node given an optional path. * E.g., node.bindAssignmentAtPath(['x', 'y'], otherNode) is called if otherNode * is assigned to node.x.y. - * The default noop implementation is ok as long as hasEffectsWhenAssigned + * The default noop implementation is ok as long as hasEffectsWhenAssignedAtPath * always returns true for this node. Otherwise it should be overridden. * @param {String[]} path * @param {Node} expression @@ -73,10 +73,11 @@ export default class Node { } /** + * @param {String[]} path * @param {ExecutionPathOptions} options * @return {boolean} */ - hasEffectsWhenAssigned () { + hasEffectsWhenAssignedAtPath () { return true; } diff --git a/src/ast/nodes/ArrayPattern.js b/src/ast/nodes/ArrayPattern.js index e0deb53cc32..5891f7bccb4 100644 --- a/src/ast/nodes/ArrayPattern.js +++ b/src/ast/nodes/ArrayPattern.js @@ -6,8 +6,11 @@ export default class ArrayPattern extends Node { this.eachChild( child => child.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT ) ); } - hasEffectsWhenAssigned ( options ) { - return this.someChild( child => child.hasEffectsWhenAssigned( options ) ); + hasEffectsWhenAssignedAtPath ( path, options ) { + if ( path.length > 0 ) { + return true; + } + return this.someChild( child => child.hasEffectsWhenAssignedAtPath( [], options ) ); } initialiseAndDeclare ( parentScope, kind ) { diff --git a/src/ast/nodes/AssignmentExpression.js b/src/ast/nodes/AssignmentExpression.js index 4ba1317417a..c722953a631 100644 --- a/src/ast/nodes/AssignmentExpression.js +++ b/src/ast/nodes/AssignmentExpression.js @@ -9,7 +9,7 @@ export default class AssignmentExpression extends Node { } hasEffects ( options ) { - return super.hasEffects( options ) || this.left.hasEffectsWhenAssigned( options ); + return super.hasEffects( options ) || this.left.hasEffectsWhenAssignedAtPath( [], options ); } hasEffectsAsExpressionStatement ( options ) { diff --git a/src/ast/nodes/AssignmentPattern.js b/src/ast/nodes/AssignmentPattern.js index 78dba7bfeb4..73ba49730cd 100644 --- a/src/ast/nodes/AssignmentPattern.js +++ b/src/ast/nodes/AssignmentPattern.js @@ -10,8 +10,11 @@ export default class AssignmentPattern extends Node { this.left.bindAssignmentAtPath( path, expression ); } - hasEffectsWhenAssigned ( options ) { - return this.left.hasEffectsWhenAssigned( options ); + hasEffectsWhenAssignedAtPath ( path, options ) { + if ( path.length > 0 ) { + return true; + } + return this.left.hasEffectsWhenAssignedAtPath( [], options ); } initialiseAndDeclare ( parentScope, kind, init ) { diff --git a/src/ast/nodes/ForInStatement.js b/src/ast/nodes/ForInStatement.js index ac5843f04d1..b4e870ceb15 100644 --- a/src/ast/nodes/ForInStatement.js +++ b/src/ast/nodes/ForInStatement.js @@ -5,7 +5,7 @@ export default class ForInStatement extends Statement { hasEffects ( options ) { return ( this.included - || this.left && (this.left.hasEffects( options ) || this.left.hasEffectsWhenAssigned( options )) + || this.left && (this.left.hasEffects( options ) || this.left.hasEffectsWhenAssignedAtPath( [], options )) || this.right && this.right.hasEffects( options ) || this.body.hasEffects( options.setIgnoreBreakStatements() ) ); diff --git a/src/ast/nodes/ForOfStatement.js b/src/ast/nodes/ForOfStatement.js index 61799d3027e..8f68cc6d396 100644 --- a/src/ast/nodes/ForOfStatement.js +++ b/src/ast/nodes/ForOfStatement.js @@ -11,7 +11,7 @@ export default class ForOfStatement extends Statement { hasEffects ( options ) { return ( this.included - || this.left && (this.left.hasEffects( options ) || this.left.hasEffectsWhenAssigned( options )) + || this.left && (this.left.hasEffects( options ) || this.left.hasEffectsWhenAssignedAtPath( [], options )) || this.right && this.right.hasEffects( options ) || this.body.hasEffects( options.setIgnoreBreakStatements() ) ); diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index 24a3152e325..01065512d83 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -40,8 +40,11 @@ export default class Identifier extends Node { return this.hasEffects( options ) || this.variable.isGlobal; } - hasEffectsWhenAssigned () { - return this.variable && this.variable.included; + hasEffectsWhenAssignedAtPath ( path, options ) { + if ( path.length === 0 ) { + return this.variable && this.variable.included; + } + return this.hasEffectsWhenMutated( options ); } hasEffectsWhenCalled ( options ) { diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index ca41dc8f779..98cc9f21650 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -83,7 +83,7 @@ export default class MemberExpression extends Node { } } - hasEffectsWhenAssigned ( options ) { + hasEffectsWhenAssignedAtPath ( path, options ) { return this.object.hasEffectsWhenMutated( options ); } diff --git a/src/ast/nodes/ObjectPattern.js b/src/ast/nodes/ObjectPattern.js index 6fab170abe3..0ce95596ea1 100644 --- a/src/ast/nodes/ObjectPattern.js +++ b/src/ast/nodes/ObjectPattern.js @@ -5,8 +5,11 @@ export default class ObjectPattern extends Node { this.properties.forEach( child => child.bindAssignmentAtPath( path, expression ) ); } - hasEffectsWhenAssigned ( options ) { - return this.someChild( child => child.hasEffectsWhenAssigned( options ) ); + hasEffectsWhenAssignedAtPath ( path, options ) { + if ( path.length > 0 ) { + return true; + } + return 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 af8fbefa2f4..ab99f8adffb 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -6,8 +6,11 @@ export default class Property extends Node { this.value.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT ); } - hasEffectsWhenAssigned ( options ) { - return this.value.hasEffectsWhenAssigned( options ); + hasEffectsWhenAssignedAtPath ( path, options ) { + if ( path.length > 0 ) { + return true; + } + return this.value.hasEffectsWhenAssignedAtPath( [], options ); } initialiseAndDeclare ( parentScope, kind ) { diff --git a/src/ast/nodes/RestElement.js b/src/ast/nodes/RestElement.js index 85fee0aa27d..65db3d87f1c 100644 --- a/src/ast/nodes/RestElement.js +++ b/src/ast/nodes/RestElement.js @@ -6,8 +6,8 @@ export default class RestElement extends Node { this.argument.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT ); } - hasEffectsWhenAssigned ( options ) { - return this.argument.hasEffectsWhenAssigned( options ); + hasEffectsWhenAssignedAtPath ( path, options ) { + return this.argument.hasEffectsWhenAssignedAtPath( path, options ); } initialiseAndDeclare ( parentScope, kind ) { diff --git a/src/ast/nodes/ThisExpression.js b/src/ast/nodes/ThisExpression.js index d7ecd024613..0b9e993d63a 100644 --- a/src/ast/nodes/ThisExpression.js +++ b/src/ast/nodes/ThisExpression.js @@ -20,6 +20,13 @@ export default class ThisExpression extends Node { this.variable = this.scope.findVariable( 'this' ); } + hasEffectsWhenAssignedAtPath ( path, options ) { + if ( path.length === 0 ) { + return true; + } + return this.hasEffectsWhenMutated( options ); + } + hasEffectsWhenMutated ( options ) { return !options.ignoreSafeThisMutations() || this.variable.hasEffectsWhenMutated( options ); } diff --git a/src/ast/nodes/UpdateExpression.js b/src/ast/nodes/UpdateExpression.js index f14fc84a3fe..8f05dedf063 100644 --- a/src/ast/nodes/UpdateExpression.js +++ b/src/ast/nodes/UpdateExpression.js @@ -12,7 +12,7 @@ export default class UpdateExpression extends Node { } hasEffects ( options ) { - return this.included || this.argument.hasEffectsWhenAssigned( options ); + return this.included || this.argument.hasEffectsWhenAssignedAtPath( [], options ); } hasEffectsAsExpressionStatement ( options ) { diff --git a/src/ast/nodes/VariableDeclaration.js b/src/ast/nodes/VariableDeclaration.js index 20a0084e94d..8f0246e15f9 100644 --- a/src/ast/nodes/VariableDeclaration.js +++ b/src/ast/nodes/VariableDeclaration.js @@ -22,7 +22,7 @@ export default class VariableDeclaration extends Node { this.eachChild( child => child.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT ) ); } - hasEffectsWhenAssigned () { + hasEffectsWhenAssignedAtPath () { return false; } From 8af661426a6a35c26ed6f8cb6080359d04ccbb4c Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Fri, 15 Sep 2017 16:24:59 +0200 Subject: [PATCH 06/76] Introduce immutable.js; even though this may have a slight performance impact at some points, this will give us the power to have much more complex structures in the execution path options in the near and far future while keeping transformations easy to reason about. --- package-lock.json | 6 ++++++ package.json | 1 + src/ast/ExecutionPathOptions.js | 27 +++++++++++++++------------ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0886e99c6c9..9d48c758f85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2436,6 +2436,12 @@ "integrity": "sha512-JLH93mL8amZQhh/p6mfQgVBH3M6epNq3DfsXsTSuSrInVjwyYlFE1nv2AgfRCC8PoOhM0jwQ5v8s9LgbK7yGDw==", "dev": true }, + "immutable": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.1.tgz", + "integrity": "sha1-IAgH8Rqw9ycQ6khVQt4IgHX2jNI=", + "dev": true + }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", diff --git a/package.json b/package.json index 2162c1740d3..b16bf3b9cd3 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "date-time": "^2.1.0", "eslint": "^4.4.1", "eslint-plugin-import": "^2.2.0", + "immutable": "^3.8.1", "is-reference": "^1.0.0", "istanbul": "^0.4.3", "locate-character": "^2.0.0", diff --git a/src/ast/ExecutionPathOptions.js b/src/ast/ExecutionPathOptions.js index fb93e7ddb23..db3ed50179b 100644 --- a/src/ast/ExecutionPathOptions.js +++ b/src/ast/ExecutionPathOptions.js @@ -1,3 +1,5 @@ +import Immutable from 'immutable'; + const OPTION_IGNORE_BREAK_STATEMENTS = 'IGNORE_BREAK_STATEMENTS'; const OPTION_IGNORE_RETURN_AWAIT_YIELD = 'IGNORE_RETURN_AWAIT_YIELD'; const OPTION_IGNORE_SAFE_THIS_MUTATIONS = 'IGNORE_SAFE_THIS_MUTATIONS'; @@ -11,7 +13,7 @@ export default class ExecutionPathOptions { * @returns {ExecutionPathOptions} */ static create () { - return new this( {} ); + return new this( Immutable.Map() ); } constructor ( optionValues ) { @@ -26,7 +28,11 @@ export default class ExecutionPathOptions { * @returns {ExecutionPathOptions} A new options instance */ set ( option, value ) { - return new this.constructor( Object.assign( {}, this._optionValues, { [option]: value } ) ); + return new this.constructor( this._optionValues.set( option, value ) ); + } + + setIn ( optionPath, value ) { + return new this.constructor( this._optionValues.setIn( optionPath, value ) ); } /** @@ -34,7 +40,7 @@ export default class ExecutionPathOptions { * @returns {*} Its value */ get ( option ) { - return this._optionValues[ option ]; + return this._optionValues.get( option ); } /** @@ -57,8 +63,7 @@ export default class ExecutionPathOptions { * @return {boolean} */ ignoreLabel ( labelName ) { - const ignoredLabels = this.get( IGNORED_LABELS ); - return ignoredLabels && ignoredLabels.has( labelName ); + return this._optionValues.getIn( [ IGNORED_LABELS, labelName ] ); } /** @@ -66,7 +71,7 @@ export default class ExecutionPathOptions { * @return {ExecutionPathOptions} */ setIgnoreLabel ( labelName ) { - return this.set( IGNORED_LABELS, new Set( this.get( IGNORED_LABELS ) ).add( labelName ) ); + return this.setIn( [ IGNORED_LABELS, labelName ], true ); } /** @@ -111,7 +116,7 @@ export default class ExecutionPathOptions { * @return {ExecutionPathOptions} */ addMutatedNode ( node ) { - return this.set( OPTION_MUTATED_NODES, new Set( this.get( OPTION_MUTATED_NODES ) ).add( node ) ); + return this.setIn( [ OPTION_MUTATED_NODES, node ], true ); } /** @@ -119,8 +124,7 @@ export default class ExecutionPathOptions { * @return {boolean} */ hasNodeBeenMutated ( node ) { - const mutatedNodes = this.get( OPTION_MUTATED_NODES ); - return mutatedNodes && mutatedNodes.has( node ); + return this._optionValues.getIn( [ OPTION_MUTATED_NODES, node ] ); } /** @@ -128,7 +132,7 @@ export default class ExecutionPathOptions { * @return {ExecutionPathOptions} */ addCalledNode ( node ) { - return this.set( OPTION_CALLED_NODES, new Set( this.get( OPTION_CALLED_NODES ) ).add( node ) ); + return this.setIn( [ OPTION_CALLED_NODES, node ], true ); } /** @@ -136,8 +140,7 @@ export default class ExecutionPathOptions { * @return {boolean} */ hasNodeBeenCalled ( node ) { - const calledNodes = this.get( OPTION_CALLED_NODES ); - return calledNodes && calledNodes.has( node ); + return this._optionValues.getIn( [ OPTION_CALLED_NODES, node ] ); } /** From 30f0a37d6b2d16e812626c6fed806d3fe6abc8d2 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Mon, 18 Sep 2017 07:27:40 +0200 Subject: [PATCH 07/76] Allow for mutated nodes to have a path in the ExecutionPathOptions --- src/ast/ExecutionPathOptions.js | 12 ++++++++---- src/ast/variables/LocalVariable.js | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/ast/ExecutionPathOptions.js b/src/ast/ExecutionPathOptions.js index db3ed50179b..eb9b2c10edc 100644 --- a/src/ast/ExecutionPathOptions.js +++ b/src/ast/ExecutionPathOptions.js @@ -7,6 +7,8 @@ const OPTION_CALLED_NODES = 'CALLED_NODES'; const OPTION_MUTATED_NODES = 'MUTATED_NODES'; const IGNORED_LABELS = 'IGNORED_LABELS'; +const RESULT_KEY = {}; + /** Wrapper to ensure immutability */ export default class ExecutionPathOptions { /** @@ -112,19 +114,21 @@ export default class ExecutionPathOptions { } /** + * @param {String[]} path * @param {Node} node * @return {ExecutionPathOptions} */ - addMutatedNode ( node ) { - return this.setIn( [ OPTION_MUTATED_NODES, node ], true ); + addMutatedNodeAtPath ( path, node ) { + return this.setIn( [ OPTION_MUTATED_NODES, node, ...path, RESULT_KEY ], true ); } /** + * @param {String[]} path * @param {Node} node * @return {boolean} */ - hasNodeBeenMutated ( node ) { - return this._optionValues.getIn( [ OPTION_MUTATED_NODES, node ] ); + hasNodeBeenMutatedAtPath ( path, node ) { + return this._optionValues.getIn( [ OPTION_MUTATED_NODES, node, ...path, RESULT_KEY ] ); } /** diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index cd28321fe64..f25762d830a 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -46,8 +46,8 @@ export default class LocalVariable extends Variable { hasEffectsWhenMutated ( options ) { return this.included || Array.from( this.assignedExpressions ).some( node => - !options.hasNodeBeenMutated( node ) && - node.hasEffectsWhenMutated( options.addMutatedNode( node ) ) + !options.hasNodeBeenMutatedAtPath( [], node ) && + node.hasEffectsWhenMutated( options.addMutatedNodeAtPath( [], node ) ) ); } From 647d7bcce32c5b09ba9501da14e62635163903a8 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Tue, 19 Sep 2017 07:42:56 +0200 Subject: [PATCH 08/76] Refactor hasEffectsWhenMutated -> hasEffectsWhenMutatedAtPath --- src/ast/Node.js | 3 ++- src/ast/nodes/ArrowFunctionExpression.js | 4 ++-- src/ast/nodes/Identifier.js | 14 ++++++++++---- src/ast/nodes/Literal.js | 4 ++-- src/ast/nodes/LogicalExpression.js | 11 ++++++----- src/ast/nodes/MemberExpression.js | 2 +- src/ast/nodes/ObjectExpression.js | 4 ++-- src/ast/nodes/ThisExpression.js | 9 ++++++--- src/ast/nodes/UnaryExpression.js | 2 +- src/ast/nodes/shared/FunctionNode.js | 4 ++-- src/ast/values.js | 6 +++--- src/ast/variables/LocalVariable.js | 6 +++--- src/ast/variables/Variable.js | 2 +- 13 files changed, 41 insertions(+), 30 deletions(-) diff --git a/src/ast/Node.js b/src/ast/Node.js index c8bd2fc08da..8a1d07cedc4 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -90,10 +90,11 @@ export default class Node { } /** + * @param {String[]} path * @param {ExecutionPathOptions} options * @return {boolean} */ - hasEffectsWhenMutated () { + hasEffectsWhenMutatedAtPath () { return true; } diff --git a/src/ast/nodes/ArrowFunctionExpression.js b/src/ast/nodes/ArrowFunctionExpression.js index 76a16a47dda..4647e768ed6 100644 --- a/src/ast/nodes/ArrowFunctionExpression.js +++ b/src/ast/nodes/ArrowFunctionExpression.js @@ -14,8 +14,8 @@ export default class ArrowFunctionExpression extends Node { || this.body.hasEffects( options ); } - hasEffectsWhenMutated () { - return this.included; + hasEffectsWhenMutatedAtPath ( path ) { + return this.included || path.length > 0; } initialiseChildren () { diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index 01065512d83..73532662bc5 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -41,10 +41,13 @@ export default class Identifier extends Node { } hasEffectsWhenAssignedAtPath ( path, options ) { + if ( !this.variable ) { + return true; + } if ( path.length === 0 ) { - return this.variable && this.variable.included; + return this.variable.included; } - return this.hasEffectsWhenMutated( options ); + return this.hasEffectsWhenMutatedAtPath( path.slice( 0, -1 ), options ); } hasEffectsWhenCalled ( options ) { @@ -54,8 +57,11 @@ export default class Identifier extends Node { return this.variable.hasEffectsWhenCalled( options ); } - hasEffectsWhenMutated ( options ) { - return this.variable && this.variable.hasEffectsWhenMutated( options ); + hasEffectsWhenMutatedAtPath ( path, options ) { + if ( !this.variable ) { + return true; + } + return this.variable.hasEffectsWhenMutatedAtPath( path, options ); } includeInBundle () { diff --git a/src/ast/nodes/Literal.js b/src/ast/nodes/Literal.js index a4664cf9973..168461b6b87 100644 --- a/src/ast/nodes/Literal.js +++ b/src/ast/nodes/Literal.js @@ -5,8 +5,8 @@ export default class Literal extends Node { return this.value; } - hasEffectsWhenMutated () { - return false; + hasEffectsWhenMutatedAtPath ( path ) { + return path.length > 0; } render ( code ) { diff --git a/src/ast/nodes/LogicalExpression.js b/src/ast/nodes/LogicalExpression.js index 1a4b5b1fe74..4e89f4e1cf2 100644 --- a/src/ast/nodes/LogicalExpression.js +++ b/src/ast/nodes/LogicalExpression.js @@ -17,14 +17,15 @@ export default class LogicalExpression extends Node { return operators[ this.operator ]( leftValue, rightValue ); } - hasEffectsWhenMutated ( options ) { + hasEffectsWhenMutatedAtPath ( path, options ) { const leftValue = this.left.getValue(); if ( leftValue === UNKNOWN_VALUE ) { - return this.left.hasEffectsWhenMutated( options ) || this.right.hasEffectsWhenMutated( options ); + return this.left.hasEffectsWhenMutatedAtPath( path, options ) + || this.right.hasEffectsWhenMutatedAtPath( path, options ); } - if ((leftValue && this.operator === '||') || (!leftValue && this.operator === '&&')) { - return this.left.hasEffectsWhenMutated( options ); + if ( (leftValue && this.operator === '||') || (!leftValue && this.operator === '&&') ) { + return this.left.hasEffectsWhenMutatedAtPath( path, options ); } - return this.right.hasEffectsWhenMutated( options ); + return this.right.hasEffectsWhenMutatedAtPath( path, options ); } } diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index 98cc9f21650..a1faab9a53e 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -84,7 +84,7 @@ export default class MemberExpression extends Node { } hasEffectsWhenAssignedAtPath ( path, options ) { - return this.object.hasEffectsWhenMutated( options ); + return this.object.hasEffectsWhenMutatedAtPath( path, options ); } includeInBundle () { diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index 300e56b5ccc..63566169a76 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -1,7 +1,7 @@ import Node from '../Node.js'; export default class ObjectExpression extends Node { - hasEffectsWhenMutated () { - return false; + hasEffectsWhenMutatedAtPath ( path ) { + return path.length > 1; } } diff --git a/src/ast/nodes/ThisExpression.js b/src/ast/nodes/ThisExpression.js index 0b9e993d63a..46e2d94db66 100644 --- a/src/ast/nodes/ThisExpression.js +++ b/src/ast/nodes/ThisExpression.js @@ -24,11 +24,14 @@ export default class ThisExpression extends Node { if ( path.length === 0 ) { return true; } - return this.hasEffectsWhenMutated( options ); + return this.hasEffectsWhenMutatedAtPath( path, options ); } - hasEffectsWhenMutated ( options ) { - return !options.ignoreSafeThisMutations() || this.variable.hasEffectsWhenMutated( options ); + hasEffectsWhenMutatedAtPath ( path, options ) { + if ( path.length === 0 ) { + return !options.ignoreSafeThisMutations() || this.variable.hasEffectsWhenMutatedAtPath( [], options ); + } + return this.variable.hasEffectsWhenMutatedAtPath( path, options ); } render ( code ) { diff --git a/src/ast/nodes/UnaryExpression.js b/src/ast/nodes/UnaryExpression.js index a453201cb68..cad05977405 100644 --- a/src/ast/nodes/UnaryExpression.js +++ b/src/ast/nodes/UnaryExpression.js @@ -28,7 +28,7 @@ export default class UnaryExpression extends Node { || this.argument.hasEffects( options ) || (this.operator === 'delete' && ( this.argument.type !== 'MemberExpression' - || this.argument.object.hasEffectsWhenMutated( options ) + || this.argument.object.hasEffectsWhenMutatedAtPath( [], options ) )); } diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index ef0bdc5c9ef..c3dad9b008d 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -27,8 +27,8 @@ export default class FunctionNode extends Node { || this.body.hasEffects( innerOptions ); } - hasEffectsWhenMutated () { - return this.included; + hasEffectsWhenMutatedAtPath ( path ) { + return this.included || path.length > 0; } initialiseScope ( parentScope ) { diff --git a/src/ast/values.js b/src/ast/values.js index 9de98dd9d6e..d9367ba0089 100644 --- a/src/ast/values.js +++ b/src/ast/values.js @@ -4,19 +4,19 @@ export const UNKNOWN_ASSIGNMENT = { type: 'UNKNOWN', bindCall: () => {}, hasEffectsWhenCalled: () => true, - hasEffectsWhenMutated: () => true, + hasEffectsWhenMutatedAtPath: () => true, }; export const UNDEFINED_ASSIGNMENT = { type: 'UNDEFINED', bindCall: () => {}, hasEffectsWhenCalled: () => true, - hasEffectsWhenMutated: () => true, + hasEffectsWhenMutatedAtPath: () => true, }; export const UNKNOWN_OBJECT_LITERAL = { type: 'UNKNOWN_OBJECT_LITERAL', bindCall: () => {}, hasEffectsWhenCalled: () => true, - hasEffectsWhenMutated: () => false, + hasEffectsWhenMutatedAtPath: path => path.length > 0, }; diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index f25762d830a..39b85aafb67 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -43,11 +43,11 @@ export default class LocalVariable extends Variable { ); } - hasEffectsWhenMutated ( options ) { + hasEffectsWhenMutatedAtPath ( path, options ) { return this.included || Array.from( this.assignedExpressions ).some( node => - !options.hasNodeBeenMutatedAtPath( [], node ) && - node.hasEffectsWhenMutated( options.addMutatedNodeAtPath( [], node ) ) + !options.hasNodeBeenMutatedAtPath( path, node ) && + node.hasEffectsWhenMutatedAtPath( path, options.addMutatedNodeAtPath( path, node ) ) ); } diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index 94c180761f1..9550e57d652 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -17,7 +17,7 @@ export default class Variable { return true; } - hasEffectsWhenMutated () { + hasEffectsWhenMutatedAtPath () { return true; } From 3be969889076ce8166ec67443334d43b8f466f63 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Wed, 20 Sep 2017 07:12:01 +0200 Subject: [PATCH 09/76] Ignore object member mutations for existing members on objects that are not included --- src/ast/ExecutionPathOptions.js | 19 +++++++++++++++++ src/ast/nodes/ArrowFunctionExpression.js | 7 +++++++ src/ast/nodes/Identifier.js | 21 ++++++------------- src/ast/nodes/LogicalExpression.js | 5 +++++ src/ast/nodes/MemberExpression.js | 5 ++++- src/ast/nodes/ObjectExpression.js | 10 +++++++++ src/ast/nodes/Property.js | 5 +---- src/ast/nodes/ThisExpression.js | 2 +- src/ast/nodes/shared/FunctionNode.js | 7 +++++++ src/ast/values.js | 3 +++ src/ast/variables/LocalVariable.js | 8 +++++++ src/ast/variables/Variable.js | 4 ++++ .../_config.js | 4 ++++ .../_expected/amd.js | 14 +++++++++++++ .../_expected/cjs.js | 12 +++++++++++ .../_expected/es.js | 8 +++++++ .../_expected/iife.js | 15 +++++++++++++ .../_expected/umd.js | 18 ++++++++++++++++ .../main.js | 10 +++++++++ 19 files changed, 156 insertions(+), 21 deletions(-) create mode 100644 test/form/samples/side-effects-object-literal-mutation/_config.js create mode 100644 test/form/samples/side-effects-object-literal-mutation/_expected/amd.js create mode 100644 test/form/samples/side-effects-object-literal-mutation/_expected/cjs.js create mode 100644 test/form/samples/side-effects-object-literal-mutation/_expected/es.js create mode 100644 test/form/samples/side-effects-object-literal-mutation/_expected/iife.js create mode 100644 test/form/samples/side-effects-object-literal-mutation/_expected/umd.js create mode 100644 test/form/samples/side-effects-object-literal-mutation/main.js diff --git a/src/ast/ExecutionPathOptions.js b/src/ast/ExecutionPathOptions.js index eb9b2c10edc..17883bd81f4 100644 --- a/src/ast/ExecutionPathOptions.js +++ b/src/ast/ExecutionPathOptions.js @@ -5,6 +5,7 @@ const OPTION_IGNORE_RETURN_AWAIT_YIELD = 'IGNORE_RETURN_AWAIT_YIELD'; const OPTION_IGNORE_SAFE_THIS_MUTATIONS = 'IGNORE_SAFE_THIS_MUTATIONS'; const OPTION_CALLED_NODES = 'CALLED_NODES'; const OPTION_MUTATED_NODES = 'MUTATED_NODES'; +const OPTION_ASSIGNED_NODES = 'ASSIGNED_NODES'; const IGNORED_LABELS = 'IGNORED_LABELS'; const RESULT_KEY = {}; @@ -113,6 +114,24 @@ export default class ExecutionPathOptions { return this.set( OPTION_IGNORE_SAFE_THIS_MUTATIONS, value ); } + /** + * @param {String[]} path + * @param {Node} node + * @return {ExecutionPathOptions} + */ + addAssignedNodeAtPath ( path, node ) { + return this.setIn( [ OPTION_ASSIGNED_NODES, node, ...path, RESULT_KEY ], true ); + } + + /** + * @param {String[]} path + * @param {Node} node + * @return {boolean} + */ + hasNodeBeenAssignedAtPath ( path, node ) { + return this._optionValues.getIn( [ OPTION_ASSIGNED_NODES, node, ...path, RESULT_KEY ] ); + } + /** * @param {String[]} path * @param {Node} node diff --git a/src/ast/nodes/ArrowFunctionExpression.js b/src/ast/nodes/ArrowFunctionExpression.js index 4647e768ed6..a59ab52a9e6 100644 --- a/src/ast/nodes/ArrowFunctionExpression.js +++ b/src/ast/nodes/ArrowFunctionExpression.js @@ -9,6 +9,13 @@ export default class ArrowFunctionExpression extends Node { return this.included; } + hasEffectsWhenAssignedAtPath ( path ) { + if ( path.length === 0 ) { + return true; + } + return this.hasEffectsWhenMutatedAtPath( path.slice( 1 ) ); + } + hasEffectsWhenCalled ( options ) { return this.params.some( param => param.hasEffects( options ) ) || this.body.hasEffects( options ); diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index 73532662bc5..729735444fd 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -41,27 +41,18 @@ export default class Identifier extends Node { } hasEffectsWhenAssignedAtPath ( path, options ) { - if ( !this.variable ) { - return true; - } - if ( path.length === 0 ) { - return this.variable.included; - } - return this.hasEffectsWhenMutatedAtPath( path.slice( 0, -1 ), options ); + return !this.variable + || this.variable.hasEffectsWhenAssignedAtPath( path, options ); } hasEffectsWhenCalled ( options ) { - if ( !this.variable ) { - return true; - } - return this.variable.hasEffectsWhenCalled( options ); + return !this.variable + || this.variable.hasEffectsWhenCalled( options ); } hasEffectsWhenMutatedAtPath ( path, options ) { - if ( !this.variable ) { - return true; - } - return this.variable.hasEffectsWhenMutatedAtPath( path, options ); + return !this.variable + || this.variable.hasEffectsWhenMutatedAtPath( path, options ); } includeInBundle () { diff --git a/src/ast/nodes/LogicalExpression.js b/src/ast/nodes/LogicalExpression.js index 4e89f4e1cf2..09f3803f241 100644 --- a/src/ast/nodes/LogicalExpression.js +++ b/src/ast/nodes/LogicalExpression.js @@ -17,6 +17,11 @@ export default class LogicalExpression extends Node { return operators[ this.operator ]( leftValue, rightValue ); } + hasEffectsWhenAssignedAtPath ( path, options ) { + return path.length === 0 + || this.hasEffectsWhenMutatedAtPath( path.slice( 1 ), options ); + } + hasEffectsWhenMutatedAtPath ( path, options ) { const leftValue = this.left.getValue(); if ( leftValue === UNKNOWN_VALUE ) { diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index a1faab9a53e..83518f69e0a 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -84,7 +84,10 @@ export default class MemberExpression extends Node { } hasEffectsWhenAssignedAtPath ( path, options ) { - return this.object.hasEffectsWhenMutatedAtPath( path, options ); + if ( this.computed ) { + return this.object.hasEffectsWhenMutatedAtPath( path, options ); + } + return this.object.hasEffectsWhenAssignedAtPath( [ this.property.name, ...path ], options ); } includeInBundle () { diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index 63566169a76..0a8d3e69f43 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -1,6 +1,16 @@ import Node from '../Node.js'; export default class ObjectExpression extends Node { + hasEffectsWhenAssignedAtPath ( path, options ) { + if ( path.length <= 1 ) { + return false; + } + const accessedProperty = this.properties.find( property => !property.computed && property.key.name === path[ 0 ] ); + + return !accessedProperty + || accessedProperty.hasEffectsWhenAssignedAtPath( path.slice( 1 ), options ); + } + hasEffectsWhenMutatedAtPath ( path ) { return path.length > 1; } diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index ab99f8adffb..28d8b5290a5 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -7,10 +7,7 @@ export default class Property extends Node { } hasEffectsWhenAssignedAtPath ( path, options ) { - if ( path.length > 0 ) { - return true; - } - return this.value.hasEffectsWhenAssignedAtPath( [], options ); + return this.value.hasEffectsWhenAssignedAtPath( path, options ); } initialiseAndDeclare ( parentScope, kind ) { diff --git a/src/ast/nodes/ThisExpression.js b/src/ast/nodes/ThisExpression.js index 46e2d94db66..74650568192 100644 --- a/src/ast/nodes/ThisExpression.js +++ b/src/ast/nodes/ThisExpression.js @@ -24,7 +24,7 @@ export default class ThisExpression extends Node { if ( path.length === 0 ) { return true; } - return this.hasEffectsWhenMutatedAtPath( path, options ); + return this.hasEffectsWhenMutatedAtPath( path.slice(1), options ); } hasEffectsWhenMutatedAtPath ( path, options ) { diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index c3dad9b008d..2b3eb89d233 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -21,6 +21,13 @@ export default class FunctionNode extends Node { return this.hasEffects( options ); } + hasEffectsWhenAssignedAtPath ( path ) { + if ( path.length === 0 ) { + return true; + } + return this.hasEffectsWhenMutatedAtPath( path.slice( 1 ) ); + } + hasEffectsWhenCalled ( options ) { const innerOptions = options.setIgnoreSafeThisMutations(); return this.params.some( param => param.hasEffects( innerOptions ) ) diff --git a/src/ast/values.js b/src/ast/values.js index d9367ba0089..99325e22da3 100644 --- a/src/ast/values.js +++ b/src/ast/values.js @@ -3,6 +3,7 @@ export const UNKNOWN_VALUE = { toString: () => '[[UNKNOWN]]' }; export const UNKNOWN_ASSIGNMENT = { type: 'UNKNOWN', bindCall: () => {}, + hasEffectsWhenAssignedAtPath: () => true, hasEffectsWhenCalled: () => true, hasEffectsWhenMutatedAtPath: () => true, }; @@ -10,6 +11,7 @@ export const UNKNOWN_ASSIGNMENT = { export const UNDEFINED_ASSIGNMENT = { type: 'UNDEFINED', bindCall: () => {}, + hasEffectsWhenAssignedAtPath: () => true, hasEffectsWhenCalled: () => true, hasEffectsWhenMutatedAtPath: () => true, }; @@ -17,6 +19,7 @@ export const UNDEFINED_ASSIGNMENT = { export const UNKNOWN_OBJECT_LITERAL = { type: 'UNKNOWN_OBJECT_LITERAL', bindCall: () => {}, + hasEffectsWhenAssignedAtPath: path => path.length > 1, hasEffectsWhenCalled: () => true, hasEffectsWhenMutatedAtPath: path => path.length > 0, }; diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index 39b85aafb67..5e58f379ed4 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -36,6 +36,14 @@ export default class LocalVariable extends Variable { return `exports.${this.exportName}`; } + hasEffectsWhenAssignedAtPath ( path, options ) { + return this.included + || (path.length > 0 && Array.from( this.assignedExpressions ).some( node => + !options.hasNodeBeenAssignedAtPath( path, node ) && + node.hasEffectsWhenAssignedAtPath( path, options.addAssignedNodeAtPath( path, node ) ) + )); + } + hasEffectsWhenCalled ( options ) { return Array.from( this.assignedExpressions ).some( node => !options.hasNodeBeenCalled( node ) diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index 9550e57d652..bec1f172d37 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -13,6 +13,10 @@ export default class Variable { return this.name; } + hasEffectsWhenAssignedAtPath () { + return true; + } + hasEffectsWhenCalled () { return true; } diff --git a/test/form/samples/side-effects-object-literal-mutation/_config.js b/test/form/samples/side-effects-object-literal-mutation/_config.js new file mode 100644 index 00000000000..8e225aac083 --- /dev/null +++ b/test/form/samples/side-effects-object-literal-mutation/_config.js @@ -0,0 +1,4 @@ +module.exports = { + description: 'detects side-effects when mutating object literals', + options: { name: 'myBundle' } +}; diff --git a/test/form/samples/side-effects-object-literal-mutation/_expected/amd.js b/test/form/samples/side-effects-object-literal-mutation/_expected/amd.js new file mode 100644 index 00000000000..9c0e691019d --- /dev/null +++ b/test/form/samples/side-effects-object-literal-mutation/_expected/amd.js @@ -0,0 +1,14 @@ +define(['exports'], function (exports) { 'use strict'; + + const x2 = { x: {} }; + x2.y = 1; + x2.x.y = 2; + + const x3 = { x: {} }; + x3.y.z = 1; + + exports.x2 = x2; + + Object.defineProperty(exports, '__esModule', { value: true }); + +}); diff --git a/test/form/samples/side-effects-object-literal-mutation/_expected/cjs.js b/test/form/samples/side-effects-object-literal-mutation/_expected/cjs.js new file mode 100644 index 00000000000..d741be85a2b --- /dev/null +++ b/test/form/samples/side-effects-object-literal-mutation/_expected/cjs.js @@ -0,0 +1,12 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +const x2 = { x: {} }; +x2.y = 1; +x2.x.y = 2; + +const x3 = { x: {} }; +x3.y.z = 1; + +exports.x2 = x2; diff --git a/test/form/samples/side-effects-object-literal-mutation/_expected/es.js b/test/form/samples/side-effects-object-literal-mutation/_expected/es.js new file mode 100644 index 00000000000..29d4634de62 --- /dev/null +++ b/test/form/samples/side-effects-object-literal-mutation/_expected/es.js @@ -0,0 +1,8 @@ +const x2 = { x: {} }; +x2.y = 1; +x2.x.y = 2; + +const x3 = { x: {} }; +x3.y.z = 1; + +export { x2 }; diff --git a/test/form/samples/side-effects-object-literal-mutation/_expected/iife.js b/test/form/samples/side-effects-object-literal-mutation/_expected/iife.js new file mode 100644 index 00000000000..40a375fa581 --- /dev/null +++ b/test/form/samples/side-effects-object-literal-mutation/_expected/iife.js @@ -0,0 +1,15 @@ +var myBundle = (function (exports) { + 'use strict'; + + const x2 = { x: {} }; + x2.y = 1; + x2.x.y = 2; + + const x3 = { x: {} }; + x3.y.z = 1; + + exports.x2 = x2; + + return exports; + +}({})); diff --git a/test/form/samples/side-effects-object-literal-mutation/_expected/umd.js b/test/form/samples/side-effects-object-literal-mutation/_expected/umd.js new file mode 100644 index 00000000000..f5549bd3744 --- /dev/null +++ b/test/form/samples/side-effects-object-literal-mutation/_expected/umd.js @@ -0,0 +1,18 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (factory((global.myBundle = {}))); +}(this, (function (exports) { 'use strict'; + + const x2 = { x: {} }; + x2.y = 1; + x2.x.y = 2; + + const x3 = { x: {} }; + x3.y.z = 1; + + exports.x2 = x2; + + Object.defineProperty(exports, '__esModule', { value: true }); + +}))); diff --git a/test/form/samples/side-effects-object-literal-mutation/main.js b/test/form/samples/side-effects-object-literal-mutation/main.js new file mode 100644 index 00000000000..00782dfcf0c --- /dev/null +++ b/test/form/samples/side-effects-object-literal-mutation/main.js @@ -0,0 +1,10 @@ +const x1 = { x: {} }; +x1.y = 1; +x1.x.y = 2; + +export const x2 = { x: {} }; +x2.y = 1; +x2.x.y = 2; + +const x3 = { x: {} }; +x3.y.z = 1; From 8e34c7df2c3e8cd0b85041a7eea36b51a4e4121a Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Wed, 20 Sep 2017 19:45:46 +0200 Subject: [PATCH 10/76] Implement object member assignment tracking on all levels. Now it should be possible to basically assign any part of an object to any other part without rollup losing track of it. For now, we can only do object literals properly, though. Next up: function prototypes! --- src/ast/nodes/ExportDefaultDeclaration.js | 4 ++ src/ast/nodes/Literal.js | 10 ++++ src/ast/nodes/MemberExpression.js | 6 +++ src/ast/nodes/UnaryExpression.js | 5 +- src/ast/utils/isProgramLevel.js | 10 ---- src/ast/variables/DeepSet.js | 37 ++++++++++++++ src/ast/variables/LocalVariable.js | 25 ++++++---- .../_config.js | 2 +- .../_expected/amd.js | 33 +++++++++--- .../_expected/cjs.js | 33 +++++++++--- .../_expected/es.js | 33 +++++++++--- .../_expected/iife.js | 35 ++++++++++--- .../_expected/umd.js | 35 ++++++++++--- .../main.js | 50 ++++++++++++++++--- 14 files changed, 255 insertions(+), 63 deletions(-) delete mode 100644 src/ast/utils/isProgramLevel.js create mode 100644 src/ast/variables/DeepSet.js diff --git a/src/ast/nodes/ExportDefaultDeclaration.js b/src/ast/nodes/ExportDefaultDeclaration.js index f8a092ce1de..dfb80e4f7eb 100644 --- a/src/ast/nodes/ExportDefaultDeclaration.js +++ b/src/ast/nodes/ExportDefaultDeclaration.js @@ -13,6 +13,10 @@ export default class ExportDefaultDeclaration extends Node { if ( this.original ) this.original.addReference( reference ); } + assignExpressionAtPath ( path, expression ) { + if ( this.original ) this.original.assignExpressionAtPath( path, expression ); + } + bind () { const name = ( this.declaration.id && this.declaration.id.name ) || this.declaration.name; if ( name ) this.original = this.scope.findVariable( name ); diff --git a/src/ast/nodes/Literal.js b/src/ast/nodes/Literal.js index 168461b6b87..eb7f162e56b 100644 --- a/src/ast/nodes/Literal.js +++ b/src/ast/nodes/Literal.js @@ -5,7 +5,17 @@ export default class Literal extends Node { return this.value; } + hasEffectsWhenAssignedAtPath ( path ) { + if (this.value === null) { + return path.length > 0; + } + return path.length > 1; + } + hasEffectsWhenMutatedAtPath ( path ) { + if (this.value === null) { + return true; + } return path.length > 0; } diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index 83518f69e0a..b4e85b91aac 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -77,6 +77,12 @@ export default class MemberExpression extends Node { } } + bindAssignmentAtPath ( path, expression ) { + if ( !this.computed ) { + this.object.bindAssignmentAtPath( [ this.property.name, ...path ], expression ); + } + } + bindCall ( callOptions ) { if ( this.variable ) { this.variable.addCall( callOptions ); diff --git a/src/ast/nodes/UnaryExpression.js b/src/ast/nodes/UnaryExpression.js index cad05977405..a4d0ea4f924 100644 --- a/src/ast/nodes/UnaryExpression.js +++ b/src/ast/nodes/UnaryExpression.js @@ -1,5 +1,5 @@ import Node from '../Node.js'; -import { UNKNOWN_VALUE } from '../values.js'; +import { UNDEFINED_ASSIGNMENT, UNKNOWN_VALUE } from '../values'; const operators = { '-': value => -value, @@ -14,6 +14,9 @@ const operators = { export default class UnaryExpression extends Node { bind () { if ( this.value === UNKNOWN_VALUE ) super.bind(); + if ( this.operator === 'delete' ) { + this.argument.bindAssignmentAtPath( [], UNDEFINED_ASSIGNMENT ); + } } getValue () { diff --git a/src/ast/utils/isProgramLevel.js b/src/ast/utils/isProgramLevel.js deleted file mode 100644 index b23de1252f3..00000000000 --- a/src/ast/utils/isProgramLevel.js +++ /dev/null @@ -1,10 +0,0 @@ -export default function isProgramLevel ( node ) { - do { - if ( node.type === 'Program' ) { - return true; - } - node = node.parent; - } while ( node && !/Function/.test( node.type ) ); - - return false; -} diff --git a/src/ast/variables/DeepSet.js b/src/ast/variables/DeepSet.js new file mode 100644 index 00000000000..f1a50aec106 --- /dev/null +++ b/src/ast/variables/DeepSet.js @@ -0,0 +1,37 @@ +const SET_KEY = {}; + +export default class DeepSet { + constructor () { + this._assignments = new Map( [ [ SET_KEY, new Set() ] ] ); + } + + addAtPath ( path, assignment ) { + 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 DeepSet() ); + } + 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 && this._assignments.has( nextPath ) ) { + this._assignments.get( nextPath ).forEachAtPath( remainingPath, callback ); + } + } + + someAtPath ( path, predicateFunction ) { + const [ nextPath, ...remainingPath ] = path; + return Array.from( this._assignments.get( SET_KEY ) ).some( assignment => predicateFunction( path, assignment ) ) + || ( + path.length > 0 + && this._assignments.has( nextPath ) + && this._assignments.get( nextPath ).someAtPath( remainingPath, predicateFunction ) + ); + } +} diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index 5e58f379ed4..4999a1ae924 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -1,4 +1,5 @@ import Variable from './Variable'; +import DeepSet from './DeepSet'; export default class LocalVariable extends Variable { constructor ( name, declarator, init ) { @@ -6,7 +7,8 @@ export default class LocalVariable extends Variable { this.isReassigned = false; this.exportName = null; this.declarations = new Set( declarator ? [ declarator ] : null ); - this.assignedExpressions = new Set( init ? [ init ] : null ); + this.assignedExpressions = new DeepSet(); + init && this.assignedExpressions.addAtPath( [], init ); this.calls = new Set(); } @@ -18,14 +20,14 @@ export default class LocalVariable extends Variable { // To prevent infinite loops if ( this.calls.has( callOptions ) ) return; this.calls.add( callOptions ); - Array.from( this.assignedExpressions ).forEach( expression => expression.bindCall( callOptions ) ); + this.assignedExpressions.forEachAtPath( [], ( relativePath, expression ) => expression.bindCall( callOptions ) ); } assignExpressionAtPath ( path, expression ) { + this.assignedExpressions.addAtPath( path, expression ); if ( path.length === 0 ) { - this.assignedExpressions.add( expression ); this.isReassigned = true; - Array.from( this.calls ).forEach( callOptions => expression.bindCall( callOptions ) ); + this.calls.forEach( callOptions => expression.bindCall( callOptions ) ); } } @@ -38,14 +40,15 @@ export default class LocalVariable extends Variable { hasEffectsWhenAssignedAtPath ( path, options ) { return this.included - || (path.length > 0 && Array.from( this.assignedExpressions ).some( node => - !options.hasNodeBeenAssignedAtPath( path, node ) && - node.hasEffectsWhenAssignedAtPath( path, options.addAssignedNodeAtPath( path, node ) ) + || (path.length > 0 && this.assignedExpressions.someAtPath( path, ( relativePath, node ) => + relativePath.length > 0 + && !options.hasNodeBeenAssignedAtPath( relativePath, node ) + && node.hasEffectsWhenAssignedAtPath( relativePath, options.addAssignedNodeAtPath( relativePath, node ) ) )); } hasEffectsWhenCalled ( options ) { - return Array.from( this.assignedExpressions ).some( node => + return this.assignedExpressions.someAtPath( [], ( relativePath, node ) => !options.hasNodeBeenCalled( node ) && node.hasEffectsWhenCalled( options.getHasEffectsWhenCalledOptions( node ) ) ); @@ -53,9 +56,9 @@ export default class LocalVariable extends Variable { hasEffectsWhenMutatedAtPath ( path, options ) { return this.included - || Array.from( this.assignedExpressions ).some( node => - !options.hasNodeBeenMutatedAtPath( path, node ) && - node.hasEffectsWhenMutatedAtPath( path, options.addMutatedNodeAtPath( path, node ) ) + || this.assignedExpressions.someAtPath( path, ( relativePath, node ) => + !options.hasNodeBeenMutatedAtPath( relativePath, node ) && + node.hasEffectsWhenMutatedAtPath( relativePath, options.addMutatedNodeAtPath( relativePath, node ) ) ); } diff --git a/test/form/samples/side-effects-object-literal-mutation/_config.js b/test/form/samples/side-effects-object-literal-mutation/_config.js index 8e225aac083..423a24bd345 100644 --- a/test/form/samples/side-effects-object-literal-mutation/_config.js +++ b/test/form/samples/side-effects-object-literal-mutation/_config.js @@ -1,4 +1,4 @@ module.exports = { description: 'detects side-effects when mutating object literals', - options: { name: 'myBundle' } + options: { name: 'bundle' } }; diff --git a/test/form/samples/side-effects-object-literal-mutation/_expected/amd.js b/test/form/samples/side-effects-object-literal-mutation/_expected/amd.js index 9c0e691019d..c8d2cca8be5 100644 --- a/test/form/samples/side-effects-object-literal-mutation/_expected/amd.js +++ b/test/form/samples/side-effects-object-literal-mutation/_expected/amd.js @@ -1,13 +1,34 @@ define(['exports'], function (exports) { 'use strict'; - const x2 = { x: {} }; - x2.y = 1; - x2.x.y = 2; + const retained1 = { x: {} }; + retained1.y = 1; + retained1.x.y = 2; - const x3 = { x: {} }; - x3.y.z = 1; + const retained2 = { x: {} }; + retained2.y.z = 1; - exports.x2 = x2; + const retained3 = { x: {} }; + delete retained3.x; + retained3.x.y = 2; + + const retained4 = { x: {} }; + retained4.x = undefined; + retained4.x.y = 2; + + const retained5 = { x: {} }; + retained5.x = null; + retained5.x.y = 2; + + const retained6 = { x: { y: {} } }; + retained6.x = {}; + retained6.x.y.z = 3; + + const retained7 = { x: { y: globalVar } }; + const retained8 = { x: { y: {} } }; + retained8.x = retained7.x; + retained8.x.y.z = 3; + + exports.retained1 = retained1; Object.defineProperty(exports, '__esModule', { value: true }); diff --git a/test/form/samples/side-effects-object-literal-mutation/_expected/cjs.js b/test/form/samples/side-effects-object-literal-mutation/_expected/cjs.js index d741be85a2b..fba914234d2 100644 --- a/test/form/samples/side-effects-object-literal-mutation/_expected/cjs.js +++ b/test/form/samples/side-effects-object-literal-mutation/_expected/cjs.js @@ -2,11 +2,32 @@ Object.defineProperty(exports, '__esModule', { value: true }); -const x2 = { x: {} }; -x2.y = 1; -x2.x.y = 2; +const retained1 = { x: {} }; +retained1.y = 1; +retained1.x.y = 2; -const x3 = { x: {} }; -x3.y.z = 1; +const retained2 = { x: {} }; +retained2.y.z = 1; -exports.x2 = x2; +const retained3 = { x: {} }; +delete retained3.x; +retained3.x.y = 2; + +const retained4 = { x: {} }; +retained4.x = undefined; +retained4.x.y = 2; + +const retained5 = { x: {} }; +retained5.x = null; +retained5.x.y = 2; + +const retained6 = { x: { y: {} } }; +retained6.x = {}; +retained6.x.y.z = 3; + +const retained7 = { x: { y: globalVar } }; +const retained8 = { x: { y: {} } }; +retained8.x = retained7.x; +retained8.x.y.z = 3; + +exports.retained1 = retained1; diff --git a/test/form/samples/side-effects-object-literal-mutation/_expected/es.js b/test/form/samples/side-effects-object-literal-mutation/_expected/es.js index 29d4634de62..a65f9857c49 100644 --- a/test/form/samples/side-effects-object-literal-mutation/_expected/es.js +++ b/test/form/samples/side-effects-object-literal-mutation/_expected/es.js @@ -1,8 +1,29 @@ -const x2 = { x: {} }; -x2.y = 1; -x2.x.y = 2; +const retained1 = { x: {} }; +retained1.y = 1; +retained1.x.y = 2; -const x3 = { x: {} }; -x3.y.z = 1; +const retained2 = { x: {} }; +retained2.y.z = 1; -export { x2 }; +const retained3 = { x: {} }; +delete retained3.x; +retained3.x.y = 2; + +const retained4 = { x: {} }; +retained4.x = undefined; +retained4.x.y = 2; + +const retained5 = { x: {} }; +retained5.x = null; +retained5.x.y = 2; + +const retained6 = { x: { y: {} } }; +retained6.x = {}; +retained6.x.y.z = 3; + +const retained7 = { x: { y: globalVar } }; +const retained8 = { x: { y: {} } }; +retained8.x = retained7.x; +retained8.x.y.z = 3; + +export { retained1 }; diff --git a/test/form/samples/side-effects-object-literal-mutation/_expected/iife.js b/test/form/samples/side-effects-object-literal-mutation/_expected/iife.js index 40a375fa581..b3747927b47 100644 --- a/test/form/samples/side-effects-object-literal-mutation/_expected/iife.js +++ b/test/form/samples/side-effects-object-literal-mutation/_expected/iife.js @@ -1,14 +1,35 @@ -var myBundle = (function (exports) { +var bundle = (function (exports) { 'use strict'; - const x2 = { x: {} }; - x2.y = 1; - x2.x.y = 2; + const retained1 = { x: {} }; + retained1.y = 1; + retained1.x.y = 2; - const x3 = { x: {} }; - x3.y.z = 1; + const retained2 = { x: {} }; + retained2.y.z = 1; - exports.x2 = x2; + const retained3 = { x: {} }; + delete retained3.x; + retained3.x.y = 2; + + const retained4 = { x: {} }; + retained4.x = undefined; + retained4.x.y = 2; + + const retained5 = { x: {} }; + retained5.x = null; + retained5.x.y = 2; + + const retained6 = { x: { y: {} } }; + retained6.x = {}; + retained6.x.y.z = 3; + + const retained7 = { x: { y: globalVar } }; + const retained8 = { x: { y: {} } }; + retained8.x = retained7.x; + retained8.x.y.z = 3; + + exports.retained1 = retained1; return exports; diff --git a/test/form/samples/side-effects-object-literal-mutation/_expected/umd.js b/test/form/samples/side-effects-object-literal-mutation/_expected/umd.js index f5549bd3744..a73414962fe 100644 --- a/test/form/samples/side-effects-object-literal-mutation/_expected/umd.js +++ b/test/form/samples/side-effects-object-literal-mutation/_expected/umd.js @@ -1,17 +1,38 @@ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : - (factory((global.myBundle = {}))); + (factory((global.bundle = {}))); }(this, (function (exports) { 'use strict'; - const x2 = { x: {} }; - x2.y = 1; - x2.x.y = 2; + const retained1 = { x: {} }; + retained1.y = 1; + retained1.x.y = 2; - const x3 = { x: {} }; - x3.y.z = 1; + const retained2 = { x: {} }; + retained2.y.z = 1; - exports.x2 = x2; + const retained3 = { x: {} }; + delete retained3.x; + retained3.x.y = 2; + + const retained4 = { x: {} }; + retained4.x = undefined; + retained4.x.y = 2; + + const retained5 = { x: {} }; + retained5.x = null; + retained5.x.y = 2; + + const retained6 = { x: { y: {} } }; + retained6.x = {}; + retained6.x.y.z = 3; + + const retained7 = { x: { y: globalVar } }; + const retained8 = { x: { y: {} } }; + retained8.x = retained7.x; + retained8.x.y.z = 3; + + exports.retained1 = retained1; Object.defineProperty(exports, '__esModule', { value: true }); 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 00782dfcf0c..6add6e5c576 100644 --- a/test/form/samples/side-effects-object-literal-mutation/main.js +++ b/test/form/samples/side-effects-object-literal-mutation/main.js @@ -1,10 +1,44 @@ -const x1 = { x: {} }; -x1.y = 1; -x1.x.y = 2; +const removed1 = { x: {} }; +removed1.y = 1; +removed1.x.y = 2; -export const x2 = { x: {} }; -x2.y = 1; -x2.x.y = 2; +export const retained1 = { x: {} }; +retained1.y = 1; +retained1.x.y = 2; -const x3 = { x: {} }; -x3.y.z = 1; +const retained2 = { x: {} }; +retained2.y.z = 1; + +const retained3 = { x: {} }; +delete retained3.x; +retained3.x.y = 2; + +const retained4 = { x: {} }; +retained4.x = undefined; +retained4.x.y = 2; + +const retained5 = { x: {} }; +retained5.x = null; +retained5.x.y = 2; + +const removed2 = { x: {} }; +removed2.x = 99; +removed2.x.y = 2; + +const removed3 = { x: { y: {} } }; +removed3.x = { y: {} }; +removed3.x.y.z = 3; + +const retained6 = { x: { y: {} } }; +retained6.x = {}; +retained6.x.y.z = 3; + +const retained7 = { x: { y: globalVar } }; +const retained8 = { x: { y: {} } }; +retained8.x = retained7.x; +retained8.x.y.z = 3; + +const removed4 = {a: { x: { y: globalVar } }}; +const removed5 = { x: { y: {} } }; +removed5.x = removed4.a.x; +removed5.x.y = 2; From ccd86ced239b8ae83d0d97e25dd30662cbc745b6 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Thu, 21 Sep 2017 09:46:10 +0200 Subject: [PATCH 11/76] Add a virtual node for function prototypes --- src/ast/Node.js | 4 ++++ src/ast/nodes/shared/FunctionNode.js | 16 +++++++++---- .../nodes/shared/VirtualObjectExpression.js | 11 +++++++++ .../_config.js | 3 --- .../_expected/amd.js | 20 ---------------- .../_expected/cjs.js | 18 -------------- .../_expected/iife.js | 21 ---------------- .../_expected/umd.js | 24 ------------------- .../_config.js | 3 +++ .../_expected/amd.js} | 12 ++++------ .../_expected/cjs.js | 12 ++++++++++ .../_expected/es.js | 10 ++++++++ .../_expected/iife.js} | 13 +++++----- .../_expected/umd.js | 18 ++++++++++++++ .../main.js | 14 +++++++++++ 15 files changed, 95 insertions(+), 104 deletions(-) create mode 100644 src/ast/nodes/shared/VirtualObjectExpression.js delete mode 100644 test/form/samples/side-effect-when-mutating-member-expression/_config.js delete mode 100644 test/form/samples/side-effect-when-mutating-member-expression/_expected/amd.js delete mode 100644 test/form/samples/side-effect-when-mutating-member-expression/_expected/cjs.js delete mode 100644 test/form/samples/side-effect-when-mutating-member-expression/_expected/iife.js delete mode 100644 test/form/samples/side-effect-when-mutating-member-expression/_expected/umd.js create mode 100644 test/form/samples/side-effects-prototype-assignments/_config.js rename test/form/samples/{side-effect-when-mutating-member-expression/_expected/es.js => side-effects-prototype-assignments/_expected/amd.js} (59%) create mode 100644 test/form/samples/side-effects-prototype-assignments/_expected/cjs.js create mode 100644 test/form/samples/side-effects-prototype-assignments/_expected/es.js rename test/form/samples/{side-effect-when-mutating-member-expression/main.js => side-effects-prototype-assignments/_expected/iife.js} (61%) create mode 100644 test/form/samples/side-effects-prototype-assignments/_expected/umd.js create mode 100644 test/form/samples/side-effects-prototype-assignments/main.js diff --git a/src/ast/Node.js b/src/ast/Node.js index 8a1d07cedc4..f4fef6553b0 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -3,6 +3,10 @@ import { UNKNOWN_VALUE } from './values.js'; import ExecutionPathOptions from './ExecutionPathOptions'; export default class Node { + constructor () { + this.keys = []; + } + /** * Called once all nodes have been initialised and the scopes have been populated. * Use this to bind assignments and calls to variables. diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index 2b3eb89d233..e7065c4ba67 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -1,6 +1,7 @@ import Node from '../../Node.js'; import FunctionScope from '../../scopes/FunctionScope'; import { UNKNOWN_ASSIGNMENT, UNKNOWN_OBJECT_LITERAL } from '../../values'; +import VirtualObjectExpression from './VirtualObjectExpression'; export default class FunctionNode extends Node { bindCall ( { withNew } ) { @@ -21,11 +22,14 @@ export default class FunctionNode extends Node { return this.hasEffects( options ); } - hasEffectsWhenAssignedAtPath ( path ) { - if ( path.length === 0 ) { - return true; + hasEffectsWhenAssignedAtPath ( path, options ) { + if ( path.length <= 1 ) { + return false; } - return this.hasEffectsWhenMutatedAtPath( path.slice( 1 ) ); + if ( path[ 0 ] === 'prototype' ) { + return this.prototypeObject.hasEffectsWhenAssignedAtPath( path.slice( 1 ), options ); + } + return true; } hasEffectsWhenCalled ( options ) { @@ -38,6 +42,10 @@ export default class FunctionNode extends Node { return this.included || path.length > 0; } + initialiseNode () { + this.prototypeObject = new VirtualObjectExpression(); + } + initialiseScope ( parentScope ) { this.scope = new FunctionScope( { parent: parentScope } ); } diff --git a/src/ast/nodes/shared/VirtualObjectExpression.js b/src/ast/nodes/shared/VirtualObjectExpression.js new file mode 100644 index 00000000000..b20acb7b195 --- /dev/null +++ b/src/ast/nodes/shared/VirtualObjectExpression.js @@ -0,0 +1,11 @@ +import Node from '../../Node'; + +export default class VirtualObjectExpression extends Node { + hasEffectsWhenAssignedAtPath ( path ) { + return path.length > 1; + } + + hasEffectsWhenMutatedAtPath ( path ) { + return path.length > 0; + } +} diff --git a/test/form/samples/side-effect-when-mutating-member-expression/_config.js b/test/form/samples/side-effect-when-mutating-member-expression/_config.js deleted file mode 100644 index 4857c58dd78..00000000000 --- a/test/form/samples/side-effect-when-mutating-member-expression/_config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - description: 'include side-effects with re-assigned member expressions (#953)' -}; diff --git a/test/form/samples/side-effect-when-mutating-member-expression/_expected/amd.js b/test/form/samples/side-effect-when-mutating-member-expression/_expected/amd.js deleted file mode 100644 index d3201e6bade..00000000000 --- a/test/form/samples/side-effect-when-mutating-member-expression/_expected/amd.js +++ /dev/null @@ -1,20 +0,0 @@ -define(function () { 'use strict'; - - var V8Engine = (function () { - function V8Engine () {} - - V8Engine.prototype.toString = function () { return 'V8'; }; - return V8Engine; - }()); - - var V6Engine = (function () { - function V6Engine () {} - - V6Engine.prototype = V8Engine.prototype; - V6Engine.prototype.toString = function () { return 'V6'; }; - return V6Engine; - }()); - - console.log( new V8Engine().toString() ); - -}); diff --git a/test/form/samples/side-effect-when-mutating-member-expression/_expected/cjs.js b/test/form/samples/side-effect-when-mutating-member-expression/_expected/cjs.js deleted file mode 100644 index 78998d12afd..00000000000 --- a/test/form/samples/side-effect-when-mutating-member-expression/_expected/cjs.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -var V8Engine = (function () { - function V8Engine () {} - - V8Engine.prototype.toString = function () { return 'V8'; }; - return V8Engine; -}()); - -var V6Engine = (function () { - function V6Engine () {} - - V6Engine.prototype = V8Engine.prototype; - V6Engine.prototype.toString = function () { return 'V6'; }; - return V6Engine; -}()); - -console.log( new V8Engine().toString() ); diff --git a/test/form/samples/side-effect-when-mutating-member-expression/_expected/iife.js b/test/form/samples/side-effect-when-mutating-member-expression/_expected/iife.js deleted file mode 100644 index 3f643389f02..00000000000 --- a/test/form/samples/side-effect-when-mutating-member-expression/_expected/iife.js +++ /dev/null @@ -1,21 +0,0 @@ -(function () { - 'use strict'; - - var V8Engine = (function () { - function V8Engine () {} - - V8Engine.prototype.toString = function () { return 'V8'; }; - return V8Engine; - }()); - - var V6Engine = (function () { - function V6Engine () {} - - V6Engine.prototype = V8Engine.prototype; - V6Engine.prototype.toString = function () { return 'V6'; }; - return V6Engine; - }()); - - console.log( new V8Engine().toString() ); - -}()); diff --git a/test/form/samples/side-effect-when-mutating-member-expression/_expected/umd.js b/test/form/samples/side-effect-when-mutating-member-expression/_expected/umd.js deleted file mode 100644 index 1b82629ac47..00000000000 --- a/test/form/samples/side-effect-when-mutating-member-expression/_expected/umd.js +++ /dev/null @@ -1,24 +0,0 @@ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory() : - typeof define === 'function' && define.amd ? define(factory) : - (factory()); -}(this, (function () { 'use strict'; - - var V8Engine = (function () { - function V8Engine () {} - - V8Engine.prototype.toString = function () { return 'V8'; }; - return V8Engine; - }()); - - var V6Engine = (function () { - function V6Engine () {} - - V6Engine.prototype = V8Engine.prototype; - V6Engine.prototype.toString = function () { return 'V6'; }; - return V6Engine; - }()); - - console.log( new V8Engine().toString() ); - -}))); diff --git a/test/form/samples/side-effects-prototype-assignments/_config.js b/test/form/samples/side-effects-prototype-assignments/_config.js new file mode 100644 index 00000000000..b71c53dd0a2 --- /dev/null +++ b/test/form/samples/side-effects-prototype-assignments/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'include side-effects with re-assigned prototypes (#953)' +}; diff --git a/test/form/samples/side-effect-when-mutating-member-expression/_expected/es.js b/test/form/samples/side-effects-prototype-assignments/_expected/amd.js similarity index 59% rename from test/form/samples/side-effect-when-mutating-member-expression/_expected/es.js rename to test/form/samples/side-effects-prototype-assignments/_expected/amd.js index 4c1a1479e27..d1d09de5bb5 100644 --- a/test/form/samples/side-effect-when-mutating-member-expression/_expected/es.js +++ b/test/form/samples/side-effects-prototype-assignments/_expected/amd.js @@ -1,16 +1,14 @@ -var V8Engine = (function () { +define(function () { 'use strict'; + function V8Engine () {} V8Engine.prototype.toString = function () { return 'V8'; }; - return V8Engine; -}()); -var V6Engine = (function () { function V6Engine () {} V6Engine.prototype = V8Engine.prototype; V6Engine.prototype.toString = function () { return 'V6'; }; - return V6Engine; -}()); -console.log( new V8Engine().toString() ); + console.log( new V8Engine().toString() ); + +}); diff --git a/test/form/samples/side-effects-prototype-assignments/_expected/cjs.js b/test/form/samples/side-effects-prototype-assignments/_expected/cjs.js new file mode 100644 index 00000000000..47c19ab9df2 --- /dev/null +++ b/test/form/samples/side-effects-prototype-assignments/_expected/cjs.js @@ -0,0 +1,12 @@ +'use strict'; + +function V8Engine () {} + +V8Engine.prototype.toString = function () { return 'V8'; }; + +function V6Engine () {} + +V6Engine.prototype = V8Engine.prototype; +V6Engine.prototype.toString = function () { return 'V6'; }; + +console.log( new V8Engine().toString() ); diff --git a/test/form/samples/side-effects-prototype-assignments/_expected/es.js b/test/form/samples/side-effects-prototype-assignments/_expected/es.js new file mode 100644 index 00000000000..fc9a69d6395 --- /dev/null +++ b/test/form/samples/side-effects-prototype-assignments/_expected/es.js @@ -0,0 +1,10 @@ +function V8Engine () {} + +V8Engine.prototype.toString = function () { return 'V8'; }; + +function V6Engine () {} + +V6Engine.prototype = V8Engine.prototype; +V6Engine.prototype.toString = function () { return 'V6'; }; + +console.log( new V8Engine().toString() ); diff --git a/test/form/samples/side-effect-when-mutating-member-expression/main.js b/test/form/samples/side-effects-prototype-assignments/_expected/iife.js similarity index 61% rename from test/form/samples/side-effect-when-mutating-member-expression/main.js rename to test/form/samples/side-effects-prototype-assignments/_expected/iife.js index a10f8acc829..de3d76004dd 100644 --- a/test/form/samples/side-effect-when-mutating-member-expression/main.js +++ b/test/form/samples/side-effects-prototype-assignments/_expected/iife.js @@ -1,16 +1,15 @@ -var V8Engine = (function () { +(function () { + 'use strict'; + function V8Engine () {} V8Engine.prototype.toString = function () { return 'V8'; }; - return V8Engine; -}()); -var V6Engine = (function () { function V6Engine () {} V6Engine.prototype = V8Engine.prototype; V6Engine.prototype.toString = function () { return 'V6'; }; - return V6Engine; -}()); -console.log( new V8Engine().toString() ); \ No newline at end of file + console.log( new V8Engine().toString() ); + +}()); diff --git a/test/form/samples/side-effects-prototype-assignments/_expected/umd.js b/test/form/samples/side-effects-prototype-assignments/_expected/umd.js new file mode 100644 index 00000000000..9511791bee2 --- /dev/null +++ b/test/form/samples/side-effects-prototype-assignments/_expected/umd.js @@ -0,0 +1,18 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + function V8Engine () {} + + V8Engine.prototype.toString = function () { return 'V8'; }; + + function V6Engine () {} + + V6Engine.prototype = V8Engine.prototype; + V6Engine.prototype.toString = function () { return 'V6'; }; + + console.log( new V8Engine().toString() ); + +}))); diff --git a/test/form/samples/side-effects-prototype-assignments/main.js b/test/form/samples/side-effects-prototype-assignments/main.js new file mode 100644 index 00000000000..f7dcc5162ec --- /dev/null +++ b/test/form/samples/side-effects-prototype-assignments/main.js @@ -0,0 +1,14 @@ +function V8Engine () {} + +V8Engine.prototype.toString = function () { return 'V8'; }; + +function V6Engine () {} + +V6Engine.prototype = V8Engine.prototype; +V6Engine.prototype.toString = function () { return 'V6'; }; + +function IgnoredEngine () {} + +IgnoredEngine.prototype.toString = function () { return 'IGNORED'; }; + +console.log( new V8Engine().toString() ); From 4128c3b074f1769cdb5ed86bbbed94c8b6539bfc Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Thu, 21 Sep 2017 12:27:17 +0200 Subject: [PATCH 12/76] * Handle mutations of nested computed object properties * Resolve #1595 * Resolve #1284 --- src/ast/nodes/MemberExpression.js | 9 ++++++++- src/ast/nodes/shared/FunctionNode.js | 10 ++++++++-- .../samples/side-effects-prototype-assignments/main.js | 1 + 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index b4e85b91aac..709651f9d44 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -91,11 +91,18 @@ export default class MemberExpression extends Node { hasEffectsWhenAssignedAtPath ( path, options ) { if ( this.computed ) { - return this.object.hasEffectsWhenMutatedAtPath( path, options ); + return this.object.hasEffectsWhenMutatedAtPath( [], options ); } return this.object.hasEffectsWhenAssignedAtPath( [ this.property.name, ...path ], options ); } + hasEffectsWhenMutatedAtPath ( path, options ) { + if ( this.computed ) { + return this.object.hasEffectsWhenMutatedAtPath( [], options ); + } + return this.object.hasEffectsWhenMutatedAtPath( [ this.property.name, ...path ], options ); + } + includeInBundle () { let addedNewNodes = super.includeInBundle(); if ( this.variable && !this.variable.included ) { diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index e7065c4ba67..9173f31f489 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -38,8 +38,14 @@ export default class FunctionNode extends Node { || this.body.hasEffects( innerOptions ); } - hasEffectsWhenMutatedAtPath ( path ) { - return this.included || path.length > 0; + hasEffectsWhenMutatedAtPath ( path, options ) { + if ( path.length === 0 ) { + return false; + } + if ( path[ 0 ] === 'prototype' ) { + return this.prototypeObject.hasEffectsWhenMutatedAtPath( path.slice( 1 ), options ); + } + return true; } initialiseNode () { diff --git a/test/form/samples/side-effects-prototype-assignments/main.js b/test/form/samples/side-effects-prototype-assignments/main.js index f7dcc5162ec..39142a71ca9 100644 --- a/test/form/samples/side-effects-prototype-assignments/main.js +++ b/test/form/samples/side-effects-prototype-assignments/main.js @@ -10,5 +10,6 @@ V6Engine.prototype.toString = function () { return 'V6'; }; function IgnoredEngine () {} IgnoredEngine.prototype.toString = function () { return 'IGNORED'; }; +IgnoredEngine.prototype[ 'to' + 'String' ] = function () { return 'IGNORED'; }; console.log( new V8Engine().toString() ); From 649ebc98d7fa83ec8d8e7ba4c32ef4f821817ca5 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Thu, 21 Sep 2017 22:11:43 +0200 Subject: [PATCH 13/76] * Handle calls to MemberExpressions * Pure calls to globals are now handled by GlobalVariable --- src/ast/Node.js | 12 ++++--- src/ast/nodes/ArrowFunctionExpression.js | 7 ++-- src/ast/nodes/CallExpression.js | 4 +-- src/ast/nodes/ClassBody.js | 13 +++++--- src/ast/nodes/ExportDefaultDeclaration.js | 8 ++--- src/ast/nodes/Identifier.js | 8 ++--- src/ast/nodes/MemberExpression.js | 32 ++++++++++--------- src/ast/nodes/MethodDefinition.js | 11 ++++--- src/ast/nodes/NewExpression.js | 4 +-- src/ast/nodes/ObjectExpression.js | 10 ++++++ src/ast/nodes/Property.js | 4 +++ src/ast/nodes/TaggedTemplateExpression.js | 4 +-- src/ast/nodes/shared/ClassNode.js | 12 +++---- src/ast/nodes/shared/FunctionNode.js | 19 +++++++---- src/ast/utils/flatten.js | 16 ---------- src/ast/values.js | 12 +++---- src/ast/variables/DeepSet.js | 12 +++++++ src/ast/variables/GlobalVariable.js | 4 +-- src/ast/variables/LocalVariable.js | 22 +++++++------ src/ast/variables/Variable.js | 4 +-- .../_config.js | 4 +++ .../_expected/amd.js | 15 +++++++++ .../_expected/cjs.js | 13 ++++++++ .../_expected/es.js | 11 +++++++ .../_expected/iife.js | 16 ++++++++++ .../_expected/umd.js | 19 +++++++++++ .../side-effects-object-literal-calls/main.js | 18 +++++++++++ 27 files changed, 220 insertions(+), 94 deletions(-) delete mode 100644 src/ast/utils/flatten.js create mode 100644 test/form/samples/side-effects-object-literal-calls/_config.js create mode 100644 test/form/samples/side-effects-object-literal-calls/_expected/amd.js create mode 100644 test/form/samples/side-effects-object-literal-calls/_expected/cjs.js create mode 100644 test/form/samples/side-effects-object-literal-calls/_expected/es.js create mode 100644 test/form/samples/side-effects-object-literal-calls/_expected/iife.js create mode 100644 test/form/samples/side-effects-object-literal-calls/_expected/umd.js create mode 100644 test/form/samples/side-effects-object-literal-calls/main.js diff --git a/src/ast/Node.js b/src/ast/Node.js index f4fef6553b0..8efb0dc9f59 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -16,7 +16,7 @@ export default class Node { } /** - * Bind an expression as an assignment to a node given an optional path. + * Bind an expression as an assignment to a node given a path. * E.g., node.bindAssignmentAtPath(['x', 'y'], otherNode) is called if otherNode * is assigned to node.x.y. * The default noop implementation is ok as long as hasEffectsWhenAssignedAtPath @@ -27,13 +27,14 @@ export default class Node { bindAssignmentAtPath () {} /** - * Binds ways a node is called to a node. Current options are: + * Binds ways a node is called to a node given a path. Current options are: * - withNew: boolean - Did this call use the "new" operator - * The default noop implementation is ok as long as hasEffectsWhenCalled + * The default noop implementation is ok as long as hasEffectsWhenCalledAtPath * always returns true for this node. Otherwise it should be overridden. + * @param {String[]} path * @param callOptions */ - bindCall () {} + bindCallAtPath () {} eachChild ( callback ) { this.keys.forEach( key => { @@ -86,10 +87,11 @@ export default class Node { } /** + * @param {String[]} path * @param {ExecutionPathOptions} options * @return {boolean} */ - hasEffectsWhenCalled () { + hasEffectsWhenCalledAtPath () { return true; } diff --git a/src/ast/nodes/ArrowFunctionExpression.js b/src/ast/nodes/ArrowFunctionExpression.js index a59ab52a9e6..e6c41876d56 100644 --- a/src/ast/nodes/ArrowFunctionExpression.js +++ b/src/ast/nodes/ArrowFunctionExpression.js @@ -3,7 +3,7 @@ import Scope from '../scopes/Scope.js'; export default class ArrowFunctionExpression extends Node { // Should receive an implementation once we start tracking parameter values - bindCall () {} + bindCallAtPath () {} hasEffects () { return this.included; @@ -16,7 +16,10 @@ export default class ArrowFunctionExpression extends Node { return this.hasEffectsWhenMutatedAtPath( path.slice( 1 ) ); } - hasEffectsWhenCalled ( options ) { + hasEffectsWhenCalledAtPath ( path, options ) { + if ( path.length > 0 ) { + return true; + } return this.params.some( param => param.hasEffects( options ) ) || this.body.hasEffects( options ); } diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index a321ea54619..3e685259505 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -22,13 +22,13 @@ export default class CallExpression extends Node { } super.bind(); - this.callee.bindCall( { withNew: false } ); + this.callee.bindCallAtPath( [], { withNew: false } ); } hasEffects ( options ) { return this.included || this.arguments.some( child => child.hasEffects( options ) ) - || this.callee.hasEffectsWhenCalled( options.getHasEffectsWhenCalledOptions( this.callee ) ); + || this.callee.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions( this.callee ) ); } hasEffectsAsExpressionStatement ( options ) { diff --git a/src/ast/nodes/ClassBody.js b/src/ast/nodes/ClassBody.js index 86153b6a8f2..cd7d71cf060 100644 --- a/src/ast/nodes/ClassBody.js +++ b/src/ast/nodes/ClassBody.js @@ -1,15 +1,18 @@ import Node from '../Node'; export default class ClassBody extends Node { - bindCall ( callOptions ) { - if ( this.classConstructor ) { - this.classConstructor.bindCall( callOptions ); + bindCallAtPath ( path, callOptions ) { + if ( this.classConstructor && path.length === 0 ) { + this.classConstructor.bindCallAtPath( [], callOptions ); } } - hasEffectsWhenCalled ( options ) { + hasEffectsWhenCalledAtPath ( path, options ) { + if ( path.length > 0 ) { + return true; + } if ( this.classConstructor ) { - return this.classConstructor.hasEffectsWhenCalled( options ); + return this.classConstructor.hasEffectsWhenCalledAtPath( [], options ); } return false; } diff --git a/src/ast/nodes/ExportDefaultDeclaration.js b/src/ast/nodes/ExportDefaultDeclaration.js index dfb80e4f7eb..26e16d5aa0d 100644 --- a/src/ast/nodes/ExportDefaultDeclaration.js +++ b/src/ast/nodes/ExportDefaultDeclaration.js @@ -4,8 +4,8 @@ import ExecutionPathOptions from '../ExecutionPathOptions'; const functionOrClassDeclaration = /^(?:Function|Class)Declaration/; export default class ExportDefaultDeclaration extends Node { - addCall ( options ) { - this.declaration.bindCall( options ); + addCallAtPath ( path, options ) { + this.declaration.bindCallAtPath( path, options ); } addReference ( reference ) { @@ -32,8 +32,8 @@ export default class ExportDefaultDeclaration extends Node { return this.name; } - hasEffectsWhenCalled ( options ) { - return this.declaration.hasEffectsWhenCalled( options ); + hasEffectsWhenCalledAtPath ( path, options ) { + return this.declaration.hasEffectsWhenCalledAtPath( path, options ); } includeVariable () { diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index 729735444fd..2f34c5a0193 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -30,9 +30,9 @@ export default class Identifier extends Node { } } - bindCall ( callOptions ) { + bindCallAtPath ( path, callOptions ) { if ( this.variable ) { - this.variable.addCall( callOptions ); + this.variable.addCallAtPath( path, callOptions ); } } @@ -45,9 +45,9 @@ export default class Identifier extends Node { || this.variable.hasEffectsWhenAssignedAtPath( path, options ); } - hasEffectsWhenCalled ( options ) { + hasEffectsWhenCalledAtPath ( path, options ) { return !this.variable - || this.variable.hasEffectsWhenCalled( options ); + || this.variable.hasEffectsWhenCalledAtPath( path, options ); } hasEffectsWhenMutatedAtPath ( path, options ) { diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index 709651f9d44..95f87165972 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -1,8 +1,6 @@ import relativeId from '../../utils/relativeId.js'; import Node from '../Node.js'; -import flatten from '../utils/flatten'; import isReference from 'is-reference'; -import pureFunctions from './shared/pureFunctions'; const validProp = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; @@ -83,9 +81,11 @@ export default class MemberExpression extends Node { } } - bindCall ( callOptions ) { + bindCallAtPath ( path, callOptions ) { if ( this.variable ) { - this.variable.addCall( callOptions ); + this.variable.addCallAtPath( path, callOptions ); + } else if ( !this.computed ) { + this.object.bindCallAtPath( [ this.property.name, ...path ], callOptions ); } } @@ -96,6 +96,19 @@ export default class MemberExpression extends Node { return this.object.hasEffectsWhenAssignedAtPath( [ this.property.name, ...path ], options ); } + hasEffectsWhenCalledAtPath ( path, options ) { + if ( this.variable ) { + return this.variable.hasEffectsWhenCalledAtPath( path, options ); + } + if ( !isReference( this ) ) { + return true; + } + if ( this.computed ) { + return true; + } + return this.object.hasEffectsWhenCalledAtPath( [ this.property.name, ...path ], options ); + } + hasEffectsWhenMutatedAtPath ( path, options ) { if ( this.computed ) { return this.object.hasEffectsWhenMutatedAtPath( [], options ); @@ -112,17 +125,6 @@ export default class MemberExpression extends Node { return addedNewNodes; } - hasEffectsWhenCalled ( options ) { - if ( this.variable ) { - return this.variable.hasEffectsWhenCalled( options ); - } - if ( !isReference( this ) ) { - return true; - } - const flattenedNode = flatten( this ); - return !(this.scope.findVariable( flattenedNode.name ).isGlobal && pureFunctions[ flattenedNode.keypath ]); - } - render ( code, es ) { if ( this.variable ) { const name = this.variable.getName( es ); diff --git a/src/ast/nodes/MethodDefinition.js b/src/ast/nodes/MethodDefinition.js index 30f559f736c..7427e87995b 100644 --- a/src/ast/nodes/MethodDefinition.js +++ b/src/ast/nodes/MethodDefinition.js @@ -1,15 +1,18 @@ import Node from '../Node'; export default class MethodDefinition extends Node { - bindCall ( callOptions ) { - this.value.bindCall( callOptions ); + bindCallAtPath ( path, callOptions ) { + this.value.bindCallAtPath( path, callOptions ); } hasEffects ( options ) { return this.key.hasEffects( options ); } - hasEffectsWhenCalled ( options ) { - return this.value.hasEffectsWhenCalled( options ); + hasEffectsWhenCalledAtPath ( path, options ) { + if ( path.length > 0 ) { + return true; + } + return this.value.hasEffectsWhenCalledAtPath( [], options ); } } diff --git a/src/ast/nodes/NewExpression.js b/src/ast/nodes/NewExpression.js index 8137ddbf2da..8ff23c32c4b 100644 --- a/src/ast/nodes/NewExpression.js +++ b/src/ast/nodes/NewExpression.js @@ -3,12 +3,12 @@ import Node from '../Node.js'; export default class NewExpression extends Node { bind () { super.bind(); - this.callee.bindCall( { withNew: true } ); + this.callee.bindCallAtPath( [], { withNew: true } ); } hasEffects ( options ) { return this.included || this.arguments.some( child => child.hasEffects( options ) ) - || this.callee.hasEffectsWhenCalled( options.getHasEffectsWhenCalledOptions( this.callee ) ); + || this.callee.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions( this.callee ) ); } } diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index 0a8d3e69f43..309ecdc1c1b 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -11,6 +11,16 @@ export default class ObjectExpression extends Node { || accessedProperty.hasEffectsWhenAssignedAtPath( path.slice( 1 ), options ); } + hasEffectsWhenCalledAtPath ( path, options ) { + if ( path.length === 0 ) { + return true; + } + const accessedProperty = this.properties.find( property => !property.computed && property.key.name === path[ 0 ] ); + + return !accessedProperty + || accessedProperty.hasEffectsWhenCalledAtPath( path.slice( 1 ), options ); + } + hasEffectsWhenMutatedAtPath ( path ) { return path.length > 1; } diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index 28d8b5290a5..b59c2670e53 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -10,6 +10,10 @@ export default class Property extends Node { return this.value.hasEffectsWhenAssignedAtPath( path, options ); } + hasEffectsWhenCalledAtPath ( path, options ) { + return this.value.hasEffectsWhenCalledAtPath( path, options ); + } + initialiseAndDeclare ( parentScope, kind ) { this.initialiseScope( parentScope ); this.key.initialise( parentScope ); diff --git a/src/ast/nodes/TaggedTemplateExpression.js b/src/ast/nodes/TaggedTemplateExpression.js index 886fae2546a..81431bd710b 100644 --- a/src/ast/nodes/TaggedTemplateExpression.js +++ b/src/ast/nodes/TaggedTemplateExpression.js @@ -22,11 +22,11 @@ export default class TaggedTemplateExpression extends Node { } super.bind(); - this.tag.bindCall( { withNew: false } ); + this.tag.bindCallAtPath( [], { withNew: false } ); } hasEffects ( options ) { return super.hasEffects( options ) - || this.tag.hasEffectsWhenCalled( options.getHasEffectsWhenCalledOptions( this.tag ) ); + || this.tag.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions( this.tag ) ); } } diff --git a/src/ast/nodes/shared/ClassNode.js b/src/ast/nodes/shared/ClassNode.js index 64886d3460f..2fbe37505fd 100644 --- a/src/ast/nodes/shared/ClassNode.js +++ b/src/ast/nodes/shared/ClassNode.js @@ -2,20 +2,20 @@ import Node from '../../Node.js'; import Scope from '../../scopes/Scope'; export default class ClassNode extends Node { - bindCall ( callOptions ) { + bindCallAtPath ( path, callOptions ) { if ( this.superClass ) { - this.superClass.bindCall( callOptions ); + this.superClass.bindCallAtPath( path, callOptions ); } - this.body.bindCall( callOptions ); + this.body.bindCallAtPath( path, callOptions ); } hasEffectsAsExpressionStatement ( options ) { return this.hasEffects( options ); } - hasEffectsWhenCalled ( options ) { - return this.body.hasEffectsWhenCalled( options ) - || ( this.superClass && this.superClass.hasEffectsWhenCalled( options ) ); + hasEffectsWhenCalledAtPath ( path, options ) { + return this.body.hasEffectsWhenCalledAtPath( path, options ) + || ( this.superClass && this.superClass.hasEffectsWhenCalledAtPath( path, options ) ); } initialiseChildren () { diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index 9173f31f489..e09d35f8438 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -4,13 +4,15 @@ import { UNKNOWN_ASSIGNMENT, UNKNOWN_OBJECT_LITERAL } from '../../values'; import VirtualObjectExpression from './VirtualObjectExpression'; export default class FunctionNode extends Node { - bindCall ( { withNew } ) { - const thisVariable = this.scope.findVariable( 'this' ); + bindCallAtPath ( path, { withNew } ) { + if ( path.length === 0 ) { + const thisVariable = this.scope.findVariable( 'this' ); - if ( withNew ) { - thisVariable.assignExpressionAtPath( [], UNKNOWN_OBJECT_LITERAL ); - } else { - thisVariable.assignExpressionAtPath( [], UNKNOWN_ASSIGNMENT ); + if ( withNew ) { + thisVariable.assignExpressionAtPath( [], UNKNOWN_OBJECT_LITERAL ); + } else { + thisVariable.assignExpressionAtPath( [], UNKNOWN_ASSIGNMENT ); + } } } @@ -32,7 +34,10 @@ export default class FunctionNode extends Node { return true; } - hasEffectsWhenCalled ( options ) { + hasEffectsWhenCalledAtPath ( path, options ) { + if ( path.length > 0 ) { + return true; + } const innerOptions = options.setIgnoreSafeThisMutations(); return this.params.some( param => param.hasEffects( innerOptions ) ) || this.body.hasEffects( innerOptions ); diff --git a/src/ast/utils/flatten.js b/src/ast/utils/flatten.js deleted file mode 100644 index 2000c8941ab..00000000000 --- a/src/ast/utils/flatten.js +++ /dev/null @@ -1,16 +0,0 @@ -export default function flatten ( node ) { - const parts = []; - while ( node.type === 'MemberExpression' ) { - if ( node.computed ) return null; - parts.unshift( node.property.name ); - - node = node.object; - } - - if ( node.type !== 'Identifier' ) return null; - - const name = node.name; - parts.unshift( name ); - - return { name, keypath: parts.join( '.' ) }; -} diff --git a/src/ast/values.js b/src/ast/values.js index 99325e22da3..715083ae3d0 100644 --- a/src/ast/values.js +++ b/src/ast/values.js @@ -2,24 +2,24 @@ export const UNKNOWN_VALUE = { toString: () => '[[UNKNOWN]]' }; export const UNKNOWN_ASSIGNMENT = { type: 'UNKNOWN', - bindCall: () => {}, + bindCallAtPath: () => {}, hasEffectsWhenAssignedAtPath: () => true, - hasEffectsWhenCalled: () => true, + hasEffectsWhenCalledAtPath: () => true, hasEffectsWhenMutatedAtPath: () => true, }; export const UNDEFINED_ASSIGNMENT = { type: 'UNDEFINED', - bindCall: () => {}, + bindCallAtPath: () => {}, hasEffectsWhenAssignedAtPath: () => true, - hasEffectsWhenCalled: () => true, + hasEffectsWhenCalledAtPath: () => true, hasEffectsWhenMutatedAtPath: () => true, }; export const UNKNOWN_OBJECT_LITERAL = { type: 'UNKNOWN_OBJECT_LITERAL', - bindCall: () => {}, + bindCallAtPath: () => {}, hasEffectsWhenAssignedAtPath: path => path.length > 1, - hasEffectsWhenCalled: () => true, + hasEffectsWhenCalledAtPath: () => true, hasEffectsWhenMutatedAtPath: path => path.length > 0, }; diff --git a/src/ast/variables/DeepSet.js b/src/ast/variables/DeepSet.js index f1a50aec106..7ad38a48c19 100644 --- a/src/ast/variables/DeepSet.js +++ b/src/ast/variables/DeepSet.js @@ -25,6 +25,18 @@ export default class DeepSet { } } + hasAtPath ( path, assignment ) { + if ( path.length === 0 ) { + return this._assignments.get( SET_KEY ).has( assignment ); + } else { + 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 ) ) diff --git a/src/ast/variables/GlobalVariable.js b/src/ast/variables/GlobalVariable.js index 6a568605bed..a412da44999 100644 --- a/src/ast/variables/GlobalVariable.js +++ b/src/ast/variables/GlobalVariable.js @@ -14,7 +14,7 @@ export default class GlobalVariable extends Variable { if ( reference.isReassignment ) this.isReassigned = true; } - hasEffectsWhenCalled () { - return !pureFunctions[ this.name ]; + hasEffectsWhenCalledAtPath ( path ) { + return !pureFunctions[ [ this.name, ...path ].join( '.' ) ]; } } diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index 4999a1ae924..d8e4c4d3863 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -9,25 +9,27 @@ export default class LocalVariable extends Variable { this.declarations = new Set( declarator ? [ declarator ] : null ); this.assignedExpressions = new DeepSet(); init && this.assignedExpressions.addAtPath( [], init ); - this.calls = new Set(); + this.calls = new DeepSet(); } addDeclaration ( identifier ) { this.declarations.add( identifier ); } - addCall ( callOptions ) { + addCallAtPath ( path, callOptions ) { // To prevent infinite loops - if ( this.calls.has( callOptions ) ) return; - this.calls.add( callOptions ); - this.assignedExpressions.forEachAtPath( [], ( relativePath, expression ) => expression.bindCall( callOptions ) ); + if ( this.calls.hasAtPath( path, callOptions ) ) return; + this.calls.addAtPath( path, callOptions ); + this.assignedExpressions.forEachAtPath( path, ( relativePath, expression ) => + expression.bindCallAtPath( relativePath, callOptions ) ); } assignExpressionAtPath ( path, expression ) { this.assignedExpressions.addAtPath( path, expression ); + this.calls.forEachAtPath( path, ( relativePath, callOptions ) => + expression.bindCallAtPath( relativePath, callOptions ) ); if ( path.length === 0 ) { this.isReassigned = true; - this.calls.forEach( callOptions => expression.bindCall( callOptions ) ); } } @@ -47,10 +49,10 @@ export default class LocalVariable extends Variable { )); } - hasEffectsWhenCalled ( options ) { - return this.assignedExpressions.someAtPath( [], ( relativePath, node ) => - !options.hasNodeBeenCalled( node ) - && node.hasEffectsWhenCalled( options.getHasEffectsWhenCalledOptions( node ) ) + hasEffectsWhenCalledAtPath ( path, options ) { + return this.assignedExpressions.someAtPath( path, ( relativePath, node ) => + !(relativePath.length === 0 && options.hasNodeBeenCalled( node )) + && node.hasEffectsWhenCalledAtPath( relativePath, options.getHasEffectsWhenCalledOptions( node ) ) ); } diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index bec1f172d37..eabcec063d8 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -3,7 +3,7 @@ export default class Variable { this.name = name; } - addCall () {} + addCallAtPath () {} addReference () {} @@ -17,7 +17,7 @@ export default class Variable { return true; } - hasEffectsWhenCalled () { + hasEffectsWhenCalledAtPath () { return true; } diff --git a/test/form/samples/side-effects-object-literal-calls/_config.js b/test/form/samples/side-effects-object-literal-calls/_config.js new file mode 100644 index 00000000000..423a24bd345 --- /dev/null +++ b/test/form/samples/side-effects-object-literal-calls/_config.js @@ -0,0 +1,4 @@ +module.exports = { + description: 'detects side-effects when mutating object literals', + options: { name: 'bundle' } +}; diff --git a/test/form/samples/side-effects-object-literal-calls/_expected/amd.js b/test/form/samples/side-effects-object-literal-calls/_expected/amd.js new file mode 100644 index 00000000000..b6ae95623d7 --- /dev/null +++ b/test/form/samples/side-effects-object-literal-calls/_expected/amd.js @@ -0,0 +1,15 @@ +define(function () { 'use strict'; + + const retained1 = { x: () => {} }; + retained1.y(); + + const retained2 = { x: () => {} }; + retained2.x = {}; + retained2.x(); + + const retained3 = { x: { y: globalFunc } }; + const retained4 = { x: { y: {} } }; + retained4.x = retained3.x; + retained4.x.y(); + +}); diff --git a/test/form/samples/side-effects-object-literal-calls/_expected/cjs.js b/test/form/samples/side-effects-object-literal-calls/_expected/cjs.js new file mode 100644 index 00000000000..7b80fb73a0f --- /dev/null +++ b/test/form/samples/side-effects-object-literal-calls/_expected/cjs.js @@ -0,0 +1,13 @@ +'use strict'; + +const retained1 = { x: () => {} }; +retained1.y(); + +const retained2 = { x: () => {} }; +retained2.x = {}; +retained2.x(); + +const retained3 = { x: { y: globalFunc } }; +const retained4 = { x: { y: {} } }; +retained4.x = retained3.x; +retained4.x.y(); diff --git a/test/form/samples/side-effects-object-literal-calls/_expected/es.js b/test/form/samples/side-effects-object-literal-calls/_expected/es.js new file mode 100644 index 00000000000..4c89e827b77 --- /dev/null +++ b/test/form/samples/side-effects-object-literal-calls/_expected/es.js @@ -0,0 +1,11 @@ +const retained1 = { x: () => {} }; +retained1.y(); + +const retained2 = { x: () => {} }; +retained2.x = {}; +retained2.x(); + +const retained3 = { x: { y: globalFunc } }; +const retained4 = { x: { y: {} } }; +retained4.x = retained3.x; +retained4.x.y(); diff --git a/test/form/samples/side-effects-object-literal-calls/_expected/iife.js b/test/form/samples/side-effects-object-literal-calls/_expected/iife.js new file mode 100644 index 00000000000..8c3da05314e --- /dev/null +++ b/test/form/samples/side-effects-object-literal-calls/_expected/iife.js @@ -0,0 +1,16 @@ +(function () { + 'use strict'; + + const retained1 = { x: () => {} }; + retained1.y(); + + const retained2 = { x: () => {} }; + retained2.x = {}; + retained2.x(); + + const retained3 = { x: { y: globalFunc } }; + const retained4 = { x: { y: {} } }; + retained4.x = retained3.x; + retained4.x.y(); + +}()); diff --git a/test/form/samples/side-effects-object-literal-calls/_expected/umd.js b/test/form/samples/side-effects-object-literal-calls/_expected/umd.js new file mode 100644 index 00000000000..44848bddc6b --- /dev/null +++ b/test/form/samples/side-effects-object-literal-calls/_expected/umd.js @@ -0,0 +1,19 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + const retained1 = { x: () => {} }; + retained1.y(); + + const retained2 = { x: () => {} }; + retained2.x = {}; + retained2.x(); + + const retained3 = { x: { y: globalFunc } }; + const retained4 = { x: { y: {} } }; + retained4.x = retained3.x; + retained4.x.y(); + +}))); diff --git a/test/form/samples/side-effects-object-literal-calls/main.js b/test/form/samples/side-effects-object-literal-calls/main.js new file mode 100644 index 00000000000..73d97862080 --- /dev/null +++ b/test/form/samples/side-effects-object-literal-calls/main.js @@ -0,0 +1,18 @@ +const removed1 = { x: () => {} }; +removed1.x(); + +const retained1 = { x: () => {} }; +retained1.y(); + +const retained2 = { x: () => {} }; +retained2.x = {}; +retained2.x(); + +const removed2 = { x: { y: () => {} } }; +removed2.x.y = function () {}; +removed2.x.y(); + +const retained3 = { x: { y: globalFunc } }; +const retained4 = { x: { y: {} } }; +retained4.x = retained3.x; +retained4.x.y(); From 777c868b1223295d576384d79642f4809ec50be6 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Thu, 21 Sep 2017 22:32:16 +0200 Subject: [PATCH 14/76] Simplify logic for infinite loop prevention in mutations and calls --- src/ast/ExecutionPathOptions.js | 10 ++++------ src/ast/variables/LocalVariable.js | 26 ++++++++++++++++---------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/ast/ExecutionPathOptions.js b/src/ast/ExecutionPathOptions.js index 17883bd81f4..2d222db9591 100644 --- a/src/ast/ExecutionPathOptions.js +++ b/src/ast/ExecutionPathOptions.js @@ -133,21 +133,19 @@ export default class ExecutionPathOptions { } /** - * @param {String[]} path * @param {Node} node * @return {ExecutionPathOptions} */ - addMutatedNodeAtPath ( path, node ) { - return this.setIn( [ OPTION_MUTATED_NODES, node, ...path, RESULT_KEY ], true ); + addMutatedNode ( node ) { + return this.setIn( [ OPTION_MUTATED_NODES, node ], true ); } /** - * @param {String[]} path * @param {Node} node * @return {boolean} */ - hasNodeBeenMutatedAtPath ( path, node ) { - return this._optionValues.getIn( [ OPTION_MUTATED_NODES, node, ...path, RESULT_KEY ] ); + hasNodeBeenMutated ( node ) { + return this._optionValues.getIn( [ OPTION_MUTATED_NODES, node ] ); } /** diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index d8e4c4d3863..4d63448605c 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -42,26 +42,32 @@ export default class LocalVariable extends Variable { hasEffectsWhenAssignedAtPath ( path, options ) { return this.included - || (path.length > 0 && this.assignedExpressions.someAtPath( path, ( relativePath, node ) => + || this.assignedExpressions.someAtPath( path, ( relativePath, node ) => relativePath.length > 0 && !options.hasNodeBeenAssignedAtPath( relativePath, node ) && node.hasEffectsWhenAssignedAtPath( relativePath, options.addAssignedNodeAtPath( relativePath, node ) ) - )); + ); } hasEffectsWhenCalledAtPath ( path, options ) { - return this.assignedExpressions.someAtPath( path, ( relativePath, node ) => - !(relativePath.length === 0 && options.hasNodeBeenCalled( node )) - && node.hasEffectsWhenCalledAtPath( relativePath, options.getHasEffectsWhenCalledOptions( node ) ) - ); + return this.assignedExpressions.someAtPath( path, ( relativePath, node ) => { + if ( relativePath.length === 0 ) { + return !options.hasNodeBeenCalled( node ) + && node.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions( node ) ); + } + return node.hasEffectsWhenCalledAtPath( relativePath, options ); + } ); } hasEffectsWhenMutatedAtPath ( path, options ) { return this.included - || this.assignedExpressions.someAtPath( path, ( relativePath, node ) => - !options.hasNodeBeenMutatedAtPath( relativePath, node ) && - node.hasEffectsWhenMutatedAtPath( relativePath, options.addMutatedNodeAtPath( relativePath, node ) ) - ); + || this.assignedExpressions.someAtPath( path, ( relativePath, node ) => { + if ( relativePath.length === 0 ) { + return !options.hasNodeBeenMutated( node ) + && node.hasEffectsWhenMutatedAtPath( [], options.addMutatedNode( node ) ); + } + return node.hasEffectsWhenMutatedAtPath( relativePath, options ); + } ); } includeVariable () { From 221890d4f1f054bb804ce07db58570ad14b6d18d Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Sun, 24 Sep 2017 20:31:49 +0200 Subject: [PATCH 15/76] * Always try to bind its variable to an Identifier first before binding anything else to avoid race conditions * Properly handle reassignments inside ObjectExpressions * Prevent infinite loops for reassignments --- src/ast/nodes/Identifier.js | 14 +++++++++---- src/ast/nodes/MemberExpression.js | 17 ++++++++++++++- src/ast/nodes/ObjectExpression.js | 21 +++++++++++++++++++ src/ast/nodes/Property.js | 8 +++++-- src/ast/values.js | 3 +++ src/ast/variables/LocalVariable.js | 14 ++++++++----- .../_expected/amd.js | 12 ++++++++++- .../_expected/cjs.js | 12 ++++++++++- .../_expected/es.js | 12 ++++++++++- .../_expected/iife.js | 12 ++++++++++- .../_expected/umd.js | 12 ++++++++++- .../side-effects-object-literal-calls/main.js | 12 ++++++++++- 12 files changed, 131 insertions(+), 18 deletions(-) diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index 2f34c5a0193..151de840c86 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -18,24 +18,30 @@ function isAssignmentPatternLhs ( node, parent ) { export default class Identifier extends Node { bind () { - if ( isReference( this, this.parent ) || isAssignmentPatternLhs( this, this.parent ) ) { - this.variable = this.scope.findVariable( this.name ); - this.variable.addReference( this ); - } + this._bindVariableIfMissing(); } bindAssignmentAtPath ( path, expression ) { + this._bindVariableIfMissing(); if ( this.variable ) { this.variable.assignExpressionAtPath( path, expression ); } } bindCallAtPath ( path, callOptions ) { + this._bindVariableIfMissing(); if ( this.variable ) { this.variable.addCallAtPath( path, callOptions ); } } + _bindVariableIfMissing () { + if ( !this.variable && (isReference( this, this.parent ) || isAssignmentPatternLhs( this, this.parent )) ) { + this.variable = this.scope.findVariable( this.name ); + this.variable.addReference( this ); + } + } + hasEffectsAsExpressionStatement ( options ) { return this.hasEffects( options ) || this.variable.isGlobal; } diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index 95f87165972..97555dd88e9 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -31,6 +31,7 @@ export default class MemberExpression extends Node { // if this resolves to a namespaced declaration, prepare // to replace it // TODO this code is a bit inefficient + this._bound = true; const keypath = new Keypath( this ); if ( !keypath.computed && keypath.root.type === 'Identifier' ) { @@ -76,12 +77,20 @@ export default class MemberExpression extends Node { } bindAssignmentAtPath ( path, expression ) { - if ( !this.computed ) { + if ( !this._bound ) { + this.bind(); + } + if ( this.variable ) { + this.variable.assignExpressionAtPath( path, expression ); + } else if ( !this.computed ) { this.object.bindAssignmentAtPath( [ this.property.name, ...path ], expression ); } } bindCallAtPath ( path, callOptions ) { + if ( !this._bound ) { + this.bind(); + } if ( this.variable ) { this.variable.addCallAtPath( path, callOptions ); } else if ( !this.computed ) { @@ -90,6 +99,9 @@ export default class MemberExpression extends Node { } hasEffectsWhenAssignedAtPath ( path, options ) { + if ( this.variable ) { + return this.variable.hasEffectsWhenAssignedAtPath( path, options ); + } if ( this.computed ) { return this.object.hasEffectsWhenMutatedAtPath( [], options ); } @@ -110,6 +122,9 @@ export default class MemberExpression extends Node { } hasEffectsWhenMutatedAtPath ( path, options ) { + if ( this.variable ) { + return this.variable.hasEffectsWhenMutatedAtPath( path, options ); + } if ( this.computed ) { return this.object.hasEffectsWhenMutatedAtPath( [], options ); } diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index 309ecdc1c1b..ff2ec78260a 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -1,6 +1,27 @@ import Node from '../Node.js'; export default class ObjectExpression extends Node { + bindAssignmentAtPath ( path, expression ) { + if ( path.length === 0 ) { + return; + } + const accessedProperty = this.properties.find( property => !property.computed && property.key.name === path[ 0 ] ); + if ( accessedProperty ) { + accessedProperty.bindAssignmentAtPath( path.slice( 1 ), expression ); + } + } + + bindCallAtPath ( path, callOptions ) { + if ( path.length === 0 ) { + return; + } + const accessedProperty = this.properties.find( property => !property.computed && property.key.name === path[ 0 ] ); + + if ( accessedProperty ) { + accessedProperty.bindCallAtPath( path.slice( 1 ), callOptions ); + } + } + hasEffectsWhenAssignedAtPath ( path, options ) { if ( path.length <= 1 ) { return false; diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index b59c2670e53..cd5fa8f3f19 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -2,8 +2,12 @@ import Node from '../Node.js'; import { UNKNOWN_ASSIGNMENT } from '../values'; export default class Property extends Node { - bindAssignmentAtPath () { - this.value.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT ); + bindAssignmentAtPath ( path, expression ) { + this.value.bindAssignmentAtPath( path, expression ); + } + + bindCallAtPath ( path, callOptions ) { + this.value.bindCallAtPath( path, callOptions ); } hasEffectsWhenAssignedAtPath ( path, options ) { diff --git a/src/ast/values.js b/src/ast/values.js index 715083ae3d0..5921576a8d2 100644 --- a/src/ast/values.js +++ b/src/ast/values.js @@ -2,6 +2,7 @@ export const UNKNOWN_VALUE = { toString: () => '[[UNKNOWN]]' }; export const UNKNOWN_ASSIGNMENT = { type: 'UNKNOWN', + bindAssignmentAtPath: () => {}, bindCallAtPath: () => {}, hasEffectsWhenAssignedAtPath: () => true, hasEffectsWhenCalledAtPath: () => true, @@ -10,6 +11,7 @@ export const UNKNOWN_ASSIGNMENT = { export const UNDEFINED_ASSIGNMENT = { type: 'UNDEFINED', + bindAssignmentAtPath: () => {}, bindCallAtPath: () => {}, hasEffectsWhenAssignedAtPath: () => true, hasEffectsWhenCalledAtPath: () => true, @@ -18,6 +20,7 @@ export const UNDEFINED_ASSIGNMENT = { export const UNKNOWN_OBJECT_LITERAL = { type: 'UNKNOWN_OBJECT_LITERAL', + bindAssignmentAtPath: () => {}, bindCallAtPath: () => {}, hasEffectsWhenAssignedAtPath: path => path.length > 1, hasEffectsWhenCalledAtPath: () => true, diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index 4d63448605c..b06e35f557b 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -17,15 +17,20 @@ export default class LocalVariable extends Variable { } addCallAtPath ( path, callOptions ) { - // To prevent infinite loops if ( this.calls.hasAtPath( path, callOptions ) ) return; this.calls.addAtPath( path, callOptions ); - this.assignedExpressions.forEachAtPath( path, ( relativePath, expression ) => - expression.bindCallAtPath( relativePath, callOptions ) ); + this.assignedExpressions.forEachAtPath( path, ( relativePath, node ) => + node.bindCallAtPath( relativePath, callOptions ) ); } assignExpressionAtPath ( path, expression ) { + if ( this.assignedExpressions.hasAtPath( path, expression ) ) return; this.assignedExpressions.addAtPath( path, expression ); + if ( path.length > 0 ) { + this.assignedExpressions.forEachAtPath( path.slice( 0, -1 ), ( relativePath, node ) => { + return node.bindAssignmentAtPath( [ ...relativePath, ...path.slice( -1 ) ], expression ); + } ); + } this.calls.forEachAtPath( path, ( relativePath, callOptions ) => expression.bindCallAtPath( relativePath, callOptions ) ); if ( path.length === 0 ) { @@ -45,8 +50,7 @@ export default class LocalVariable extends Variable { || this.assignedExpressions.someAtPath( path, ( relativePath, node ) => relativePath.length > 0 && !options.hasNodeBeenAssignedAtPath( relativePath, node ) - && node.hasEffectsWhenAssignedAtPath( relativePath, options.addAssignedNodeAtPath( relativePath, node ) ) - ); + && node.hasEffectsWhenAssignedAtPath( relativePath, options.addAssignedNodeAtPath( relativePath, node ) ) ); } hasEffectsWhenCalledAtPath ( path, options ) { diff --git a/test/form/samples/side-effects-object-literal-calls/_expected/amd.js b/test/form/samples/side-effects-object-literal-calls/_expected/amd.js index b6ae95623d7..62791770a75 100644 --- a/test/form/samples/side-effects-object-literal-calls/_expected/amd.js +++ b/test/form/samples/side-effects-object-literal-calls/_expected/amd.js @@ -7,9 +7,19 @@ define(function () { 'use strict'; retained2.x = {}; retained2.x(); - const retained3 = { x: { y: globalFunc } }; + const retained3 = { x: { y: () => console.log('effect') } }; const retained4 = { x: { y: {} } }; retained4.x = retained3.x; retained4.x.y(); + const retained5 = { x: () => {} }; + const retained6 = retained5; + retained6.x = () => console.log('effect'); + retained5.x(); + + const retained7 = { x: { y: () => {} } }; + const retained8 = { x: retained7.x }; + retained8.x.y = () => console.log( 'effect' ); + retained7.x.y(); + }); diff --git a/test/form/samples/side-effects-object-literal-calls/_expected/cjs.js b/test/form/samples/side-effects-object-literal-calls/_expected/cjs.js index 7b80fb73a0f..f46c6e0344f 100644 --- a/test/form/samples/side-effects-object-literal-calls/_expected/cjs.js +++ b/test/form/samples/side-effects-object-literal-calls/_expected/cjs.js @@ -7,7 +7,17 @@ const retained2 = { x: () => {} }; retained2.x = {}; retained2.x(); -const retained3 = { x: { y: globalFunc } }; +const retained3 = { x: { y: () => console.log('effect') } }; const retained4 = { x: { y: {} } }; retained4.x = retained3.x; retained4.x.y(); + +const retained5 = { x: () => {} }; +const retained6 = retained5; +retained6.x = () => console.log('effect'); +retained5.x(); + +const retained7 = { x: { y: () => {} } }; +const retained8 = { x: retained7.x }; +retained8.x.y = () => console.log( 'effect' ); +retained7.x.y(); diff --git a/test/form/samples/side-effects-object-literal-calls/_expected/es.js b/test/form/samples/side-effects-object-literal-calls/_expected/es.js index 4c89e827b77..fc8dcbfe778 100644 --- a/test/form/samples/side-effects-object-literal-calls/_expected/es.js +++ b/test/form/samples/side-effects-object-literal-calls/_expected/es.js @@ -5,7 +5,17 @@ const retained2 = { x: () => {} }; retained2.x = {}; retained2.x(); -const retained3 = { x: { y: globalFunc } }; +const retained3 = { x: { y: () => console.log('effect') } }; const retained4 = { x: { y: {} } }; retained4.x = retained3.x; retained4.x.y(); + +const retained5 = { x: () => {} }; +const retained6 = retained5; +retained6.x = () => console.log('effect'); +retained5.x(); + +const retained7 = { x: { y: () => {} } }; +const retained8 = { x: retained7.x }; +retained8.x.y = () => console.log( 'effect' ); +retained7.x.y(); diff --git a/test/form/samples/side-effects-object-literal-calls/_expected/iife.js b/test/form/samples/side-effects-object-literal-calls/_expected/iife.js index 8c3da05314e..cc2c085f035 100644 --- a/test/form/samples/side-effects-object-literal-calls/_expected/iife.js +++ b/test/form/samples/side-effects-object-literal-calls/_expected/iife.js @@ -8,9 +8,19 @@ retained2.x = {}; retained2.x(); - const retained3 = { x: { y: globalFunc } }; + const retained3 = { x: { y: () => console.log('effect') } }; const retained4 = { x: { y: {} } }; retained4.x = retained3.x; retained4.x.y(); + const retained5 = { x: () => {} }; + const retained6 = retained5; + retained6.x = () => console.log('effect'); + retained5.x(); + + const retained7 = { x: { y: () => {} } }; + const retained8 = { x: retained7.x }; + retained8.x.y = () => console.log( 'effect' ); + retained7.x.y(); + }()); diff --git a/test/form/samples/side-effects-object-literal-calls/_expected/umd.js b/test/form/samples/side-effects-object-literal-calls/_expected/umd.js index 44848bddc6b..764e3fc71cf 100644 --- a/test/form/samples/side-effects-object-literal-calls/_expected/umd.js +++ b/test/form/samples/side-effects-object-literal-calls/_expected/umd.js @@ -11,9 +11,19 @@ retained2.x = {}; retained2.x(); - const retained3 = { x: { y: globalFunc } }; + const retained3 = { x: { y: () => console.log('effect') } }; const retained4 = { x: { y: {} } }; retained4.x = retained3.x; retained4.x.y(); + const retained5 = { x: () => {} }; + const retained6 = retained5; + retained6.x = () => console.log('effect'); + retained5.x(); + + const retained7 = { x: { y: () => {} } }; + const retained8 = { x: retained7.x }; + retained8.x.y = () => console.log( 'effect' ); + retained7.x.y(); + }))); 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 73d97862080..434d04938a2 100644 --- a/test/form/samples/side-effects-object-literal-calls/main.js +++ b/test/form/samples/side-effects-object-literal-calls/main.js @@ -12,7 +12,17 @@ const removed2 = { x: { y: () => {} } }; removed2.x.y = function () {}; removed2.x.y(); -const retained3 = { x: { y: globalFunc } }; +const retained3 = { x: { y: () => console.log('effect') } }; const retained4 = { x: { y: {} } }; retained4.x = retained3.x; retained4.x.y(); + +const retained5 = { x: () => {} }; +const retained6 = retained5; +retained6.x = () => console.log('effect'); +retained5.x(); + +const retained7 = { x: { y: () => {} } }; +const retained8 = { x: retained7.x }; +retained8.x.y = () => console.log( 'effect' ); +retained7.x.y(); From b1248289157dcee881b1f69b7d65d2e576b0d7f4 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Sun, 24 Sep 2017 21:39:09 +0200 Subject: [PATCH 16/76] On addDeclaration, return the declared variable to simplify logic in Identifier --- src/ast/nodes/Identifier.js | 23 ++++------------------- src/ast/scopes/BlockScope.js | 4 ++-- src/ast/scopes/Scope.js | 3 +++ 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index 151de840c86..c1e5c0ec4e7 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -1,21 +1,6 @@ import Node from '../Node.js'; import isReference from 'is-reference'; -function isAssignmentPatternLhs ( node, parent ) { - // special case: `({ foo = 42 }) => {...}` - // `foo` actually has two different parents, the Property of the - // ObjectPattern, and the AssignmentPattern. In one case it's a - // reference, in one case it's not, because it's shorthand for - // `({ foo: foo = 42 }) => {...}`. But unlike a regular shorthand - // property, the `foo` node appears at different levels of the tree - return ( - parent.type === 'Property' && - parent.shorthand && - parent.value.type === 'AssignmentPattern' && - parent.value.left === node - ); -} - export default class Identifier extends Node { bind () { this._bindVariableIfMissing(); @@ -36,7 +21,7 @@ export default class Identifier extends Node { } _bindVariableIfMissing () { - if ( !this.variable && (isReference( this, this.parent ) || isAssignmentPatternLhs( this, this.parent )) ) { + if ( !this.variable && isReference( this, this.parent ) ) { this.variable = this.scope.findVariable( this.name ); this.variable.addReference( this ); } @@ -73,15 +58,15 @@ export default class Identifier extends Node { switch ( kind ) { case 'var': case 'function': - this.scope.addDeclaration( this, { isHoisted: true, init } ); + this.variable = this.scope.addDeclaration( this, { isHoisted: true, init } ); break; case 'let': case 'const': case 'class': - this.scope.addDeclaration( this, { init } ); + this.variable = this.scope.addDeclaration( this, { init } ); break; case 'parameter': - this.scope.addParameterDeclaration( this ); + this.variable = this.scope.addParameterDeclaration( this ); break; default: throw new Error( 'Unexpected identifier kind', kind ); diff --git a/src/ast/scopes/BlockScope.js b/src/ast/scopes/BlockScope.js index 47beb8de142..8d110d62f34 100644 --- a/src/ast/scopes/BlockScope.js +++ b/src/ast/scopes/BlockScope.js @@ -3,9 +3,9 @@ import Scope from './Scope'; export default class BlockScope extends Scope { addDeclaration ( identifier, options = {} ) { if ( options.isHoisted ) { - this.parent.addDeclaration( identifier, options ); + return this.parent.addDeclaration( identifier, options ); } else { - super.addDeclaration( identifier, options ); + return super.addDeclaration( identifier, options ); } } } diff --git a/src/ast/scopes/Scope.js b/src/ast/scopes/Scope.js index d64c91854db..9d9cf53ed8a 100644 --- a/src/ast/scopes/Scope.js +++ b/src/ast/scopes/Scope.js @@ -19,6 +19,7 @@ export default class Scope { * @param {Object} [options] - valid options are * {(Node|null)} init * {boolean} isHoisted + * @return {Variable} */ addDeclaration ( identifier, options = {} ) { const name = identifier.name; @@ -29,11 +30,13 @@ export default class Scope { } else { this.variables[ name ] = new LocalVariable( identifier.name, identifier, options.init || UNDEFINED_ASSIGNMENT ); } + return this.variables[ name ]; } addParameterDeclaration ( identifier ) { const name = identifier.name; this.variables[ name ] = new ParameterVariable( name, identifier ); + return this.variables[ name ]; } contains ( name ) { From 7a7570779cb795738ff3a39c9e7b23c6b2f013e1 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Mon, 25 Sep 2017 07:07:37 +0200 Subject: [PATCH 17/76] * Separate ExportDefaultDeclaration from its variable to not break the new object shape features across default exports * Make sure property reassignments also work across imports --- src/Bundle.js | 6 +-- src/ast/nodes/ExportDefaultDeclaration.js | 45 ++++--------------- src/ast/scopes/Scope.js | 6 +++ src/ast/variables/ExportDefaultVariable.js | 40 +++++++++++++++++ .../samples/mutations-in-imports/_config.js | 3 ++ .../mutations-in-imports/_expected/amd.js | 19 ++++++++ .../mutations-in-imports/_expected/cjs.js | 17 +++++++ .../mutations-in-imports/_expected/es.js | 15 +++++++ .../mutations-in-imports/_expected/iife.js | 20 +++++++++ .../mutations-in-imports/_expected/umd.js | 23 ++++++++++ .../form/samples/mutations-in-imports/lib1.js | 3 ++ .../form/samples/mutations-in-imports/lib2.js | 3 ++ .../form/samples/mutations-in-imports/lib3.js | 3 ++ .../samples/mutations-in-imports/lib3a.js | 3 ++ .../samples/mutations-in-imports/lib3b.js | 3 ++ .../samples/mutations-in-imports/lib3c.js | 3 ++ .../form/samples/mutations-in-imports/main.js | 17 +++++++ 17 files changed, 189 insertions(+), 40 deletions(-) create mode 100644 src/ast/variables/ExportDefaultVariable.js create mode 100644 test/form/samples/mutations-in-imports/_config.js create mode 100644 test/form/samples/mutations-in-imports/_expected/amd.js create mode 100644 test/form/samples/mutations-in-imports/_expected/cjs.js create mode 100644 test/form/samples/mutations-in-imports/_expected/es.js create mode 100644 test/form/samples/mutations-in-imports/_expected/iife.js create mode 100644 test/form/samples/mutations-in-imports/_expected/umd.js create mode 100644 test/form/samples/mutations-in-imports/lib1.js create mode 100644 test/form/samples/mutations-in-imports/lib2.js create mode 100644 test/form/samples/mutations-in-imports/lib3.js create mode 100644 test/form/samples/mutations-in-imports/lib3a.js create mode 100644 test/form/samples/mutations-in-imports/lib3b.js create mode 100644 test/form/samples/mutations-in-imports/lib3c.js create mode 100644 test/form/samples/mutations-in-imports/main.js diff --git a/src/Bundle.js b/src/Bundle.js index 03f04623747..f08f94052db 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -261,11 +261,9 @@ export default class Bundle { this.modules.forEach( module => { forOwn( module.scope.variables, variable => { - if ( variable.isDefault && variable.declaration.id ) { - return; + if ( !variable.isDefault || !variable.hasId ) { + variable.name = getSafeName( variable.name ); } - - variable.name = getSafeName( variable.name ); } ); // deconflict reified namespaces diff --git a/src/ast/nodes/ExportDefaultDeclaration.js b/src/ast/nodes/ExportDefaultDeclaration.js index 26e16d5aa0d..e253b84421c 100644 --- a/src/ast/nodes/ExportDefaultDeclaration.js +++ b/src/ast/nodes/ExportDefaultDeclaration.js @@ -4,45 +4,20 @@ import ExecutionPathOptions from '../ExecutionPathOptions'; const functionOrClassDeclaration = /^(?:Function|Class)Declaration/; export default class ExportDefaultDeclaration extends Node { - addCallAtPath ( path, options ) { - this.declaration.bindCallAtPath( path, options ); - } - - addReference ( reference ) { - this.name = reference.name; - if ( this.original ) this.original.addReference( reference ); - } - - assignExpressionAtPath ( path, expression ) { - if ( this.original ) this.original.assignExpressionAtPath( path, expression ); - } - bind () { - const name = ( this.declaration.id && this.declaration.id.name ) || this.declaration.name; - if ( name ) this.original = this.scope.findVariable( name ); - - this.declaration.bind(); - } - - getName ( es ) { - if ( this.original && !this.original.isReassigned ) { - return this.original.getName( es ); + if ( this._declarationName ) { + this.variable.setOriginalVariable( this.scope.findVariable( this._declarationName ) ); } - - return this.name; + this.declaration.bind(); } hasEffectsWhenCalledAtPath ( path, options ) { return this.declaration.hasEffectsWhenCalledAtPath( path, options ); } - includeVariable () { - if ( this.included ) { - return false; - } + includeDefaultExport () { this.included = true; this.declaration.includeInBundle(); - return true; } includeInBundle () { @@ -54,16 +29,14 @@ export default class ExportDefaultDeclaration extends Node { initialiseNode () { this.isExportDeclaration = true; - this.isDefault = true; - - this.name = ( this.declaration.id && this.declaration.id.name ) || this.declaration.name || this.module.basename(); - this.scope.variables.default = this; + this._declarationName = (this.declaration.id && this.declaration.id.name ) || this.declaration.name; + this.variable = this.scope.addExportDefaultDeclaration( this._declarationName || this.module.basename(), this ); } // TODO this is total chaos, tidy it up render ( code, es ) { const treeshake = this.module.bundle.treeshake; - const name = this.getName( es ); + const name = this.variable.getName( es ); // paren workaround: find first non-whitespace character position after `export default` let declaration_start; @@ -78,13 +51,13 @@ export default class ExportDefaultDeclaration extends Node { if ( this.declaration.id ) { code.remove( this.start, declaration_start ); } else { - code.overwrite( this.start, declaration_start, `var ${this.name} = ` ); + code.overwrite( this.start, declaration_start, `var ${this.variable.name} = ` ); if ( code.original[ this.end - 1 ] !== ';' ) code.appendLeft( this.end, ';' ); } } else { - if ( this.original && this.original.getName( es ) === name ) { + if ( this.variable.getOriginalVariableName( es ) === name ) { // prevent `var foo = foo` code.remove( this.leadingCommentStart || this.start, this.next || this.end ); return; // don't render children. TODO this seems like a bit of a hack diff --git a/src/ast/scopes/Scope.js b/src/ast/scopes/Scope.js index 9d9cf53ed8a..e02196eb474 100644 --- a/src/ast/scopes/Scope.js +++ b/src/ast/scopes/Scope.js @@ -1,6 +1,7 @@ import { blank, keys } from '../../utils/object.js'; import LocalVariable from '../variables/LocalVariable'; import ParameterVariable from '../variables/ParameterVariable'; +import ExportDefaultVariable from '../variables/ExportDefaultVariable'; import { UNDEFINED_ASSIGNMENT } from '../values'; export default class Scope { @@ -33,6 +34,11 @@ export default class Scope { return this.variables[ name ]; } + addExportDefaultDeclaration ( name, exportDefaultDeclaration ) { + this.variables.default = new ExportDefaultVariable( name, exportDefaultDeclaration ); + return this.variables.default; + } + addParameterDeclaration ( identifier ) { const name = identifier.name; this.variables[ name ] = new ParameterVariable( name, identifier ); diff --git a/src/ast/variables/ExportDefaultVariable.js b/src/ast/variables/ExportDefaultVariable.js new file mode 100644 index 00000000000..cb4fe3f6627 --- /dev/null +++ b/src/ast/variables/ExportDefaultVariable.js @@ -0,0 +1,40 @@ +import LocalVariable from './LocalVariable'; + +export default class ExportDefaultVariable extends LocalVariable { + constructor ( name, exportDefaultDeclaration ) { + super( name, exportDefaultDeclaration, exportDefaultDeclaration.declaration ); + this.isDefault = true; + this.hasId = !!exportDefaultDeclaration.declaration.id; + } + + addReference ( reference ) { + this.name = reference.name; + if ( this._original ) { + this._original.addReference( reference ); + } + } + + getName ( es ) { + if ( this._original && !this._original.isReassigned ) { + return this._original.getName( es ); + } + return this.name; + } + + getOriginalVariableName ( es ) { + return this._original && this._original.getName( es ); + } + + includeVariable () { + if ( this.included ) { + return false; + } + this.included = true; + this.declarations.forEach( declaration => declaration.includeDefaultExport() ); + return true; + } + + setOriginalVariable ( original ) { + this._original = original; + } +} diff --git a/test/form/samples/mutations-in-imports/_config.js b/test/form/samples/mutations-in-imports/_config.js new file mode 100644 index 00000000000..bfb50011d5e --- /dev/null +++ b/test/form/samples/mutations-in-imports/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'track mutations of imports' +}; diff --git a/test/form/samples/mutations-in-imports/_expected/amd.js b/test/form/samples/mutations-in-imports/_expected/amd.js new file mode 100644 index 00000000000..8b38018ee16 --- /dev/null +++ b/test/form/samples/mutations-in-imports/_expected/amd.js @@ -0,0 +1,19 @@ +define(function () { 'use strict'; + + const x = { a: { b: () => {} } }; + const y = { a: x.a }; + + const x$1 = { a: { b: () => {} } }; + const y$1 = { a: x$1.a }; + + const x$2 = { a: { b: () => {} } }; + const y$2 = { a: x$2.a }; + + y.a.b = () => console.log( 'effect' ); + x.a.b(); + y$1.a.b = () => console.log( 'effect' ); + x$1.a.b(); + y$2.a.b = () => console.log( 'effect' ); + x$2.a.b(); + +}); diff --git a/test/form/samples/mutations-in-imports/_expected/cjs.js b/test/form/samples/mutations-in-imports/_expected/cjs.js new file mode 100644 index 00000000000..910139e5e36 --- /dev/null +++ b/test/form/samples/mutations-in-imports/_expected/cjs.js @@ -0,0 +1,17 @@ +'use strict'; + +const x = { a: { b: () => {} } }; +const y = { a: x.a }; + +const x$1 = { a: { b: () => {} } }; +const y$1 = { a: x$1.a }; + +const x$2 = { a: { b: () => {} } }; +const y$2 = { a: x$2.a }; + +y.a.b = () => console.log( 'effect' ); +x.a.b(); +y$1.a.b = () => console.log( 'effect' ); +x$1.a.b(); +y$2.a.b = () => console.log( 'effect' ); +x$2.a.b(); diff --git a/test/form/samples/mutations-in-imports/_expected/es.js b/test/form/samples/mutations-in-imports/_expected/es.js new file mode 100644 index 00000000000..c9ada965a1e --- /dev/null +++ b/test/form/samples/mutations-in-imports/_expected/es.js @@ -0,0 +1,15 @@ +const x = { a: { b: () => {} } }; +const y = { a: x.a }; + +const x$1 = { a: { b: () => {} } }; +const y$1 = { a: x$1.a }; + +const x$2 = { a: { b: () => {} } }; +const y$2 = { a: x$2.a }; + +y.a.b = () => console.log( 'effect' ); +x.a.b(); +y$1.a.b = () => console.log( 'effect' ); +x$1.a.b(); +y$2.a.b = () => console.log( 'effect' ); +x$2.a.b(); diff --git a/test/form/samples/mutations-in-imports/_expected/iife.js b/test/form/samples/mutations-in-imports/_expected/iife.js new file mode 100644 index 00000000000..df58fe73794 --- /dev/null +++ b/test/form/samples/mutations-in-imports/_expected/iife.js @@ -0,0 +1,20 @@ +(function () { + 'use strict'; + + const x = { a: { b: () => {} } }; + const y = { a: x.a }; + + const x$1 = { a: { b: () => {} } }; + const y$1 = { a: x$1.a }; + + const x$2 = { a: { b: () => {} } }; + const y$2 = { a: x$2.a }; + + y.a.b = () => console.log( 'effect' ); + x.a.b(); + y$1.a.b = () => console.log( 'effect' ); + x$1.a.b(); + y$2.a.b = () => console.log( 'effect' ); + x$2.a.b(); + +}()); diff --git a/test/form/samples/mutations-in-imports/_expected/umd.js b/test/form/samples/mutations-in-imports/_expected/umd.js new file mode 100644 index 00000000000..9b3b2b84418 --- /dev/null +++ b/test/form/samples/mutations-in-imports/_expected/umd.js @@ -0,0 +1,23 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + const x = { a: { b: () => {} } }; + const y = { a: x.a }; + + const x$1 = { a: { b: () => {} } }; + const y$1 = { a: x$1.a }; + + const x$2 = { a: { b: () => {} } }; + const y$2 = { a: x$2.a }; + + y.a.b = () => console.log( 'effect' ); + x.a.b(); + y$1.a.b = () => console.log( 'effect' ); + x$1.a.b(); + y$2.a.b = () => console.log( 'effect' ); + x$2.a.b(); + +}))); diff --git a/test/form/samples/mutations-in-imports/lib1.js b/test/form/samples/mutations-in-imports/lib1.js new file mode 100644 index 00000000000..2c2ef1da81c --- /dev/null +++ b/test/form/samples/mutations-in-imports/lib1.js @@ -0,0 +1,3 @@ +export const x = { a: { b: () => {} } }; +export const y = { a: x.a }; +export const z = { a: { b: () => {} } }; diff --git a/test/form/samples/mutations-in-imports/lib2.js b/test/form/samples/mutations-in-imports/lib2.js new file mode 100644 index 00000000000..2c2ef1da81c --- /dev/null +++ b/test/form/samples/mutations-in-imports/lib2.js @@ -0,0 +1,3 @@ +export const x = { a: { b: () => {} } }; +export const y = { a: x.a }; +export const z = { a: { b: () => {} } }; diff --git a/test/form/samples/mutations-in-imports/lib3.js b/test/form/samples/mutations-in-imports/lib3.js new file mode 100644 index 00000000000..2c2ef1da81c --- /dev/null +++ b/test/form/samples/mutations-in-imports/lib3.js @@ -0,0 +1,3 @@ +export const x = { a: { b: () => {} } }; +export const y = { a: x.a }; +export const z = { a: { b: () => {} } }; diff --git a/test/form/samples/mutations-in-imports/lib3a.js b/test/form/samples/mutations-in-imports/lib3a.js new file mode 100644 index 00000000000..5b8983a08d5 --- /dev/null +++ b/test/form/samples/mutations-in-imports/lib3a.js @@ -0,0 +1,3 @@ +import { x } from './lib3'; + +export default x; diff --git a/test/form/samples/mutations-in-imports/lib3b.js b/test/form/samples/mutations-in-imports/lib3b.js new file mode 100644 index 00000000000..ba0e71922ba --- /dev/null +++ b/test/form/samples/mutations-in-imports/lib3b.js @@ -0,0 +1,3 @@ +import { y } from './lib3'; + +export default y; diff --git a/test/form/samples/mutations-in-imports/lib3c.js b/test/form/samples/mutations-in-imports/lib3c.js new file mode 100644 index 00000000000..323a33a87df --- /dev/null +++ b/test/form/samples/mutations-in-imports/lib3c.js @@ -0,0 +1,3 @@ +import { z } from './lib3'; + +export default z; diff --git a/test/form/samples/mutations-in-imports/main.js b/test/form/samples/mutations-in-imports/main.js new file mode 100644 index 00000000000..529a3e4a9de --- /dev/null +++ b/test/form/samples/mutations-in-imports/main.js @@ -0,0 +1,17 @@ +import * as lib1 from './lib1'; +import { x, y, z } from './lib2'; +import x3 from './lib3a'; +import y3 from './lib3b'; +import z3 from './lib3c'; + +lib1.y.a.b = () => console.log( 'effect' ); +lib1.x.a.b(); +lib1.z.a.b = () => {}; + +y.a.b = () => console.log( 'effect' ); +x.a.b(); +z.a.b = () => {}; + +y3.a.b = () => console.log( 'effect' ); +x3.a.b(); +z3.a.b = () => {}; From 1901077bbe06e8cee6474e1037ee06161bf121ac Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Mon, 25 Sep 2017 08:46:12 +0200 Subject: [PATCH 18/76] * Handle property overwrites in object literals --- src/ast/nodes/ObjectExpression.js | 50 +++++++++---- .../_config.js | 3 + .../_expected/amd.js | 37 ++++++++++ .../_expected/cjs.js | 35 +++++++++ .../_expected/es.js | 33 +++++++++ .../_expected/iife.js | 38 ++++++++++ .../_expected/umd.js | 41 +++++++++++ .../main.js | 71 +++++++++++++++++++ 8 files changed, 293 insertions(+), 15 deletions(-) create mode 100644 test/form/samples/object-literal-property-overwrites/_config.js create mode 100644 test/form/samples/object-literal-property-overwrites/_expected/amd.js create mode 100644 test/form/samples/object-literal-property-overwrites/_expected/cjs.js create mode 100644 test/form/samples/object-literal-property-overwrites/_expected/es.js create mode 100644 test/form/samples/object-literal-property-overwrites/_expected/iife.js create mode 100644 test/form/samples/object-literal-property-overwrites/_expected/umd.js create mode 100644 test/form/samples/object-literal-property-overwrites/main.js diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index ff2ec78260a..82f99c02e44 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -5,44 +5,64 @@ export default class ObjectExpression extends Node { if ( path.length === 0 ) { return; } - const accessedProperty = this.properties.find( property => !property.computed && property.key.name === path[ 0 ] ); - if ( accessedProperty ) { - accessedProperty.bindAssignmentAtPath( path.slice( 1 ), expression ); - } + this._getPossiblePropertiesWithName( path[ 0 ] ).properties.forEach( property => + property.bindAssignmentAtPath( path.slice( 1 ), expression ) ); } bindCallAtPath ( path, callOptions ) { if ( path.length === 0 ) { return; } - const accessedProperty = this.properties.find( property => !property.computed && property.key.name === path[ 0 ] ); + this._getPossiblePropertiesWithName( path[ 0 ] ).properties.forEach( property => + property.bindCallAtPath( path.slice( 1 ), callOptions ) ); + } - if ( accessedProperty ) { - accessedProperty.bindCallAtPath( path.slice( 1 ), callOptions ); + _getPossiblePropertiesWithName ( name ) { + const properties = []; + let hasComputed = false; + + for ( let index = this.properties.length - 1; index >= 0; index-- ) { + const property = this.properties[ index ]; + if ( property.computed ) { + if ( !hasComputed ) { + properties.push( property ); + hasComputed = true; + } + } else if ( property.key.name === name ) { + properties.push( property ); + break; + } } + return { properties, onlyComputed: properties.length === 0 || (hasComputed && properties.length === 1) }; } hasEffectsWhenAssignedAtPath ( path, options ) { if ( path.length <= 1 ) { return false; } - const accessedProperty = this.properties.find( property => !property.computed && property.key.name === path[ 0 ] ); + const { properties, onlyComputed } = this._getPossiblePropertiesWithName( path[ 0 ] ); - return !accessedProperty - || accessedProperty.hasEffectsWhenAssignedAtPath( path.slice( 1 ), options ); + return onlyComputed || properties.some( property => + property.hasEffectsWhenAssignedAtPath( path.slice( 1 ), options ) ); } hasEffectsWhenCalledAtPath ( path, options ) { if ( path.length === 0 ) { return true; } - const accessedProperty = this.properties.find( property => !property.computed && property.key.name === path[ 0 ] ); + const { properties, onlyComputed } = this._getPossiblePropertiesWithName( path[ 0 ] ); - return !accessedProperty - || accessedProperty.hasEffectsWhenCalledAtPath( path.slice( 1 ), options ); + return onlyComputed || properties.some( property => + property.hasEffectsWhenCalledAtPath( path.slice( 1 ), options ) ); } - hasEffectsWhenMutatedAtPath ( path ) { - return path.length > 1; + hasEffectsWhenMutatedAtPath ( path, options ) { + if ( path.length === 0 ) { + return false; + } + const { properties, onlyComputed } = this._getPossiblePropertiesWithName( path[ 0 ] ); + + return onlyComputed || properties.some( property => + property.hasEffectsWhenMutatedAtPath( path.slice( 1 ), options ) ); } } diff --git a/test/form/samples/object-literal-property-overwrites/_config.js b/test/form/samples/object-literal-property-overwrites/_config.js new file mode 100644 index 00000000000..4c75daffbfe --- /dev/null +++ b/test/form/samples/object-literal-property-overwrites/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'detect side-effects in overwritten properties of object literals' +}; diff --git a/test/form/samples/object-literal-property-overwrites/_expected/amd.js b/test/form/samples/object-literal-property-overwrites/_expected/amd.js new file mode 100644 index 00000000000..19a86a212b2 --- /dev/null +++ b/test/form/samples/object-literal-property-overwrites/_expected/amd.js @@ -0,0 +1,37 @@ +define(function () { 'use strict'; + + const retained1 = { + foo: () => {}, + foo: () => console.log( 'effect' ) + }; + retained1.foo(); + + const retained2 = { + foo: () => {}, + ['f' + 'oo']: () => console.log( 'effect' ) + }; + retained2.foo(); + + const retained3 = { + ['f' + 'oo']: () => {} + }; + retained3.bar(); + + const retained4 = { + foo: () => {}, + foo: globalVar + }; + retained4.foo.bar = 1; + + const retained5 = { + foo: () => {}, + ['f' + 'oo']: globalVar + }; + retained5.foo.bar = 1; + + const retained6 = { + ['f' + 'oo']: () => {} + }; + retained6.bar.baz = 1; + +}); diff --git a/test/form/samples/object-literal-property-overwrites/_expected/cjs.js b/test/form/samples/object-literal-property-overwrites/_expected/cjs.js new file mode 100644 index 00000000000..d12c048c469 --- /dev/null +++ b/test/form/samples/object-literal-property-overwrites/_expected/cjs.js @@ -0,0 +1,35 @@ +'use strict'; + +const retained1 = { + foo: () => {}, + foo: () => console.log( 'effect' ) +}; +retained1.foo(); + +const retained2 = { + foo: () => {}, + ['f' + 'oo']: () => console.log( 'effect' ) +}; +retained2.foo(); + +const retained3 = { + ['f' + 'oo']: () => {} +}; +retained3.bar(); + +const retained4 = { + foo: () => {}, + foo: globalVar +}; +retained4.foo.bar = 1; + +const retained5 = { + foo: () => {}, + ['f' + 'oo']: globalVar +}; +retained5.foo.bar = 1; + +const retained6 = { + ['f' + 'oo']: () => {} +}; +retained6.bar.baz = 1; diff --git a/test/form/samples/object-literal-property-overwrites/_expected/es.js b/test/form/samples/object-literal-property-overwrites/_expected/es.js new file mode 100644 index 00000000000..dd27ce3f648 --- /dev/null +++ b/test/form/samples/object-literal-property-overwrites/_expected/es.js @@ -0,0 +1,33 @@ +const retained1 = { + foo: () => {}, + foo: () => console.log( 'effect' ) +}; +retained1.foo(); + +const retained2 = { + foo: () => {}, + ['f' + 'oo']: () => console.log( 'effect' ) +}; +retained2.foo(); + +const retained3 = { + ['f' + 'oo']: () => {} +}; +retained3.bar(); + +const retained4 = { + foo: () => {}, + foo: globalVar +}; +retained4.foo.bar = 1; + +const retained5 = { + foo: () => {}, + ['f' + 'oo']: globalVar +}; +retained5.foo.bar = 1; + +const retained6 = { + ['f' + 'oo']: () => {} +}; +retained6.bar.baz = 1; diff --git a/test/form/samples/object-literal-property-overwrites/_expected/iife.js b/test/form/samples/object-literal-property-overwrites/_expected/iife.js new file mode 100644 index 00000000000..1e3a9bfcab2 --- /dev/null +++ b/test/form/samples/object-literal-property-overwrites/_expected/iife.js @@ -0,0 +1,38 @@ +(function () { + 'use strict'; + + const retained1 = { + foo: () => {}, + foo: () => console.log( 'effect' ) + }; + retained1.foo(); + + const retained2 = { + foo: () => {}, + ['f' + 'oo']: () => console.log( 'effect' ) + }; + retained2.foo(); + + const retained3 = { + ['f' + 'oo']: () => {} + }; + retained3.bar(); + + const retained4 = { + foo: () => {}, + foo: globalVar + }; + retained4.foo.bar = 1; + + const retained5 = { + foo: () => {}, + ['f' + 'oo']: globalVar + }; + retained5.foo.bar = 1; + + const retained6 = { + ['f' + 'oo']: () => {} + }; + retained6.bar.baz = 1; + +}()); diff --git a/test/form/samples/object-literal-property-overwrites/_expected/umd.js b/test/form/samples/object-literal-property-overwrites/_expected/umd.js new file mode 100644 index 00000000000..25631124783 --- /dev/null +++ b/test/form/samples/object-literal-property-overwrites/_expected/umd.js @@ -0,0 +1,41 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + const retained1 = { + foo: () => {}, + foo: () => console.log( 'effect' ) + }; + retained1.foo(); + + const retained2 = { + foo: () => {}, + ['f' + 'oo']: () => console.log( 'effect' ) + }; + retained2.foo(); + + const retained3 = { + ['f' + 'oo']: () => {} + }; + retained3.bar(); + + const retained4 = { + foo: () => {}, + foo: globalVar + }; + retained4.foo.bar = 1; + + const retained5 = { + foo: () => {}, + ['f' + 'oo']: globalVar + }; + retained5.foo.bar = 1; + + const retained6 = { + ['f' + 'oo']: () => {} + }; + retained6.bar.baz = 1; + +}))); diff --git a/test/form/samples/object-literal-property-overwrites/main.js b/test/form/samples/object-literal-property-overwrites/main.js new file mode 100644 index 00000000000..ee61f96ab8e --- /dev/null +++ b/test/form/samples/object-literal-property-overwrites/main.js @@ -0,0 +1,71 @@ +const removed1 = { + foo: () => {}, + foo: () => {}, + ['f' + 'oo']: () => {} +}; +removed1.foo(); + +const removed2 = { + foo: () => console.log( 'effect' ), + foo: () => {} +}; +removed2.foo(); + +const removed3 = { + ['f' + 'oo']: () => console.log( 'effect' ), + foo: () => {} +}; +removed3.foo(); + +const removed4 = { + foo: () => {}, + foo: () => {}, + ['f' + 'oo']: () => {} +}; +removed4.foo.bar = 1; + +const removed5 = { + foo: globalVar, + foo: () => {} +}; +removed5.foo.bar = 1; + +const removed6 = { + ['f' + 'oo']: globalVar, + foo: () => {} +}; +removed6.foo.bar = 1; + +const retained1 = { + foo: () => {}, + foo: () => console.log( 'effect' ) +}; +retained1.foo(); + +const retained2 = { + foo: () => {}, + ['f' + 'oo']: () => console.log( 'effect' ) +}; +retained2.foo(); + +const retained3 = { + ['f' + 'oo']: () => {} +}; +retained3.bar(); + +const retained4 = { + foo: () => {}, + foo: globalVar +}; +retained4.foo.bar = 1; + +const retained5 = { + foo: () => {}, + ['f' + 'oo']: globalVar +}; +retained5.foo.bar = 1; + +const retained6 = { + ['f' + 'oo']: () => {} +}; +retained6.bar.baz = 1; From 24875e31a8a4e7b399c5fd30595029fa7bf508f7 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Mon, 25 Sep 2017 09:13:39 +0200 Subject: [PATCH 19/76] Properly handle multiple computed properties --- src/ast/nodes/ObjectExpression.js | 22 +++++++++---------- .../_expected/amd.js | 14 +++++++----- .../_expected/cjs.js | 14 +++++++----- .../_expected/es.js | 14 +++++++----- .../_expected/iife.js | 14 +++++++----- .../_expected/umd.js | 14 +++++++----- .../main.js | 16 +++++++++----- 7 files changed, 66 insertions(+), 42 deletions(-) diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index 82f99c02e44..5bdeaea1db9 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -19,30 +19,28 @@ export default class ObjectExpression extends Node { _getPossiblePropertiesWithName ( name ) { const properties = []; - let hasComputed = false; + let hasCertainHit = false; for ( let index = this.properties.length - 1; index >= 0; index-- ) { const property = this.properties[ index ]; if ( property.computed ) { - if ( !hasComputed ) { - properties.push( property ); - hasComputed = true; - } + properties.push( property ); } else if ( property.key.name === name ) { properties.push( property ); + hasCertainHit = true; break; } } - return { properties, onlyComputed: properties.length === 0 || (hasComputed && properties.length === 1) }; + return { properties, hasCertainHit }; } hasEffectsWhenAssignedAtPath ( path, options ) { if ( path.length <= 1 ) { return false; } - const { properties, onlyComputed } = this._getPossiblePropertiesWithName( path[ 0 ] ); + const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ] ); - return onlyComputed || properties.some( property => + return !hasCertainHit || properties.some( property => property.hasEffectsWhenAssignedAtPath( path.slice( 1 ), options ) ); } @@ -50,9 +48,9 @@ export default class ObjectExpression extends Node { if ( path.length === 0 ) { return true; } - const { properties, onlyComputed } = this._getPossiblePropertiesWithName( path[ 0 ] ); + const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ] ); - return onlyComputed || properties.some( property => + return !hasCertainHit || properties.some( property => property.hasEffectsWhenCalledAtPath( path.slice( 1 ), options ) ); } @@ -60,9 +58,9 @@ export default class ObjectExpression extends Node { if ( path.length === 0 ) { return false; } - const { properties, onlyComputed } = this._getPossiblePropertiesWithName( path[ 0 ] ); + const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ] ); - return onlyComputed || properties.some( property => + return !hasCertainHit || properties.some( property => property.hasEffectsWhenMutatedAtPath( path.slice( 1 ), options ) ); } } diff --git a/test/form/samples/object-literal-property-overwrites/_expected/amd.js b/test/form/samples/object-literal-property-overwrites/_expected/amd.js index 19a86a212b2..8af158736af 100644 --- a/test/form/samples/object-literal-property-overwrites/_expected/amd.js +++ b/test/form/samples/object-literal-property-overwrites/_expected/amd.js @@ -8,29 +8,33 @@ define(function () { 'use strict'; const retained2 = { foo: () => {}, - ['f' + 'oo']: () => console.log( 'effect' ) + ['f' + 'oo']: () => console.log( 'effect' ), + ['b' + 'ar']: () => {} }; retained2.foo(); const retained3 = { + ['fo' + 'o']: () => {}, ['f' + 'oo']: () => {} }; retained3.bar(); const retained4 = { - foo: () => {}, + foo: {}, foo: globalVar }; retained4.foo.bar = 1; const retained5 = { - foo: () => {}, - ['f' + 'oo']: globalVar + foo: {}, + ['f' + 'oo']: globalVar, + ['b' + 'ar']: {}, }; retained5.foo.bar = 1; const retained6 = { - ['f' + 'oo']: () => {} + ['fo' + 'o']: {}, + ['f' + 'oo']: {} }; retained6.bar.baz = 1; diff --git a/test/form/samples/object-literal-property-overwrites/_expected/cjs.js b/test/form/samples/object-literal-property-overwrites/_expected/cjs.js index d12c048c469..91f37773f9a 100644 --- a/test/form/samples/object-literal-property-overwrites/_expected/cjs.js +++ b/test/form/samples/object-literal-property-overwrites/_expected/cjs.js @@ -8,28 +8,32 @@ retained1.foo(); const retained2 = { foo: () => {}, - ['f' + 'oo']: () => console.log( 'effect' ) + ['f' + 'oo']: () => console.log( 'effect' ), + ['b' + 'ar']: () => {} }; retained2.foo(); const retained3 = { + ['fo' + 'o']: () => {}, ['f' + 'oo']: () => {} }; retained3.bar(); const retained4 = { - foo: () => {}, + foo: {}, foo: globalVar }; retained4.foo.bar = 1; const retained5 = { - foo: () => {}, - ['f' + 'oo']: globalVar + foo: {}, + ['f' + 'oo']: globalVar, + ['b' + 'ar']: {}, }; retained5.foo.bar = 1; const retained6 = { - ['f' + 'oo']: () => {} + ['fo' + 'o']: {}, + ['f' + 'oo']: {} }; retained6.bar.baz = 1; diff --git a/test/form/samples/object-literal-property-overwrites/_expected/es.js b/test/form/samples/object-literal-property-overwrites/_expected/es.js index dd27ce3f648..a987bb48b75 100644 --- a/test/form/samples/object-literal-property-overwrites/_expected/es.js +++ b/test/form/samples/object-literal-property-overwrites/_expected/es.js @@ -6,28 +6,32 @@ retained1.foo(); const retained2 = { foo: () => {}, - ['f' + 'oo']: () => console.log( 'effect' ) + ['f' + 'oo']: () => console.log( 'effect' ), + ['b' + 'ar']: () => {} }; retained2.foo(); const retained3 = { + ['fo' + 'o']: () => {}, ['f' + 'oo']: () => {} }; retained3.bar(); const retained4 = { - foo: () => {}, + foo: {}, foo: globalVar }; retained4.foo.bar = 1; const retained5 = { - foo: () => {}, - ['f' + 'oo']: globalVar + foo: {}, + ['f' + 'oo']: globalVar, + ['b' + 'ar']: {}, }; retained5.foo.bar = 1; const retained6 = { - ['f' + 'oo']: () => {} + ['fo' + 'o']: {}, + ['f' + 'oo']: {} }; retained6.bar.baz = 1; diff --git a/test/form/samples/object-literal-property-overwrites/_expected/iife.js b/test/form/samples/object-literal-property-overwrites/_expected/iife.js index 1e3a9bfcab2..498d56a5a81 100644 --- a/test/form/samples/object-literal-property-overwrites/_expected/iife.js +++ b/test/form/samples/object-literal-property-overwrites/_expected/iife.js @@ -9,29 +9,33 @@ const retained2 = { foo: () => {}, - ['f' + 'oo']: () => console.log( 'effect' ) + ['f' + 'oo']: () => console.log( 'effect' ), + ['b' + 'ar']: () => {} }; retained2.foo(); const retained3 = { + ['fo' + 'o']: () => {}, ['f' + 'oo']: () => {} }; retained3.bar(); const retained4 = { - foo: () => {}, + foo: {}, foo: globalVar }; retained4.foo.bar = 1; const retained5 = { - foo: () => {}, - ['f' + 'oo']: globalVar + foo: {}, + ['f' + 'oo']: globalVar, + ['b' + 'ar']: {}, }; retained5.foo.bar = 1; const retained6 = { - ['f' + 'oo']: () => {} + ['fo' + 'o']: {}, + ['f' + 'oo']: {} }; retained6.bar.baz = 1; diff --git a/test/form/samples/object-literal-property-overwrites/_expected/umd.js b/test/form/samples/object-literal-property-overwrites/_expected/umd.js index 25631124783..dcde7075e86 100644 --- a/test/form/samples/object-literal-property-overwrites/_expected/umd.js +++ b/test/form/samples/object-literal-property-overwrites/_expected/umd.js @@ -12,29 +12,33 @@ const retained2 = { foo: () => {}, - ['f' + 'oo']: () => console.log( 'effect' ) + ['f' + 'oo']: () => console.log( 'effect' ), + ['b' + 'ar']: () => {} }; retained2.foo(); const retained3 = { + ['fo' + 'o']: () => {}, ['f' + 'oo']: () => {} }; retained3.bar(); const retained4 = { - foo: () => {}, + foo: {}, foo: globalVar }; retained4.foo.bar = 1; const retained5 = { - foo: () => {}, - ['f' + 'oo']: globalVar + foo: {}, + ['f' + 'oo']: globalVar, + ['b' + 'ar']: {}, }; retained5.foo.bar = 1; const retained6 = { - ['f' + 'oo']: () => {} + ['fo' + 'o']: {}, + ['f' + 'oo']: {} }; retained6.bar.baz = 1; diff --git a/test/form/samples/object-literal-property-overwrites/main.js b/test/form/samples/object-literal-property-overwrites/main.js index ee61f96ab8e..40c8db0e59a 100644 --- a/test/form/samples/object-literal-property-overwrites/main.js +++ b/test/form/samples/object-literal-property-overwrites/main.js @@ -12,6 +12,7 @@ const removed2 = { removed2.foo(); const removed3 = { + ['fo' + 'o']: () => console.log( 'effect' ), ['f' + 'oo']: () => console.log( 'effect' ), foo: () => {} }; @@ -32,6 +33,7 @@ removed5.foo.bar = 1; const removed6 = { ['f' + 'oo']: globalVar, + ['fo' + 'o']: globalVar, foo: () => {} }; removed6.foo.bar = 1; @@ -44,28 +46,32 @@ retained1.foo(); const retained2 = { foo: () => {}, - ['f' + 'oo']: () => console.log( 'effect' ) + ['f' + 'oo']: () => console.log( 'effect' ), + ['b' + 'ar']: () => {} }; retained2.foo(); const retained3 = { + ['fo' + 'o']: () => {}, ['f' + 'oo']: () => {} }; retained3.bar(); const retained4 = { - foo: () => {}, + foo: {}, foo: globalVar }; retained4.foo.bar = 1; const retained5 = { - foo: () => {}, - ['f' + 'oo']: globalVar + foo: {}, + ['f' + 'oo']: globalVar, + ['b' + 'ar']: {}, }; retained5.foo.bar = 1; const retained6 = { - ['f' + 'oo']: () => {} + ['fo' + 'o']: {}, + ['f' + 'oo']: {} }; retained6.bar.baz = 1; From cab0bf0b694187b52b3f9d80810e2c6e7aa6c4de Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Tue, 26 Sep 2017 07:46:39 +0200 Subject: [PATCH 20/76] Properly bind calls and assignments involving computed properties --- src/ast/nodes/MemberExpression.js | 18 +++++----- src/ast/nodes/ObjectExpression.js | 4 +++ src/ast/nodes/Property.js | 4 +++ src/ast/variables/DeepSet.js | 35 ++++++++++++++++--- src/ast/variables/LocalVariable.js | 5 ++- .../_config.js | 3 ++ .../_expected/amd.js | 20 +++++++++++ .../_expected/cjs.js | 18 ++++++++++ .../_expected/es.js | 16 +++++++++ .../_expected/iife.js | 21 +++++++++++ .../_expected/umd.js | 24 +++++++++++++ .../main.js | 23 ++++++++++++ .../_expected/amd.js | 4 +-- .../_expected/cjs.js | 4 +-- .../_expected/es.js | 4 +-- .../_expected/iife.js | 4 +-- .../_expected/umd.js | 4 +-- .../main.js | 6 ++-- 18 files changed, 189 insertions(+), 28 deletions(-) create mode 100644 test/form/samples/computed-member-expression-assignments/_config.js create mode 100644 test/form/samples/computed-member-expression-assignments/_expected/amd.js create mode 100644 test/form/samples/computed-member-expression-assignments/_expected/cjs.js create mode 100644 test/form/samples/computed-member-expression-assignments/_expected/es.js create mode 100644 test/form/samples/computed-member-expression-assignments/_expected/iife.js create mode 100644 test/form/samples/computed-member-expression-assignments/_expected/umd.js create mode 100644 test/form/samples/computed-member-expression-assignments/main.js diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index 97555dd88e9..db53d84a0b4 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -1,6 +1,7 @@ import relativeId from '../../utils/relativeId.js'; import Node from '../Node.js'; import isReference from 'is-reference'; +import { UNKNOWN_KEY } from '../variables/DeepSet'; const validProp = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; @@ -82,7 +83,9 @@ export default class MemberExpression extends Node { } if ( this.variable ) { this.variable.assignExpressionAtPath( path, expression ); - } else if ( !this.computed ) { + } else if ( this.computed ) { + this.object.bindAssignmentAtPath( [ UNKNOWN_KEY, ...path ], expression ); + } else { this.object.bindAssignmentAtPath( [ this.property.name, ...path ], expression ); } } @@ -93,7 +96,9 @@ export default class MemberExpression extends Node { } if ( this.variable ) { this.variable.addCallAtPath( path, callOptions ); - } else if ( !this.computed ) { + } else if ( this.computed ) { + this.object.bindCallAtPath( [ UNKNOWN_KEY, ...path ], callOptions ); + } else { this.object.bindCallAtPath( [ this.property.name, ...path ], callOptions ); } } @@ -103,7 +108,7 @@ export default class MemberExpression extends Node { return this.variable.hasEffectsWhenAssignedAtPath( path, options ); } if ( this.computed ) { - return this.object.hasEffectsWhenMutatedAtPath( [], options ); + return path.length > 0 || this.object.hasEffectsWhenMutatedAtPath( [], options ); } return this.object.hasEffectsWhenAssignedAtPath( [ this.property.name, ...path ], options ); } @@ -112,10 +117,7 @@ export default class MemberExpression extends Node { if ( this.variable ) { return this.variable.hasEffectsWhenCalledAtPath( path, options ); } - if ( !isReference( this ) ) { - return true; - } - if ( this.computed ) { + if ( !isReference( this ) || this.computed ) { return true; } return this.object.hasEffectsWhenCalledAtPath( [ this.property.name, ...path ], options ); @@ -126,7 +128,7 @@ export default class MemberExpression extends Node { return this.variable.hasEffectsWhenMutatedAtPath( path, options ); } if ( this.computed ) { - return this.object.hasEffectsWhenMutatedAtPath( [], options ); + return path.length > 0 || this.object.hasEffectsWhenMutatedAtPath( [], options ); } return this.object.hasEffectsWhenMutatedAtPath( [ this.property.name, ...path ], options ); } diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index 5bdeaea1db9..cf9fec0d85e 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -1,4 +1,5 @@ import Node from '../Node.js'; +import { UNKNOWN_KEY } from '../variables/DeepSet'; export default class ObjectExpression extends Node { bindAssignmentAtPath ( path, expression ) { @@ -18,6 +19,9 @@ export default class ObjectExpression extends Node { } _getPossiblePropertiesWithName ( name ) { + if ( name === UNKNOWN_KEY ) { + return { properties: this.properties, hasCertainHit: false }; + } const properties = []; let hasCertainHit = false; diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index cd5fa8f3f19..21b656c8199 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -18,6 +18,10 @@ export default class Property extends Node { return this.value.hasEffectsWhenCalledAtPath( path, options ); } + hasEffectsWhenMutatedAtPath ( path, options ) { + return this.value.hasEffectsWhenMutatedAtPath( path, options ); + } + initialiseAndDeclare ( parentScope, kind ) { this.initialiseScope( parentScope ); this.key.initialise( parentScope ); diff --git a/src/ast/variables/DeepSet.js b/src/ast/variables/DeepSet.js index 7ad38a48c19..fd2a918e6ed 100644 --- a/src/ast/variables/DeepSet.js +++ b/src/ast/variables/DeepSet.js @@ -1,4 +1,5 @@ const SET_KEY = {}; +export const UNKNOWN_KEY = {}; export default class DeepSet { constructor () { @@ -20,8 +21,21 @@ export default class DeepSet { forEachAtPath ( path, callback ) { const [ nextPath, ...remainingPath ] = path; this._assignments.get( SET_KEY ).forEach( assignment => callback( path, assignment ) ); - if ( path.length > 0 && this._assignments.has( nextPath ) ) { - this._assignments.get( nextPath ).forEachAtPath( remainingPath, callback ); + if ( path.length > 0 ) { + if ( nextPath === UNKNOWN_KEY ) { + this._assignments.forEach( ( assignment, subPath ) => { + if ( subPath !== SET_KEY ) { + assignment.forEachAtPath( remainingPath, callback ); + } + } ); + } else { + if ( this._assignments.has( nextPath ) ) { + this._assignments.get( nextPath ).forEachAtPath( remainingPath, callback ); + } + if ( this._assignments.has( UNKNOWN_KEY ) ) { + this._assignments.get( UNKNOWN_KEY ).forEachAtPath( remainingPath, callback ); + } + } } } @@ -42,8 +56,21 @@ export default class DeepSet { return Array.from( this._assignments.get( SET_KEY ) ).some( assignment => predicateFunction( path, assignment ) ) || ( path.length > 0 - && this._assignments.has( nextPath ) - && this._assignments.get( nextPath ).someAtPath( remainingPath, predicateFunction ) + && ( + (nextPath === UNKNOWN_KEY + && Array.from( this._assignments ).some( ( assignment, subPath ) => { + if ( subPath !== SET_KEY ) { + return assignment.someAtPath( remainingPath, predicateFunction ); + } + } )) + || (nextPath !== UNKNOWN_KEY + && ( + (this._assignments.has( nextPath ) + && this._assignments.get( nextPath ).someAtPath( remainingPath, predicateFunction )) + || (this._assignments.has( UNKNOWN_KEY ) + && this._assignments.get( UNKNOWN_KEY ).someAtPath( remainingPath, predicateFunction )) + )) + ) ); } } diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index b06e35f557b..a5b557b8d22 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -27,9 +27,8 @@ export default class LocalVariable extends Variable { if ( this.assignedExpressions.hasAtPath( path, expression ) ) return; this.assignedExpressions.addAtPath( path, expression ); if ( path.length > 0 ) { - this.assignedExpressions.forEachAtPath( path.slice( 0, -1 ), ( relativePath, node ) => { - return node.bindAssignmentAtPath( [ ...relativePath, ...path.slice( -1 ) ], expression ); - } ); + this.assignedExpressions.forEachAtPath( path.slice( 0, -1 ), ( relativePath, node ) => + node.bindAssignmentAtPath( [ ...relativePath, ...path.slice( -1 ) ], expression ) ); } this.calls.forEachAtPath( path, ( relativePath, callOptions ) => expression.bindCallAtPath( relativePath, callOptions ) ); diff --git a/test/form/samples/computed-member-expression-assignments/_config.js b/test/form/samples/computed-member-expression-assignments/_config.js new file mode 100644 index 00000000000..28124ab2672 --- /dev/null +++ b/test/form/samples/computed-member-expression-assignments/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'detect side-effects in assignments involving computed members' +}; diff --git a/test/form/samples/computed-member-expression-assignments/_expected/amd.js b/test/form/samples/computed-member-expression-assignments/_expected/amd.js new file mode 100644 index 00000000000..1a5957392ff --- /dev/null +++ b/test/form/samples/computed-member-expression-assignments/_expected/amd.js @@ -0,0 +1,20 @@ +define(function () { 'use strict'; + + const retained1 = { foo: {} }; + retained1[ 'f' + 'oo' ] = globalVar; + retained1.foo.bar = 1; + + const retained2 = {}; + retained2[ 'f' + 'oo' ].bar = 1; + + const retained3 = { foo: globalVar }; + retained3.foo[ 'b' + 'ar' ] = 1; + + const retained4 = { foo: () => {} }; + retained4[ 'f' + 'oo' ] = function () {this.x = 1;}; + retained4.foo(); + + const retained5 = { foo: function () {this.x = 1;} }; + retained5[ 'f' + 'oo' ](); + +}); diff --git a/test/form/samples/computed-member-expression-assignments/_expected/cjs.js b/test/form/samples/computed-member-expression-assignments/_expected/cjs.js new file mode 100644 index 00000000000..92523c3dd90 --- /dev/null +++ b/test/form/samples/computed-member-expression-assignments/_expected/cjs.js @@ -0,0 +1,18 @@ +'use strict'; + +const retained1 = { foo: {} }; +retained1[ 'f' + 'oo' ] = globalVar; +retained1.foo.bar = 1; + +const retained2 = {}; +retained2[ 'f' + 'oo' ].bar = 1; + +const retained3 = { foo: globalVar }; +retained3.foo[ 'b' + 'ar' ] = 1; + +const retained4 = { foo: () => {} }; +retained4[ 'f' + 'oo' ] = function () {this.x = 1;}; +retained4.foo(); + +const retained5 = { foo: function () {this.x = 1;} }; +retained5[ 'f' + 'oo' ](); diff --git a/test/form/samples/computed-member-expression-assignments/_expected/es.js b/test/form/samples/computed-member-expression-assignments/_expected/es.js new file mode 100644 index 00000000000..9d96b1ed68e --- /dev/null +++ b/test/form/samples/computed-member-expression-assignments/_expected/es.js @@ -0,0 +1,16 @@ +const retained1 = { foo: {} }; +retained1[ 'f' + 'oo' ] = globalVar; +retained1.foo.bar = 1; + +const retained2 = {}; +retained2[ 'f' + 'oo' ].bar = 1; + +const retained3 = { foo: globalVar }; +retained3.foo[ 'b' + 'ar' ] = 1; + +const retained4 = { foo: () => {} }; +retained4[ 'f' + 'oo' ] = function () {this.x = 1;}; +retained4.foo(); + +const retained5 = { foo: function () {this.x = 1;} }; +retained5[ 'f' + 'oo' ](); diff --git a/test/form/samples/computed-member-expression-assignments/_expected/iife.js b/test/form/samples/computed-member-expression-assignments/_expected/iife.js new file mode 100644 index 00000000000..c57a69a8822 --- /dev/null +++ b/test/form/samples/computed-member-expression-assignments/_expected/iife.js @@ -0,0 +1,21 @@ +(function () { + 'use strict'; + + const retained1 = { foo: {} }; + retained1[ 'f' + 'oo' ] = globalVar; + retained1.foo.bar = 1; + + const retained2 = {}; + retained2[ 'f' + 'oo' ].bar = 1; + + const retained3 = { foo: globalVar }; + retained3.foo[ 'b' + 'ar' ] = 1; + + const retained4 = { foo: () => {} }; + retained4[ 'f' + 'oo' ] = function () {this.x = 1;}; + retained4.foo(); + + const retained5 = { foo: function () {this.x = 1;} }; + retained5[ 'f' + 'oo' ](); + +}()); diff --git a/test/form/samples/computed-member-expression-assignments/_expected/umd.js b/test/form/samples/computed-member-expression-assignments/_expected/umd.js new file mode 100644 index 00000000000..1ffe0c39f81 --- /dev/null +++ b/test/form/samples/computed-member-expression-assignments/_expected/umd.js @@ -0,0 +1,24 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + const retained1 = { foo: {} }; + retained1[ 'f' + 'oo' ] = globalVar; + retained1.foo.bar = 1; + + const retained2 = {}; + retained2[ 'f' + 'oo' ].bar = 1; + + const retained3 = { foo: globalVar }; + retained3.foo[ 'b' + 'ar' ] = 1; + + const retained4 = { foo: () => {} }; + retained4[ 'f' + 'oo' ] = function () {this.x = 1;}; + retained4.foo(); + + const retained5 = { foo: function () {this.x = 1;} }; + retained5[ 'f' + 'oo' ](); + +}))); diff --git a/test/form/samples/computed-member-expression-assignments/main.js b/test/form/samples/computed-member-expression-assignments/main.js new file mode 100644 index 00000000000..220a12f7203 --- /dev/null +++ b/test/form/samples/computed-member-expression-assignments/main.js @@ -0,0 +1,23 @@ +const retained1 = { foo: {} }; +retained1[ 'f' + 'oo' ] = globalVar; +retained1.foo.bar = 1; + +const retained2 = {}; +retained2[ 'f' + 'oo' ].bar = 1; + +const retained3 = { foo: globalVar }; +retained3.foo[ 'b' + 'ar' ] = 1; + +const retained4 = { foo: () => {} }; +retained4[ 'f' + 'oo' ] = function () {this.x = 1;}; +retained4.foo(); + +const retained5 = { foo: function () {this.x = 1;} }; +retained5[ 'f' + 'oo' ](); + +const removed1 = { foo: {} }; +removed1.foo[ 'b' + 'ar' ] = 1; + +const removed2 = { foo: () => {} }; +removed2[ 'f' + 'oo' ] = function () {this.x = 1;}; +const result2 = new removed2.foo(); diff --git a/test/form/samples/object-literal-property-overwrites/_expected/amd.js b/test/form/samples/object-literal-property-overwrites/_expected/amd.js index 8af158736af..0270ef5deaa 100644 --- a/test/form/samples/object-literal-property-overwrites/_expected/amd.js +++ b/test/form/samples/object-literal-property-overwrites/_expected/amd.js @@ -2,13 +2,13 @@ define(function () { 'use strict'; const retained1 = { foo: () => {}, - foo: () => console.log( 'effect' ) + foo: function () {this.x = 1;} }; retained1.foo(); const retained2 = { foo: () => {}, - ['f' + 'oo']: () => console.log( 'effect' ), + ['f' + 'oo']: function () {this.x = 1;}, ['b' + 'ar']: () => {} }; retained2.foo(); diff --git a/test/form/samples/object-literal-property-overwrites/_expected/cjs.js b/test/form/samples/object-literal-property-overwrites/_expected/cjs.js index 91f37773f9a..6d7143c492a 100644 --- a/test/form/samples/object-literal-property-overwrites/_expected/cjs.js +++ b/test/form/samples/object-literal-property-overwrites/_expected/cjs.js @@ -2,13 +2,13 @@ const retained1 = { foo: () => {}, - foo: () => console.log( 'effect' ) + foo: function () {this.x = 1;} }; retained1.foo(); const retained2 = { foo: () => {}, - ['f' + 'oo']: () => console.log( 'effect' ), + ['f' + 'oo']: function () {this.x = 1;}, ['b' + 'ar']: () => {} }; retained2.foo(); diff --git a/test/form/samples/object-literal-property-overwrites/_expected/es.js b/test/form/samples/object-literal-property-overwrites/_expected/es.js index a987bb48b75..205a9baf9cf 100644 --- a/test/form/samples/object-literal-property-overwrites/_expected/es.js +++ b/test/form/samples/object-literal-property-overwrites/_expected/es.js @@ -1,12 +1,12 @@ const retained1 = { foo: () => {}, - foo: () => console.log( 'effect' ) + foo: function () {this.x = 1;} }; retained1.foo(); const retained2 = { foo: () => {}, - ['f' + 'oo']: () => console.log( 'effect' ), + ['f' + 'oo']: function () {this.x = 1;}, ['b' + 'ar']: () => {} }; retained2.foo(); diff --git a/test/form/samples/object-literal-property-overwrites/_expected/iife.js b/test/form/samples/object-literal-property-overwrites/_expected/iife.js index 498d56a5a81..2758c9ed398 100644 --- a/test/form/samples/object-literal-property-overwrites/_expected/iife.js +++ b/test/form/samples/object-literal-property-overwrites/_expected/iife.js @@ -3,13 +3,13 @@ const retained1 = { foo: () => {}, - foo: () => console.log( 'effect' ) + foo: function () {this.x = 1;} }; retained1.foo(); const retained2 = { foo: () => {}, - ['f' + 'oo']: () => console.log( 'effect' ), + ['f' + 'oo']: function () {this.x = 1;}, ['b' + 'ar']: () => {} }; retained2.foo(); diff --git a/test/form/samples/object-literal-property-overwrites/_expected/umd.js b/test/form/samples/object-literal-property-overwrites/_expected/umd.js index dcde7075e86..946b2f08a25 100644 --- a/test/form/samples/object-literal-property-overwrites/_expected/umd.js +++ b/test/form/samples/object-literal-property-overwrites/_expected/umd.js @@ -6,13 +6,13 @@ const retained1 = { foo: () => {}, - foo: () => console.log( 'effect' ) + foo: function () {this.x = 1;} }; retained1.foo(); const retained2 = { foo: () => {}, - ['f' + 'oo']: () => console.log( 'effect' ), + ['f' + 'oo']: function () {this.x = 1;}, ['b' + 'ar']: () => {} }; retained2.foo(); diff --git a/test/form/samples/object-literal-property-overwrites/main.js b/test/form/samples/object-literal-property-overwrites/main.js index 40c8db0e59a..fa1b06e1cc4 100644 --- a/test/form/samples/object-literal-property-overwrites/main.js +++ b/test/form/samples/object-literal-property-overwrites/main.js @@ -12,7 +12,7 @@ const removed2 = { removed2.foo(); const removed3 = { - ['fo' + 'o']: () => console.log( 'effect' ), + ['fo' + 'o']: function () {this.x = 1;}, ['f' + 'oo']: () => console.log( 'effect' ), foo: () => {} }; @@ -40,13 +40,13 @@ removed6.foo.bar = 1; const retained1 = { foo: () => {}, - foo: () => console.log( 'effect' ) + foo: function () {this.x = 1;} }; retained1.foo(); const retained2 = { foo: () => {}, - ['f' + 'oo']: () => console.log( 'effect' ), + ['f' + 'oo']: function () {this.x = 1;}, ['b' + 'ar']: () => {} }; retained2.foo(); From 835503b35ac7e2561f8e80250d684468a34e27a6 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Tue, 26 Sep 2017 21:47:53 +0200 Subject: [PATCH 21/76] Implement a hard limit for member expression path lengths as there are some pathological situations involving recursive self-assignments that the algorithm just cannot handle otherwise. --- src/ast/variables/LocalVariable.js | 18 ++++++++++++++++++ .../samples/recursive-assignments/_config.js | 3 +++ .../recursive-assignments/_expected/amd.js | 14 ++++++++++++++ .../recursive-assignments/_expected/cjs.js | 12 ++++++++++++ .../recursive-assignments/_expected/es.js | 10 ++++++++++ .../recursive-assignments/_expected/iife.js | 15 +++++++++++++++ .../recursive-assignments/_expected/umd.js | 18 ++++++++++++++++++ .../form/samples/recursive-assignments/main.js | 10 ++++++++++ 8 files changed, 100 insertions(+) create mode 100644 test/form/samples/recursive-assignments/_config.js create mode 100644 test/form/samples/recursive-assignments/_expected/amd.js create mode 100644 test/form/samples/recursive-assignments/_expected/cjs.js create mode 100644 test/form/samples/recursive-assignments/_expected/es.js create mode 100644 test/form/samples/recursive-assignments/_expected/iife.js create mode 100644 test/form/samples/recursive-assignments/_expected/umd.js create mode 100644 test/form/samples/recursive-assignments/main.js diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index a5b557b8d22..170cc2ac16f 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -1,6 +1,9 @@ import Variable from './Variable'; import DeepSet from './DeepSet'; +// To avoid recursions +const MAX_PATH_LENGTH = 8; + export default class LocalVariable extends Variable { constructor ( name, declarator, init ) { super( name ); @@ -17,6 +20,9 @@ export default class LocalVariable extends Variable { } addCallAtPath ( path, callOptions ) { + if (path.length > MAX_PATH_LENGTH) { + return; + } if ( this.calls.hasAtPath( path, callOptions ) ) return; this.calls.addAtPath( path, callOptions ); this.assignedExpressions.forEachAtPath( path, ( relativePath, node ) => @@ -24,6 +30,9 @@ export default class LocalVariable extends Variable { } assignExpressionAtPath ( path, expression ) { + if (path.length > MAX_PATH_LENGTH) { + return; + } if ( this.assignedExpressions.hasAtPath( path, expression ) ) return; this.assignedExpressions.addAtPath( path, expression ); if ( path.length > 0 ) { @@ -45,6 +54,9 @@ export default class LocalVariable extends Variable { } hasEffectsWhenAssignedAtPath ( path, options ) { + if (path.length > MAX_PATH_LENGTH) { + return true; + } return this.included || this.assignedExpressions.someAtPath( path, ( relativePath, node ) => relativePath.length > 0 @@ -53,6 +65,9 @@ export default class LocalVariable extends Variable { } hasEffectsWhenCalledAtPath ( path, options ) { + if (path.length > MAX_PATH_LENGTH) { + return true; + } return this.assignedExpressions.someAtPath( path, ( relativePath, node ) => { if ( relativePath.length === 0 ) { return !options.hasNodeBeenCalled( node ) @@ -63,6 +78,9 @@ export default class LocalVariable extends Variable { } hasEffectsWhenMutatedAtPath ( path, options ) { + if (path.length > MAX_PATH_LENGTH) { + return true; + } return this.included || this.assignedExpressions.someAtPath( path, ( relativePath, node ) => { if ( relativePath.length === 0 ) { diff --git a/test/form/samples/recursive-assignments/_config.js b/test/form/samples/recursive-assignments/_config.js new file mode 100644 index 00000000000..f14e7c21536 --- /dev/null +++ b/test/form/samples/recursive-assignments/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'do not fail for pathological recursive algorithms and circular structures' +}; diff --git a/test/form/samples/recursive-assignments/_expected/amd.js b/test/form/samples/recursive-assignments/_expected/amd.js new file mode 100644 index 00000000000..262f3db04e3 --- /dev/null +++ b/test/form/samples/recursive-assignments/_expected/amd.js @@ -0,0 +1,14 @@ +define(function () { 'use strict'; + + let foo = () => {}; + foo.value = foo; + + while ( foo.value ) { + foo = foo.value; + } + + foo(); + foo.bar = 1; + foo['baz'] = 1; + +}); diff --git a/test/form/samples/recursive-assignments/_expected/cjs.js b/test/form/samples/recursive-assignments/_expected/cjs.js new file mode 100644 index 00000000000..4ace26df3e2 --- /dev/null +++ b/test/form/samples/recursive-assignments/_expected/cjs.js @@ -0,0 +1,12 @@ +'use strict'; + +let foo = () => {}; +foo.value = foo; + +while ( foo.value ) { + foo = foo.value; +} + +foo(); +foo.bar = 1; +foo['baz'] = 1; diff --git a/test/form/samples/recursive-assignments/_expected/es.js b/test/form/samples/recursive-assignments/_expected/es.js new file mode 100644 index 00000000000..42f9044790f --- /dev/null +++ b/test/form/samples/recursive-assignments/_expected/es.js @@ -0,0 +1,10 @@ +let foo = () => {}; +foo.value = foo; + +while ( foo.value ) { + foo = foo.value; +} + +foo(); +foo.bar = 1; +foo['baz'] = 1; diff --git a/test/form/samples/recursive-assignments/_expected/iife.js b/test/form/samples/recursive-assignments/_expected/iife.js new file mode 100644 index 00000000000..611bd2db1c0 --- /dev/null +++ b/test/form/samples/recursive-assignments/_expected/iife.js @@ -0,0 +1,15 @@ +(function () { + 'use strict'; + + let foo = () => {}; + foo.value = foo; + + while ( foo.value ) { + foo = foo.value; + } + + foo(); + foo.bar = 1; + foo['baz'] = 1; + +}()); diff --git a/test/form/samples/recursive-assignments/_expected/umd.js b/test/form/samples/recursive-assignments/_expected/umd.js new file mode 100644 index 00000000000..074e4c950f1 --- /dev/null +++ b/test/form/samples/recursive-assignments/_expected/umd.js @@ -0,0 +1,18 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + let foo = () => {}; + foo.value = foo; + + while ( foo.value ) { + foo = foo.value; + } + + foo(); + foo.bar = 1; + foo['baz'] = 1; + +}))); diff --git a/test/form/samples/recursive-assignments/main.js b/test/form/samples/recursive-assignments/main.js new file mode 100644 index 00000000000..42f9044790f --- /dev/null +++ b/test/form/samples/recursive-assignments/main.js @@ -0,0 +1,10 @@ +let foo = () => {}; +foo.value = foo; + +while ( foo.value ) { + foo = foo.value; +} + +foo(); +foo.bar = 1; +foo['baz'] = 1; From 27e27196ff581aa458a389b162446b9d4819311a Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Thu, 28 Sep 2017 07:12:50 +0200 Subject: [PATCH 22/76] Fix some overlooked side-effects when dealing with update expressions --- src/ast/nodes/UpdateExpression.js | 8 ++++++-- src/ast/nodes/shared/VirtualNumberLiteral.js | 11 +++++++++++ .../update-expression-side-effects/_config.js | 3 +++ .../_expected/amd.js | 15 +++++++++++++++ .../_expected/cjs.js | 13 +++++++++++++ .../_expected/es.js | 11 +++++++++++ .../_expected/iife.js | 16 ++++++++++++++++ .../_expected/umd.js | 19 +++++++++++++++++++ .../update-expression-side-effects/main.js | 11 +++++++++++ 9 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 src/ast/nodes/shared/VirtualNumberLiteral.js create mode 100644 test/form/samples/update-expression-side-effects/_config.js create mode 100644 test/form/samples/update-expression-side-effects/_expected/amd.js create mode 100644 test/form/samples/update-expression-side-effects/_expected/cjs.js create mode 100644 test/form/samples/update-expression-side-effects/_expected/es.js create mode 100644 test/form/samples/update-expression-side-effects/_expected/iife.js create mode 100644 test/form/samples/update-expression-side-effects/_expected/umd.js create mode 100644 test/form/samples/update-expression-side-effects/main.js diff --git a/src/ast/nodes/UpdateExpression.js b/src/ast/nodes/UpdateExpression.js index 8f05dedf063..85ca2b41eea 100644 --- a/src/ast/nodes/UpdateExpression.js +++ b/src/ast/nodes/UpdateExpression.js @@ -1,18 +1,22 @@ import Node from '../Node.js'; import disallowIllegalReassignment from './shared/disallowIllegalReassignment.js'; +import VirtualNumberLiteral from './shared/VirtualNumberLiteral'; export default class UpdateExpression extends Node { bind () { + super.bind(); disallowIllegalReassignment( this.scope, this.argument ); + this.argument.bindAssignmentAtPath( [], new VirtualNumberLiteral() ); if ( this.argument.type === 'Identifier' ) { const variable = this.scope.findVariable( this.argument.name ); variable.isReassigned = true; } - super.bind(); } hasEffects ( options ) { - return this.included || this.argument.hasEffectsWhenAssignedAtPath( [], options ); + return this.included + || this.argument.hasEffects( options ) + || this.argument.hasEffectsWhenAssignedAtPath( [], options ); } hasEffectsAsExpressionStatement ( options ) { diff --git a/src/ast/nodes/shared/VirtualNumberLiteral.js b/src/ast/nodes/shared/VirtualNumberLiteral.js new file mode 100644 index 00000000000..46eda7bdb60 --- /dev/null +++ b/src/ast/nodes/shared/VirtualNumberLiteral.js @@ -0,0 +1,11 @@ +import Node from '../../Node'; + +export default class VirtualNumberLiteral extends Node { + hasEffectsWhenAssignedAtPath ( path ) { + return path.length > 1; + } + + hasEffectsWhenMutatedAtPath ( path ) { + return path.length > 0; + } +} diff --git a/test/form/samples/update-expression-side-effects/_config.js b/test/form/samples/update-expression-side-effects/_config.js new file mode 100644 index 00000000000..4dbc0f43e3f --- /dev/null +++ b/test/form/samples/update-expression-side-effects/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'detect side-effects when dealing with update expressions' +}; diff --git a/test/form/samples/update-expression-side-effects/_expected/amd.js b/test/form/samples/update-expression-side-effects/_expected/amd.js new file mode 100644 index 00000000000..5b0fca61a05 --- /dev/null +++ b/test/form/samples/update-expression-side-effects/_expected/amd.js @@ -0,0 +1,15 @@ +define(function () { 'use strict'; + + const effectY = () => { + console.log('effect'); + return 'y'; + }; + + const x = {y: 1}; + x[effectY()]++; + + let foo = {bar: {}}; + foo++; + foo.bar.baz = 1; + +}); diff --git a/test/form/samples/update-expression-side-effects/_expected/cjs.js b/test/form/samples/update-expression-side-effects/_expected/cjs.js new file mode 100644 index 00000000000..2dc41eec652 --- /dev/null +++ b/test/form/samples/update-expression-side-effects/_expected/cjs.js @@ -0,0 +1,13 @@ +'use strict'; + +const effectY = () => { + console.log('effect'); + return 'y'; +}; + +const x = {y: 1}; +x[effectY()]++; + +let foo = {bar: {}}; +foo++; +foo.bar.baz = 1; diff --git a/test/form/samples/update-expression-side-effects/_expected/es.js b/test/form/samples/update-expression-side-effects/_expected/es.js new file mode 100644 index 00000000000..39711399d7b --- /dev/null +++ b/test/form/samples/update-expression-side-effects/_expected/es.js @@ -0,0 +1,11 @@ +const effectY = () => { + console.log('effect'); + return 'y'; +}; + +const x = {y: 1}; +x[effectY()]++; + +let foo = {bar: {}}; +foo++; +foo.bar.baz = 1; diff --git a/test/form/samples/update-expression-side-effects/_expected/iife.js b/test/form/samples/update-expression-side-effects/_expected/iife.js new file mode 100644 index 00000000000..4811226e493 --- /dev/null +++ b/test/form/samples/update-expression-side-effects/_expected/iife.js @@ -0,0 +1,16 @@ +(function () { + 'use strict'; + + const effectY = () => { + console.log('effect'); + return 'y'; + }; + + const x = {y: 1}; + x[effectY()]++; + + let foo = {bar: {}}; + foo++; + foo.bar.baz = 1; + +}()); diff --git a/test/form/samples/update-expression-side-effects/_expected/umd.js b/test/form/samples/update-expression-side-effects/_expected/umd.js new file mode 100644 index 00000000000..68ba4244062 --- /dev/null +++ b/test/form/samples/update-expression-side-effects/_expected/umd.js @@ -0,0 +1,19 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + const effectY = () => { + console.log('effect'); + return 'y'; + }; + + const x = {y: 1}; + x[effectY()]++; + + let foo = {bar: {}}; + foo++; + foo.bar.baz = 1; + +}))); diff --git a/test/form/samples/update-expression-side-effects/main.js b/test/form/samples/update-expression-side-effects/main.js new file mode 100644 index 00000000000..39711399d7b --- /dev/null +++ b/test/form/samples/update-expression-side-effects/main.js @@ -0,0 +1,11 @@ +const effectY = () => { + console.log('effect'); + return 'y'; +}; + +const x = {y: 1}; +x[effectY()]++; + +let foo = {bar: {}}; +foo++; +foo.bar.baz = 1; From d5177cd1048b099bd22c8956888d6b88e564b615 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Thu, 28 Sep 2017 07:39:38 +0200 Subject: [PATCH 23/76] Use real node-like objects for virtual assignments --- src/ast/nodes/UnaryExpression.js | 5 +++-- src/ast/nodes/shared/FunctionNode.js | 4 ++-- src/ast/nodes/shared/UndefinedIdentifier.js | 15 +++++++++++++++ src/ast/scopes/ModuleScope.js | 4 ++-- src/ast/scopes/Scope.js | 4 ++-- src/ast/values.js | 18 ------------------ 6 files changed, 24 insertions(+), 26 deletions(-) create mode 100644 src/ast/nodes/shared/UndefinedIdentifier.js diff --git a/src/ast/nodes/UnaryExpression.js b/src/ast/nodes/UnaryExpression.js index a4d0ea4f924..86c9549214d 100644 --- a/src/ast/nodes/UnaryExpression.js +++ b/src/ast/nodes/UnaryExpression.js @@ -1,5 +1,6 @@ import Node from '../Node.js'; -import { UNDEFINED_ASSIGNMENT, UNKNOWN_VALUE } from '../values'; +import { UNKNOWN_VALUE } from '../values'; +import UndefinedIdentifier from './shared/UndefinedIdentifier'; const operators = { '-': value => -value, @@ -15,7 +16,7 @@ export default class UnaryExpression extends Node { bind () { if ( this.value === UNKNOWN_VALUE ) super.bind(); if ( this.operator === 'delete' ) { - this.argument.bindAssignmentAtPath( [], UNDEFINED_ASSIGNMENT ); + this.argument.bindAssignmentAtPath( [], new UndefinedIdentifier() ); } } diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index e09d35f8438..c6a5f06a4bb 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -1,6 +1,6 @@ import Node from '../../Node.js'; import FunctionScope from '../../scopes/FunctionScope'; -import { UNKNOWN_ASSIGNMENT, UNKNOWN_OBJECT_LITERAL } from '../../values'; +import { UNKNOWN_ASSIGNMENT } from '../../values'; import VirtualObjectExpression from './VirtualObjectExpression'; export default class FunctionNode extends Node { @@ -9,7 +9,7 @@ export default class FunctionNode extends Node { const thisVariable = this.scope.findVariable( 'this' ); if ( withNew ) { - thisVariable.assignExpressionAtPath( [], UNKNOWN_OBJECT_LITERAL ); + thisVariable.assignExpressionAtPath( [], new VirtualObjectExpression() ); } else { thisVariable.assignExpressionAtPath( [], UNKNOWN_ASSIGNMENT ); } diff --git a/src/ast/nodes/shared/UndefinedIdentifier.js b/src/ast/nodes/shared/UndefinedIdentifier.js new file mode 100644 index 00000000000..5441ba1e087 --- /dev/null +++ b/src/ast/nodes/shared/UndefinedIdentifier.js @@ -0,0 +1,15 @@ +import Node from '../../Node'; + +export default class UndefinedIdentifier extends Node { + hasEffectsAsExpressionStatement () { + return false; + } + + hasEffectsWhenAssignedAtPath ( path ) { + return path.length > 0; + } + + hasEffectsWhenMutatedAtPath ( path ) { + return path.length > 0; + } +} diff --git a/src/ast/scopes/ModuleScope.js b/src/ast/scopes/ModuleScope.js index 6d643cedaa5..ebdc5c58cfd 100644 --- a/src/ast/scopes/ModuleScope.js +++ b/src/ast/scopes/ModuleScope.js @@ -1,8 +1,8 @@ import { forOwn } from '../../utils/object.js'; import relativeId from '../../utils/relativeId.js'; import Scope from './Scope.js'; -import { UNDEFINED_ASSIGNMENT } from '../values'; import LocalVariable from '../variables/LocalVariable'; +import UndefinedIdentifier from '../nodes/shared/UndefinedIdentifier'; export default class ModuleScope extends Scope { constructor ( module ) { @@ -12,7 +12,7 @@ export default class ModuleScope extends Scope { } ); this.module = module; - this.variables.this = new LocalVariable( 'this', null, UNDEFINED_ASSIGNMENT ); + this.variables.this = new LocalVariable( 'this', null, new UndefinedIdentifier() ); } deshadow ( names ) { diff --git a/src/ast/scopes/Scope.js b/src/ast/scopes/Scope.js index e02196eb474..c067a8af554 100644 --- a/src/ast/scopes/Scope.js +++ b/src/ast/scopes/Scope.js @@ -2,7 +2,7 @@ import { blank, keys } from '../../utils/object.js'; import LocalVariable from '../variables/LocalVariable'; import ParameterVariable from '../variables/ParameterVariable'; import ExportDefaultVariable from '../variables/ExportDefaultVariable'; -import { UNDEFINED_ASSIGNMENT } from '../values'; +import UndefinedIdentifier from '../nodes/shared/UndefinedIdentifier'; export default class Scope { constructor ( options = {} ) { @@ -29,7 +29,7 @@ export default class Scope { variable.addDeclaration( identifier ); options.init && variable.assignExpressionAtPath( [], options.init ); } else { - this.variables[ name ] = new LocalVariable( identifier.name, identifier, options.init || UNDEFINED_ASSIGNMENT ); + this.variables[ name ] = new LocalVariable( identifier.name, identifier, options.init || new UndefinedIdentifier() ); } return this.variables[ name ]; } diff --git a/src/ast/values.js b/src/ast/values.js index 5921576a8d2..275aa5744ba 100644 --- a/src/ast/values.js +++ b/src/ast/values.js @@ -8,21 +8,3 @@ export const UNKNOWN_ASSIGNMENT = { hasEffectsWhenCalledAtPath: () => true, hasEffectsWhenMutatedAtPath: () => true, }; - -export const UNDEFINED_ASSIGNMENT = { - type: 'UNDEFINED', - bindAssignmentAtPath: () => {}, - bindCallAtPath: () => {}, - hasEffectsWhenAssignedAtPath: () => true, - hasEffectsWhenCalledAtPath: () => true, - hasEffectsWhenMutatedAtPath: () => true, -}; - -export const UNKNOWN_OBJECT_LITERAL = { - type: 'UNKNOWN_OBJECT_LITERAL', - bindAssignmentAtPath: () => {}, - bindCallAtPath: () => {}, - hasEffectsWhenAssignedAtPath: path => path.length > 1, - hasEffectsWhenCalledAtPath: () => true, - hasEffectsWhenMutatedAtPath: path => path.length > 0, -}; From 3ad6908c26184bbb41e092d291f8513bdb5f5359 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Mon, 2 Oct 2017 08:34:03 +0200 Subject: [PATCH 24/76] Array.from(Map) provides a list of (key, value)-tuples. --- src/ast/variables/DeepSet.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ast/variables/DeepSet.js b/src/ast/variables/DeepSet.js index fd2a918e6ed..04761cedc5c 100644 --- a/src/ast/variables/DeepSet.js +++ b/src/ast/variables/DeepSet.js @@ -1,5 +1,5 @@ -const SET_KEY = {}; -export const UNKNOWN_KEY = {}; +const SET_KEY = { type: 'SET_KEY' }; +export const UNKNOWN_KEY = { type: 'UNKNOWN_KEY' }; export default class DeepSet { constructor () { @@ -58,7 +58,7 @@ export default class DeepSet { path.length > 0 && ( (nextPath === UNKNOWN_KEY - && Array.from( this._assignments ).some( ( assignment, subPath ) => { + && Array.from( this._assignments ).some( ( [ subPath, assignment ] ) => { if ( subPath !== SET_KEY ) { return assignment.someAtPath( remainingPath, predicateFunction ); } From 09643f3d095e44a9b8626035d2e0d76a0a95f82c Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Tue, 3 Oct 2017 12:07:15 +0200 Subject: [PATCH 25/76] Introduce hasEffectsWhenAccessed. For now we no longer swallow errors due to forbidden property access. In the future, we can use this to properly handle getters. --- src/ast/ExecutionPathOptions.js | 21 +++++++- src/ast/Node.js | 13 ++++- src/ast/nodes/ArrayExpression.js | 7 +++ src/ast/nodes/ArrowFunctionExpression.js | 4 ++ src/ast/nodes/AssignmentExpression.js | 4 ++ src/ast/nodes/BinaryExpression.js | 4 ++ src/ast/nodes/Identifier.js | 5 ++ src/ast/nodes/Literal.js | 7 +++ src/ast/nodes/LogicalExpression.js | 15 ++++++ src/ast/nodes/MemberExpression.js | 12 +++++ src/ast/nodes/NewExpression.js | 4 ++ src/ast/nodes/ObjectExpression.js | 10 ++++ src/ast/nodes/Property.js | 4 ++ src/ast/nodes/ThisExpression.js | 6 ++- src/ast/nodes/UnaryExpression.js | 7 +++ src/ast/nodes/UpdateExpression.js | 4 ++ src/ast/nodes/index.js | 4 +- src/ast/nodes/shared/ClassNode.js | 8 +++ src/ast/nodes/shared/FunctionNode.js | 10 ++++ src/ast/nodes/shared/UndefinedIdentifier.js | 12 +++++ src/ast/nodes/shared/VirtualNumberLiteral.js | 8 +++ .../nodes/shared/VirtualObjectExpression.js | 8 +++ src/ast/values.js | 2 + src/ast/variables/GlobalVariable.js | 6 +++ src/ast/variables/LocalVariable.js | 20 ++++++-- src/ast/variables/Variable.js | 4 ++ .../main.js | 2 +- .../_config.js | 2 +- .../_expected/amd.js | 4 +- .../_expected/cjs.js | 4 +- .../_expected/es.js | 4 +- .../_expected/iife.js | 4 +- .../_expected/umd.js | 4 +- .../main.js | 4 +- .../samples/effect-in-for-of-loop/_config.js | 2 +- .../effect-in-for-of-loop/_expected/amd.js | 6 +-- .../effect-in-for-of-loop/_expected/cjs.js | 6 +-- .../effect-in-for-of-loop/_expected/es.js | 6 +-- .../effect-in-for-of-loop/_expected/iife.js | 6 +-- .../effect-in-for-of-loop/_expected/umd.js | 6 +-- .../samples/effect-in-for-of-loop/main.js | 12 ++--- .../function-mutation/_expected/amd.js | 31 +++++++++--- .../function-mutation/_expected/cjs.js | 31 +++++++++--- .../samples/function-mutation/_expected/es.js | 31 ++++++++---- .../function-mutation/_expected/iife.js | 31 +++++++++--- .../function-mutation/_expected/umd.js | 31 +++++++++--- test/form/samples/function-mutation/main.js | 50 ++++++++++++++----- .../_config.js | 2 +- .../namespace-optimization-b/_expected/amd.js | 5 +- .../namespace-optimization-b/_expected/cjs.js | 5 +- .../namespace-optimization-b/_expected/es.js | 5 +- .../_expected/iife.js | 5 +- .../namespace-optimization-b/_expected/umd.js | 5 +- .../samples/nested-member-access/_config.js | 3 ++ .../nested-member-access/_expected/amd.js | 27 ++++++++++ .../nested-member-access/_expected/cjs.js | 25 ++++++++++ .../nested-member-access/_expected/es.js | 23 +++++++++ .../nested-member-access/_expected/iife.js | 28 +++++++++++ .../nested-member-access/_expected/umd.js | 31 ++++++++++++ .../form/samples/nested-member-access/main.js | 48 ++++++++++++++++++ 60 files changed, 593 insertions(+), 105 deletions(-) create mode 100644 src/ast/nodes/ArrayExpression.js create mode 100644 test/form/samples/nested-member-access/_config.js create mode 100644 test/form/samples/nested-member-access/_expected/amd.js create mode 100644 test/form/samples/nested-member-access/_expected/cjs.js create mode 100644 test/form/samples/nested-member-access/_expected/es.js create mode 100644 test/form/samples/nested-member-access/_expected/iife.js create mode 100644 test/form/samples/nested-member-access/_expected/umd.js create mode 100644 test/form/samples/nested-member-access/main.js diff --git a/src/ast/ExecutionPathOptions.js b/src/ast/ExecutionPathOptions.js index 2d222db9591..08af4123824 100644 --- a/src/ast/ExecutionPathOptions.js +++ b/src/ast/ExecutionPathOptions.js @@ -3,9 +3,10 @@ import Immutable from 'immutable'; const OPTION_IGNORE_BREAK_STATEMENTS = 'IGNORE_BREAK_STATEMENTS'; const OPTION_IGNORE_RETURN_AWAIT_YIELD = 'IGNORE_RETURN_AWAIT_YIELD'; const OPTION_IGNORE_SAFE_THIS_MUTATIONS = 'IGNORE_SAFE_THIS_MUTATIONS'; +const OPTION_ACCESSED_NODES = 'ACCESSED_NODES'; +const OPTION_ASSIGNED_NODES = 'ASSIGNED_NODES'; const OPTION_CALLED_NODES = 'CALLED_NODES'; const OPTION_MUTATED_NODES = 'MUTATED_NODES'; -const OPTION_ASSIGNED_NODES = 'ASSIGNED_NODES'; const IGNORED_LABELS = 'IGNORED_LABELS'; const RESULT_KEY = {}; @@ -114,6 +115,24 @@ export default class ExecutionPathOptions { return this.set( OPTION_IGNORE_SAFE_THIS_MUTATIONS, value ); } + /** + * @param {String[]} path + * @param {Node} node + * @return {ExecutionPathOptions} + */ + addAccessedNodeAtPath ( path, node ) { + return this.setIn( [ OPTION_ACCESSED_NODES, node, ...path, RESULT_KEY ], true ); + } + + /** + * @param {String[]} path + * @param {Node} node + * @return {boolean} + */ + hasNodeBeenAccessedAtPath ( path, node ) { + return this._optionValues.getIn( [ OPTION_ACCESSED_NODES, node, ...path, RESULT_KEY ] ); + } + /** * @param {String[]} path * @param {Node} node diff --git a/src/ast/Node.js b/src/ast/Node.js index 8efb0dc9f59..794298dd85c 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -62,7 +62,9 @@ export default class Node { * @return {boolean} */ hasEffects ( options ) { - return this.included || this.someChild( child => child.hasEffects( options ) ); + return this.included + || this.hasEffectsWhenAccessedAtPath( [], options ) + || this.someChild( child => child.hasEffects( options ) ); } /** @@ -77,6 +79,15 @@ export default class Node { return true; } + /** + * @param {String[]} path + * @param {ExecutionPathOptions} options + * @return {boolean} + */ + hasEffectsWhenAccessedAtPath ( path ) { + return path.length > 0; + } + /** * @param {String[]} path * @param {ExecutionPathOptions} options diff --git a/src/ast/nodes/ArrayExpression.js b/src/ast/nodes/ArrayExpression.js new file mode 100644 index 00000000000..b6b122bd2c8 --- /dev/null +++ b/src/ast/nodes/ArrayExpression.js @@ -0,0 +1,7 @@ +import Node from '../Node.js'; + +export default class ArrayExpression extends Node { + hasEffectsWhenAccessedAtPath ( path ) { + return path.length > 1; + } +} diff --git a/src/ast/nodes/ArrowFunctionExpression.js b/src/ast/nodes/ArrowFunctionExpression.js index e6c41876d56..54e8db6a116 100644 --- a/src/ast/nodes/ArrowFunctionExpression.js +++ b/src/ast/nodes/ArrowFunctionExpression.js @@ -9,6 +9,10 @@ export default class ArrowFunctionExpression extends Node { return this.included; } + hasEffectsWhenAccessedAtPath ( path ) { + return path.length > 1; + } + hasEffectsWhenAssignedAtPath ( path ) { if ( path.length === 0 ) { return true; diff --git a/src/ast/nodes/AssignmentExpression.js b/src/ast/nodes/AssignmentExpression.js index c722953a631..99882df7da1 100644 --- a/src/ast/nodes/AssignmentExpression.js +++ b/src/ast/nodes/AssignmentExpression.js @@ -15,4 +15,8 @@ export default class AssignmentExpression extends Node { hasEffectsAsExpressionStatement ( options ) { return this.hasEffects( options ); } + + hasEffectsWhenAccessedAtPath ( path, options ) { + return this.right.hasEffectsWhenAccessedAtPath( path, options ); + } } diff --git a/src/ast/nodes/BinaryExpression.js b/src/ast/nodes/BinaryExpression.js index 8c7f8826165..52e4acb0337 100644 --- a/src/ast/nodes/BinaryExpression.js +++ b/src/ast/nodes/BinaryExpression.js @@ -38,4 +38,8 @@ export default class BinaryExpression extends Node { return operators[ this.operator ]( leftValue, rightValue ); } + + hasEffectsWhenAccessedAtPath ( path ) { + return path.length > 1; + } } diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index c1e5c0ec4e7..4249b3ea01d 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -31,6 +31,11 @@ export default class Identifier extends Node { return this.hasEffects( options ) || this.variable.isGlobal; } + hasEffectsWhenAccessedAtPath ( path, options ) { + return this.variable + && this.variable.hasEffectsWhenAccessedAtPath( path, options ); + } + hasEffectsWhenAssignedAtPath ( path, options ) { return !this.variable || this.variable.hasEffectsWhenAssignedAtPath( path, options ); diff --git a/src/ast/nodes/Literal.js b/src/ast/nodes/Literal.js index eb7f162e56b..b8b1277763f 100644 --- a/src/ast/nodes/Literal.js +++ b/src/ast/nodes/Literal.js @@ -5,6 +5,13 @@ export default class Literal extends Node { return this.value; } + hasEffectsWhenAccessedAtPath ( path ) { + if (this.value === null) { + return path.length > 0; + } + return path.length > 1; + } + hasEffectsWhenAssignedAtPath ( path ) { if (this.value === null) { return path.length > 0; diff --git a/src/ast/nodes/LogicalExpression.js b/src/ast/nodes/LogicalExpression.js index 09f3803f241..6fcb733dbc0 100644 --- a/src/ast/nodes/LogicalExpression.js +++ b/src/ast/nodes/LogicalExpression.js @@ -17,6 +17,21 @@ export default class LogicalExpression extends Node { return operators[ this.operator ]( leftValue, rightValue ); } + hasEffectsWhenAccessedAtPath ( path, options ) { + if (path.length === 0) { + return false; + } + const leftValue = this.left.getValue(); + if ( leftValue === UNKNOWN_VALUE ) { + return this.left.hasEffectsWhenAccessedAtPath( path, options ) + || this.right.hasEffectsWhenAccessedAtPath( path, options ); + } + if ( (leftValue && this.operator === '||') || (!leftValue && this.operator === '&&') ) { + return this.left.hasEffectsWhenAccessedAtPath( path, options ); + } + return this.right.hasEffectsWhenAccessedAtPath( path, options ); + } + hasEffectsWhenAssignedAtPath ( path, options ) { return path.length === 0 || this.hasEffectsWhenMutatedAtPath( path.slice( 1 ), options ); diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index db53d84a0b4..30d6e00319c 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -103,6 +103,18 @@ export default class MemberExpression extends Node { } } + hasEffects ( options ) { + return super.hasEffects( options ) + || this.object.hasEffectsWhenAccessedAtPath( [ this.computed ? UNKNOWN_KEY : this.property.name ], options ); + } + + hasEffectsWhenAccessedAtPath ( path, options ) { + if ( this.variable ) { + return this.variable.hasEffectsWhenAccessedAtPath( path, options ); + } + return this.object.hasEffectsWhenAccessedAtPath( [ this.computed ? UNKNOWN_KEY : this.property.name, ...path ], options ); + } + hasEffectsWhenAssignedAtPath ( path, options ) { if ( this.variable ) { return this.variable.hasEffectsWhenAssignedAtPath( path, options ); diff --git a/src/ast/nodes/NewExpression.js b/src/ast/nodes/NewExpression.js index 8ff23c32c4b..c6e5a51e852 100644 --- a/src/ast/nodes/NewExpression.js +++ b/src/ast/nodes/NewExpression.js @@ -11,4 +11,8 @@ export default class NewExpression extends Node { || this.arguments.some( child => child.hasEffects( options ) ) || this.callee.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions( this.callee ) ); } + + hasEffectsWhenAccessedAtPath ( path ) { + return path.length > 1; + } } diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index cf9fec0d85e..7cf9f525f86 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -38,6 +38,16 @@ export default class ObjectExpression extends Node { return { properties, hasCertainHit }; } + hasEffectsWhenAccessedAtPath ( path, options ) { + if ( path.length <= 1 ) { + return false; + } + const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ] ); + + return !hasCertainHit || properties.some( property => + property.hasEffectsWhenAccessedAtPath( path.slice( 1 ), options ) ); + } + hasEffectsWhenAssignedAtPath ( path, options ) { if ( path.length <= 1 ) { return false; diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index 21b656c8199..2785f7b7c3f 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -10,6 +10,10 @@ export default class Property extends Node { this.value.bindCallAtPath( path, callOptions ); } + hasEffectsWhenAccessedAtPath ( path, options ) { + return this.value.hasEffectsWhenAccessedAtPath( path, options ); + } + hasEffectsWhenAssignedAtPath ( path, options ) { return this.value.hasEffectsWhenAssignedAtPath( path, options ); } diff --git a/src/ast/nodes/ThisExpression.js b/src/ast/nodes/ThisExpression.js index 74650568192..230ce6f60b2 100644 --- a/src/ast/nodes/ThisExpression.js +++ b/src/ast/nodes/ThisExpression.js @@ -20,11 +20,15 @@ export default class ThisExpression extends Node { this.variable = this.scope.findVariable( 'this' ); } + hasEffectsWhenAccessedAtPath ( path, options ) { + return this.variable.hasEffectsWhenAccessedAtPath( path, options ); + } + hasEffectsWhenAssignedAtPath ( path, options ) { if ( path.length === 0 ) { return true; } - return this.hasEffectsWhenMutatedAtPath( path.slice(1), options ); + return this.hasEffectsWhenMutatedAtPath( path.slice( 1 ), options ); } hasEffectsWhenMutatedAtPath ( path, options ) { diff --git a/src/ast/nodes/UnaryExpression.js b/src/ast/nodes/UnaryExpression.js index 86c9549214d..c8edc0bf702 100644 --- a/src/ast/nodes/UnaryExpression.js +++ b/src/ast/nodes/UnaryExpression.js @@ -40,6 +40,13 @@ export default class UnaryExpression extends Node { return this.hasEffects( options ); } + hasEffectsWhenAccessedAtPath ( path ) { + if ( this.operator === 'void' ) { + return path.length > 0; + } + return path.length > 1; + } + initialiseNode () { this.value = this.getValue(); } diff --git a/src/ast/nodes/UpdateExpression.js b/src/ast/nodes/UpdateExpression.js index 85ca2b41eea..f5542f2a0b7 100644 --- a/src/ast/nodes/UpdateExpression.js +++ b/src/ast/nodes/UpdateExpression.js @@ -22,4 +22,8 @@ export default class UpdateExpression extends Node { hasEffectsAsExpressionStatement ( options ) { return this.hasEffects( options ); } + + hasEffectsWhenAccessedAtPath ( path ) { + return path.length > 1; + } } diff --git a/src/ast/nodes/index.js b/src/ast/nodes/index.js index dc9bb950ae7..76f0ff3b5e7 100644 --- a/src/ast/nodes/index.js +++ b/src/ast/nodes/index.js @@ -1,3 +1,4 @@ +import ArrayExpression from './ArrayExpression.js'; import ArrayPattern from './ArrayPattern.js'; import ArrowFunctionExpression from './ArrowFunctionExpression.js'; import AssignmentExpression from './AssignmentExpression.js'; @@ -52,10 +53,9 @@ import VariableDeclarator from './VariableDeclarator.js'; import VariableDeclaration from './VariableDeclaration.js'; import WhileStatement from './WhileStatement.js'; import YieldExpression from './YieldExpression.js'; -import Node from '../Node'; export default { - ArrayExpression: Node, + ArrayExpression, ArrayPattern, ArrowFunctionExpression, AssignmentExpression, diff --git a/src/ast/nodes/shared/ClassNode.js b/src/ast/nodes/shared/ClassNode.js index 2fbe37505fd..110c1f5bb32 100644 --- a/src/ast/nodes/shared/ClassNode.js +++ b/src/ast/nodes/shared/ClassNode.js @@ -13,6 +13,14 @@ export default class ClassNode extends Node { return this.hasEffects( options ); } + hasEffectsWhenAccessedAtPath ( path ) { + return path.length > 1; + } + + hasEffectsWhenAssignedAtPath ( path ) { + return path.length > 1; + } + hasEffectsWhenCalledAtPath ( path, options ) { return this.body.hasEffectsWhenCalledAtPath( path, options ) || ( this.superClass && this.superClass.hasEffectsWhenCalledAtPath( path, options ) ); diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index c6a5f06a4bb..1a59af1c9a2 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -24,6 +24,16 @@ export default class FunctionNode extends Node { return this.hasEffects( options ); } + hasEffectsWhenAccessedAtPath ( path, options ) { + if ( path.length <= 1 ) { + return false; + } + if ( path[ 0 ] === 'prototype' ) { + return this.prototypeObject.hasEffectsWhenAccessedAtPath( path.slice( 1 ), options ); + } + return true; + } + hasEffectsWhenAssignedAtPath ( path, options ) { if ( path.length <= 1 ) { return false; diff --git a/src/ast/nodes/shared/UndefinedIdentifier.js b/src/ast/nodes/shared/UndefinedIdentifier.js index 5441ba1e087..b0f45866659 100644 --- a/src/ast/nodes/shared/UndefinedIdentifier.js +++ b/src/ast/nodes/shared/UndefinedIdentifier.js @@ -1,10 +1,18 @@ import Node from '../../Node'; export default class UndefinedIdentifier extends Node { + hasEffects () { + return false; + } + hasEffectsAsExpressionStatement () { return false; } + hasEffectsWhenAccessedAtPath ( path ) { + return path.length > 0; + } + hasEffectsWhenAssignedAtPath ( path ) { return path.length > 0; } @@ -12,4 +20,8 @@ export default class UndefinedIdentifier extends Node { hasEffectsWhenMutatedAtPath ( path ) { return path.length > 0; } + + toString () { + return 'undefined'; + } } diff --git a/src/ast/nodes/shared/VirtualNumberLiteral.js b/src/ast/nodes/shared/VirtualNumberLiteral.js index 46eda7bdb60..e2951151f6e 100644 --- a/src/ast/nodes/shared/VirtualNumberLiteral.js +++ b/src/ast/nodes/shared/VirtualNumberLiteral.js @@ -1,6 +1,10 @@ import Node from '../../Node'; export default class VirtualNumberLiteral extends Node { + hasEffectsWhenAccessedAtPath ( path ) { + return path.length > 1; + } + hasEffectsWhenAssignedAtPath ( path ) { return path.length > 1; } @@ -8,4 +12,8 @@ export default class VirtualNumberLiteral extends Node { hasEffectsWhenMutatedAtPath ( path ) { return path.length > 0; } + + toString () { + return '[[VIRTUAL NUMBER]]'; + } } diff --git a/src/ast/nodes/shared/VirtualObjectExpression.js b/src/ast/nodes/shared/VirtualObjectExpression.js index b20acb7b195..b7c897a2746 100644 --- a/src/ast/nodes/shared/VirtualObjectExpression.js +++ b/src/ast/nodes/shared/VirtualObjectExpression.js @@ -1,6 +1,10 @@ import Node from '../../Node'; export default class VirtualObjectExpression extends Node { + hasEffectsWhenAccessedAtPath ( path ) { + return path.length > 1; + } + hasEffectsWhenAssignedAtPath ( path ) { return path.length > 1; } @@ -8,4 +12,8 @@ export default class VirtualObjectExpression extends Node { hasEffectsWhenMutatedAtPath ( path ) { return path.length > 0; } + + toString () { + return '[[VIRTUAL OBJECT]]'; + } } diff --git a/src/ast/values.js b/src/ast/values.js index 275aa5744ba..171b69678b0 100644 --- a/src/ast/values.js +++ b/src/ast/values.js @@ -4,7 +4,9 @@ export const UNKNOWN_ASSIGNMENT = { type: 'UNKNOWN', bindAssignmentAtPath: () => {}, bindCallAtPath: () => {}, + hasEffectsWhenAccessedAtPath: path => path.length > 0, hasEffectsWhenAssignedAtPath: () => true, hasEffectsWhenCalledAtPath: () => true, hasEffectsWhenMutatedAtPath: () => true, + toString: () => '[[UNKNOWN]]' }; diff --git a/src/ast/variables/GlobalVariable.js b/src/ast/variables/GlobalVariable.js index a412da44999..21979658d52 100644 --- a/src/ast/variables/GlobalVariable.js +++ b/src/ast/variables/GlobalVariable.js @@ -14,6 +14,12 @@ export default class GlobalVariable extends Variable { if ( reference.isReassignment ) this.isReassigned = true; } + hasEffectsWhenAccessedAtPath ( path ) { + // path.length == 0 can also have an effect but we postpone this for now + return path.length > 0 + && !pureFunctions[ [ this.name, ...path ].join( '.' ) ]; + } + hasEffectsWhenCalledAtPath ( path ) { return !pureFunctions[ [ this.name, ...path ].join( '.' ) ]; } diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index 170cc2ac16f..a3f94a69820 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -20,7 +20,7 @@ export default class LocalVariable extends Variable { } addCallAtPath ( path, callOptions ) { - if (path.length > MAX_PATH_LENGTH) { + if ( path.length > MAX_PATH_LENGTH ) { return; } if ( this.calls.hasAtPath( path, callOptions ) ) return; @@ -30,7 +30,7 @@ export default class LocalVariable extends Variable { } assignExpressionAtPath ( path, expression ) { - if (path.length > MAX_PATH_LENGTH) { + if ( path.length > MAX_PATH_LENGTH ) { return; } if ( this.assignedExpressions.hasAtPath( path, expression ) ) return; @@ -53,8 +53,18 @@ export default class LocalVariable extends Variable { return `exports.${this.exportName}`; } + hasEffectsWhenAccessedAtPath ( path, options ) { + if ( path.length > MAX_PATH_LENGTH ) { + return true; + } + return this.assignedExpressions.someAtPath( path, ( relativePath, node ) => + relativePath.length > 0 + && !options.hasNodeBeenAccessedAtPath( relativePath, node ) + && node.hasEffectsWhenAccessedAtPath( relativePath, options.addAccessedNodeAtPath( relativePath, node ) ) ); + } + hasEffectsWhenAssignedAtPath ( path, options ) { - if (path.length > MAX_PATH_LENGTH) { + if ( path.length > MAX_PATH_LENGTH ) { return true; } return this.included @@ -65,7 +75,7 @@ export default class LocalVariable extends Variable { } hasEffectsWhenCalledAtPath ( path, options ) { - if (path.length > MAX_PATH_LENGTH) { + if ( path.length > MAX_PATH_LENGTH ) { return true; } return this.assignedExpressions.someAtPath( path, ( relativePath, node ) => { @@ -78,7 +88,7 @@ export default class LocalVariable extends Variable { } hasEffectsWhenMutatedAtPath ( path, options ) { - if (path.length > MAX_PATH_LENGTH) { + if ( path.length > MAX_PATH_LENGTH ) { return true; } return this.included diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index eabcec063d8..657c31cb891 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -13,6 +13,10 @@ export default class Variable { return this.name; } + hasEffectsWhenAccessedAtPath ( path ) { + return path.length > 0; + } + hasEffectsWhenAssignedAtPath () { return true; } diff --git a/test/form/samples/computed-member-expression-assignments/main.js b/test/form/samples/computed-member-expression-assignments/main.js index 220a12f7203..2e0f4520d73 100644 --- a/test/form/samples/computed-member-expression-assignments/main.js +++ b/test/form/samples/computed-member-expression-assignments/main.js @@ -18,6 +18,6 @@ retained5[ 'f' + 'oo' ](); const removed1 = { foo: {} }; removed1.foo[ 'b' + 'ar' ] = 1; -const removed2 = { foo: () => {} }; +const removed2 = { foo: function () {} }; removed2[ 'f' + 'oo' ] = function () {this.x = 1;}; const result2 = new removed2.foo(); diff --git a/test/form/samples/effect-in-for-of-loop-in-functions/_config.js b/test/form/samples/effect-in-for-of-loop-in-functions/_config.js index 79abe7413e3..0e80a63f4a0 100644 --- a/test/form/samples/effect-in-for-of-loop-in-functions/_config.js +++ b/test/form/samples/effect-in-for-of-loop-in-functions/_config.js @@ -1,3 +1,3 @@ module.exports = { description: 'includes effects in for-of loop (#870)' -} +}; diff --git a/test/form/samples/effect-in-for-of-loop-in-functions/_expected/amd.js b/test/form/samples/effect-in-for-of-loop-in-functions/_expected/amd.js index a7c1b488500..4d69a4b15b7 100644 --- a/test/form/samples/effect-in-for-of-loop-in-functions/_expected/amd.js +++ b/test/form/samples/effect-in-for-of-loop-in-functions/_expected/amd.js @@ -1,6 +1,6 @@ define(function () { 'use strict'; - const items = [{}, {}, {}]; + const items = { children: [ {}, {}, {} ] }; function a () { for ( const item of items.children ) { @@ -23,6 +23,6 @@ define(function () { 'use strict'; { foo: 'a', bar: 'c' }, { foo: 'a', bar: 'c' }, { foo: 'a', bar: 'c' } - ]); + ] ); }); diff --git a/test/form/samples/effect-in-for-of-loop-in-functions/_expected/cjs.js b/test/form/samples/effect-in-for-of-loop-in-functions/_expected/cjs.js index 51c5f3b83a8..8b4a0c2380d 100644 --- a/test/form/samples/effect-in-for-of-loop-in-functions/_expected/cjs.js +++ b/test/form/samples/effect-in-for-of-loop-in-functions/_expected/cjs.js @@ -1,6 +1,6 @@ 'use strict'; -const items = [{}, {}, {}]; +const items = { children: [ {}, {}, {} ] }; function a () { for ( const item of items.children ) { @@ -23,4 +23,4 @@ assert.deepEqual( items, [ { foo: 'a', bar: 'c' }, { foo: 'a', bar: 'c' }, { foo: 'a', bar: 'c' } -]); +] ); diff --git a/test/form/samples/effect-in-for-of-loop-in-functions/_expected/es.js b/test/form/samples/effect-in-for-of-loop-in-functions/_expected/es.js index 0aafee1f7be..5b7bb91e0c7 100644 --- a/test/form/samples/effect-in-for-of-loop-in-functions/_expected/es.js +++ b/test/form/samples/effect-in-for-of-loop-in-functions/_expected/es.js @@ -1,4 +1,4 @@ -const items = [{}, {}, {}]; +const items = { children: [ {}, {}, {} ] }; function a () { for ( const item of items.children ) { @@ -21,4 +21,4 @@ assert.deepEqual( items, [ { foo: 'a', bar: 'c' }, { foo: 'a', bar: 'c' }, { foo: 'a', bar: 'c' } -]); +] ); diff --git a/test/form/samples/effect-in-for-of-loop-in-functions/_expected/iife.js b/test/form/samples/effect-in-for-of-loop-in-functions/_expected/iife.js index 406ab3aeb8e..7e12d263cb1 100644 --- a/test/form/samples/effect-in-for-of-loop-in-functions/_expected/iife.js +++ b/test/form/samples/effect-in-for-of-loop-in-functions/_expected/iife.js @@ -1,7 +1,7 @@ (function () { 'use strict'; - const items = [{}, {}, {}]; + const items = { children: [ {}, {}, {} ] }; function a () { for ( const item of items.children ) { @@ -24,6 +24,6 @@ { foo: 'a', bar: 'c' }, { foo: 'a', bar: 'c' }, { foo: 'a', bar: 'c' } - ]); + ] ); }()); diff --git a/test/form/samples/effect-in-for-of-loop-in-functions/_expected/umd.js b/test/form/samples/effect-in-for-of-loop-in-functions/_expected/umd.js index 33518c0de73..7141426d87f 100644 --- a/test/form/samples/effect-in-for-of-loop-in-functions/_expected/umd.js +++ b/test/form/samples/effect-in-for-of-loop-in-functions/_expected/umd.js @@ -4,7 +4,7 @@ (factory()); }(this, (function () { 'use strict'; - const items = [{}, {}, {}]; + const items = { children: [ {}, {}, {} ] }; function a () { for ( const item of items.children ) { @@ -27,6 +27,6 @@ { foo: 'a', bar: 'c' }, { foo: 'a', bar: 'c' }, { foo: 'a', bar: 'c' } - ]); + ] ); }))); diff --git a/test/form/samples/effect-in-for-of-loop-in-functions/main.js b/test/form/samples/effect-in-for-of-loop-in-functions/main.js index 7ba1241a984..2db042360d8 100644 --- a/test/form/samples/effect-in-for-of-loop-in-functions/main.js +++ b/test/form/samples/effect-in-for-of-loop-in-functions/main.js @@ -1,4 +1,4 @@ -const items = [{}, {}, {}]; +const items = { children: [ {}, {}, {} ] }; function a () { for ( const item of items.children ) { @@ -38,4 +38,4 @@ assert.deepEqual( items, [ { foo: 'a', bar: 'c' }, { foo: 'a', bar: 'c' }, { foo: 'a', bar: 'c' } -]); +] ); diff --git a/test/form/samples/effect-in-for-of-loop/_config.js b/test/form/samples/effect-in-for-of-loop/_config.js index 79abe7413e3..0e80a63f4a0 100644 --- a/test/form/samples/effect-in-for-of-loop/_config.js +++ b/test/form/samples/effect-in-for-of-loop/_config.js @@ -1,3 +1,3 @@ module.exports = { description: 'includes effects in for-of loop (#870)' -} +}; diff --git a/test/form/samples/effect-in-for-of-loop/_expected/amd.js b/test/form/samples/effect-in-for-of-loop/_expected/amd.js index 9f4f7a81a9b..cba6da29411 100644 --- a/test/form/samples/effect-in-for-of-loop/_expected/amd.js +++ b/test/form/samples/effect-in-for-of-loop/_expected/amd.js @@ -2,16 +2,16 @@ define(function () { 'use strict'; const items = [{}, {}, {}]; - for ( const a of items.children ) { + for ( const a of items ) { a.foo = 'a'; } let c; - for ( c of items.children ) { + for ( c of items ) { c.bar = 'c'; } - for ( e of items.children ) { + for ( e of items ) { e.baz = 'e'; } var e; diff --git a/test/form/samples/effect-in-for-of-loop/_expected/cjs.js b/test/form/samples/effect-in-for-of-loop/_expected/cjs.js index 448aba40d66..56d59be6e60 100644 --- a/test/form/samples/effect-in-for-of-loop/_expected/cjs.js +++ b/test/form/samples/effect-in-for-of-loop/_expected/cjs.js @@ -2,16 +2,16 @@ const items = [{}, {}, {}]; -for ( const a of items.children ) { +for ( const a of items ) { a.foo = 'a'; } let c; -for ( c of items.children ) { +for ( c of items ) { c.bar = 'c'; } -for ( e of items.children ) { +for ( e of items ) { e.baz = 'e'; } var e; diff --git a/test/form/samples/effect-in-for-of-loop/_expected/es.js b/test/form/samples/effect-in-for-of-loop/_expected/es.js index 881fa011f65..eccc0b7a9b3 100644 --- a/test/form/samples/effect-in-for-of-loop/_expected/es.js +++ b/test/form/samples/effect-in-for-of-loop/_expected/es.js @@ -1,15 +1,15 @@ const items = [{}, {}, {}]; -for ( const a of items.children ) { +for ( const a of items ) { a.foo = 'a'; } let c; -for ( c of items.children ) { +for ( c of items ) { c.bar = 'c'; } -for ( e of items.children ) { +for ( e of items ) { e.baz = 'e'; } var e; diff --git a/test/form/samples/effect-in-for-of-loop/_expected/iife.js b/test/form/samples/effect-in-for-of-loop/_expected/iife.js index 1e5f236fe36..5a3e4a30f96 100644 --- a/test/form/samples/effect-in-for-of-loop/_expected/iife.js +++ b/test/form/samples/effect-in-for-of-loop/_expected/iife.js @@ -3,16 +3,16 @@ const items = [{}, {}, {}]; - for ( const a of items.children ) { + for ( const a of items ) { a.foo = 'a'; } let c; - for ( c of items.children ) { + for ( c of items ) { c.bar = 'c'; } - for ( e of items.children ) { + for ( e of items ) { e.baz = 'e'; } var e; diff --git a/test/form/samples/effect-in-for-of-loop/_expected/umd.js b/test/form/samples/effect-in-for-of-loop/_expected/umd.js index e0c6be69450..79ae821c7fb 100644 --- a/test/form/samples/effect-in-for-of-loop/_expected/umd.js +++ b/test/form/samples/effect-in-for-of-loop/_expected/umd.js @@ -6,16 +6,16 @@ const items = [{}, {}, {}]; - for ( const a of items.children ) { + for ( const a of items ) { a.foo = 'a'; } let c; - for ( c of items.children ) { + for ( c of items ) { c.bar = 'c'; } - for ( e of items.children ) { + for ( e of items ) { e.baz = 'e'; } var e; diff --git a/test/form/samples/effect-in-for-of-loop/main.js b/test/form/samples/effect-in-for-of-loop/main.js index 2b82950e745..851682f5491 100644 --- a/test/form/samples/effect-in-for-of-loop/main.js +++ b/test/form/samples/effect-in-for-of-loop/main.js @@ -1,29 +1,29 @@ const items = [{}, {}, {}]; -for ( const a of items.children ) { +for ( const a of items ) { a.foo = 'a'; } -for ( const b of items.children ) { +for ( const b of items ) { // do nothing } let c; -for ( c of items.children ) { +for ( c of items ) { c.bar = 'c'; } let d; -for ( d of items.children ) { +for ( d of items ) { // do nothing } -for ( e of items.children ) { +for ( e of items ) { e.baz = 'e'; } var e; -for ( f of items.children ) { +for ( f of items ) { // do nothing } var f; diff --git a/test/form/samples/function-mutation/_expected/amd.js b/test/form/samples/function-mutation/_expected/amd.js index d5baeb0360c..c234d477bda 100644 --- a/test/form/samples/function-mutation/_expected/amd.js +++ b/test/form/samples/function-mutation/_expected/amd.js @@ -4,31 +4,46 @@ define(['exports'], function (exports) { 'use strict'; console.log( 'foo' ); } + const bar = function () { + console.log( 'bar' ); + }; + + const baz = () => console.log( 'baz' ); + function a () { console.log( 'a' ); } a.foo = foo; - const bar = function () { - console.log( 'bar' ); - }; - const c = function () { console.log( 'c' ); }; - c.bar = bar; - const baz = () => console.log( 'baz' ); + const e = () => console.log( 'e' ); + e.baz = baz; - const e = () => console.log( 'c' ); + class g { + constructor () { + console.log( 'g' ); + } + } - e.baz = baz; + g.foo = foo; + + const i = class { + constructor () { + console.log( 'i' ); + } + }; + i.foo = foo; exports.a = a; exports.c = c; exports.e = e; + exports.g = g; + exports.i = i; Object.defineProperty(exports, '__esModule', { value: true }); diff --git a/test/form/samples/function-mutation/_expected/cjs.js b/test/form/samples/function-mutation/_expected/cjs.js index d657dc40e9f..51b5d461cf8 100644 --- a/test/form/samples/function-mutation/_expected/cjs.js +++ b/test/form/samples/function-mutation/_expected/cjs.js @@ -6,28 +6,43 @@ function foo () { console.log( 'foo' ); } +const bar = function () { + console.log( 'bar' ); +}; + +const baz = () => console.log( 'baz' ); + function a () { console.log( 'a' ); } a.foo = foo; -const bar = function () { - console.log( 'bar' ); -}; - const c = function () { console.log( 'c' ); }; - c.bar = bar; -const baz = () => console.log( 'baz' ); +const e = () => console.log( 'e' ); +e.baz = baz; -const e = () => console.log( 'c' ); +class g { + constructor () { + console.log( 'g' ); + } +} -e.baz = baz; +g.foo = foo; + +const i = class { + constructor () { + console.log( 'i' ); + } +}; +i.foo = foo; exports.a = a; exports.c = c; exports.e = e; +exports.g = g; +exports.i = i; diff --git a/test/form/samples/function-mutation/_expected/es.js b/test/form/samples/function-mutation/_expected/es.js index b846d717e9a..436092bcd6e 100644 --- a/test/form/samples/function-mutation/_expected/es.js +++ b/test/form/samples/function-mutation/_expected/es.js @@ -2,26 +2,39 @@ function foo () { console.log( 'foo' ); } +const bar = function () { + console.log( 'bar' ); +}; + +const baz = () => console.log( 'baz' ); + function a () { console.log( 'a' ); } a.foo = foo; -const bar = function () { - console.log( 'bar' ); -}; - const c = function () { console.log( 'c' ); }; - c.bar = bar; -const baz = () => console.log( 'baz' ); +const e = () => console.log( 'e' ); +e.baz = baz; -const e = () => console.log( 'c' ); +class g { + constructor () { + console.log( 'g' ); + } +} -e.baz = baz; +g.foo = foo; + +const i = class { + constructor () { + console.log( 'i' ); + } +}; +i.foo = foo; -export { a, c, e }; +export { a, c, e, g, i }; diff --git a/test/form/samples/function-mutation/_expected/iife.js b/test/form/samples/function-mutation/_expected/iife.js index 4c7f183bcd3..2f94be5089a 100644 --- a/test/form/samples/function-mutation/_expected/iife.js +++ b/test/form/samples/function-mutation/_expected/iife.js @@ -5,31 +5,46 @@ var bundle = (function (exports) { console.log( 'foo' ); } + const bar = function () { + console.log( 'bar' ); + }; + + const baz = () => console.log( 'baz' ); + function a () { console.log( 'a' ); } a.foo = foo; - const bar = function () { - console.log( 'bar' ); - }; - const c = function () { console.log( 'c' ); }; - c.bar = bar; - const baz = () => console.log( 'baz' ); + const e = () => console.log( 'e' ); + e.baz = baz; - const e = () => console.log( 'c' ); + class g { + constructor () { + console.log( 'g' ); + } + } - e.baz = baz; + g.foo = foo; + + const i = class { + constructor () { + console.log( 'i' ); + } + }; + i.foo = foo; exports.a = a; exports.c = c; exports.e = e; + exports.g = g; + exports.i = i; return exports; diff --git a/test/form/samples/function-mutation/_expected/umd.js b/test/form/samples/function-mutation/_expected/umd.js index 62cbfc53d76..b70f448fcc0 100644 --- a/test/form/samples/function-mutation/_expected/umd.js +++ b/test/form/samples/function-mutation/_expected/umd.js @@ -8,31 +8,46 @@ console.log( 'foo' ); } + const bar = function () { + console.log( 'bar' ); + }; + + const baz = () => console.log( 'baz' ); + function a () { console.log( 'a' ); } a.foo = foo; - const bar = function () { - console.log( 'bar' ); - }; - const c = function () { console.log( 'c' ); }; - c.bar = bar; - const baz = () => console.log( 'baz' ); + const e = () => console.log( 'e' ); + e.baz = baz; - const e = () => console.log( 'c' ); + class g { + constructor () { + console.log( 'g' ); + } + } - e.baz = baz; + g.foo = foo; + + const i = class { + constructor () { + console.log( 'i' ); + } + }; + i.foo = foo; exports.a = a; exports.c = c; exports.e = e; + exports.g = g; + exports.i = i; Object.defineProperty(exports, '__esModule', { value: true }); diff --git a/test/form/samples/function-mutation/main.js b/test/form/samples/function-mutation/main.js index 061f858a7bc..63ce0806709 100644 --- a/test/form/samples/function-mutation/main.js +++ b/test/form/samples/function-mutation/main.js @@ -2,6 +2,12 @@ function foo () { console.log( 'foo' ); } +const bar = function () { + console.log( 'bar' ); +}; + +const baz = () => console.log( 'baz' ); + function a () { console.log( 'a' ); } @@ -14,30 +20,50 @@ function b () { b.foo = foo; -const bar = function () { - console.log( 'bar' ); -}; - const c = function () { console.log( 'c' ); }; - c.bar = bar; const d = function () { console.log( 'd' ); }; - d.bar = bar; -const baz = () => console.log( 'baz' ); +const e = () => console.log( 'e' ); +e.baz = baz; -const e = () => console.log( 'c' ); +const f = () => console.log( 'f' ); +f.baz = baz; -e.baz = baz; +class g { + constructor () { + console.log( 'g' ); + } +} -const f = () => console.log( 'd' ); +g.foo = foo; -f.baz = baz; +class h { + constructor () { + console.log( 'g' ); + } +} + +h.foo = foo; + +const i = class { + constructor () { + console.log( 'i' ); + } +}; +i.foo = foo; + +const j = class { + constructor () { + console.log( 'j' ); + } +}; +j.foo = foo; -export { a, c, e }; \ No newline at end of file +export { a, c, e, g, i }; diff --git a/test/form/samples/includes-all-namespace-declarations/_config.js b/test/form/samples/includes-all-namespace-declarations/_config.js index 30ffc5edba8..a97236d3054 100644 --- a/test/form/samples/includes-all-namespace-declarations/_config.js +++ b/test/form/samples/includes-all-namespace-declarations/_config.js @@ -1,3 +1,3 @@ module.exports = { description: 'includes all declarations referenced by reified namespaces' -} +}; diff --git a/test/form/samples/namespace-optimization-b/_expected/amd.js b/test/form/samples/namespace-optimization-b/_expected/amd.js index c7e8abe0e60..e2582ccbce3 100644 --- a/test/form/samples/namespace-optimization-b/_expected/amd.js +++ b/test/form/samples/namespace-optimization-b/_expected/amd.js @@ -8,7 +8,10 @@ define(function () { 'use strict'; foo(); foo(); - + var a; + if ( a.b ) { + // empty + } } a(); diff --git a/test/form/samples/namespace-optimization-b/_expected/cjs.js b/test/form/samples/namespace-optimization-b/_expected/cjs.js index f25caa7c2dd..a826df23f94 100644 --- a/test/form/samples/namespace-optimization-b/_expected/cjs.js +++ b/test/form/samples/namespace-optimization-b/_expected/cjs.js @@ -8,7 +8,10 @@ function a () { foo(); foo(); - + var a; + if ( a.b ) { + // empty + } } a(); diff --git a/test/form/samples/namespace-optimization-b/_expected/es.js b/test/form/samples/namespace-optimization-b/_expected/es.js index aad9f322ce2..85c15d863f4 100644 --- a/test/form/samples/namespace-optimization-b/_expected/es.js +++ b/test/form/samples/namespace-optimization-b/_expected/es.js @@ -6,7 +6,10 @@ function a () { foo(); foo(); - + var a; + if ( a.b ) { + // empty + } } a(); diff --git a/test/form/samples/namespace-optimization-b/_expected/iife.js b/test/form/samples/namespace-optimization-b/_expected/iife.js index e25c3d88152..6c497f5c205 100644 --- a/test/form/samples/namespace-optimization-b/_expected/iife.js +++ b/test/form/samples/namespace-optimization-b/_expected/iife.js @@ -9,7 +9,10 @@ foo(); foo(); - + var a; + if ( a.b ) { + // empty + } } a(); diff --git a/test/form/samples/namespace-optimization-b/_expected/umd.js b/test/form/samples/namespace-optimization-b/_expected/umd.js index e789ad82f7c..7a2c4b0ab6a 100644 --- a/test/form/samples/namespace-optimization-b/_expected/umd.js +++ b/test/form/samples/namespace-optimization-b/_expected/umd.js @@ -12,7 +12,10 @@ foo(); foo(); - + var a; + if ( a.b ) { + // empty + } } a(); diff --git a/test/form/samples/nested-member-access/_config.js b/test/form/samples/nested-member-access/_config.js new file mode 100644 index 00000000000..7c37a494f01 --- /dev/null +++ b/test/form/samples/nested-member-access/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'throw an error when accessing members of null or undefined' +}; diff --git a/test/form/samples/nested-member-access/_expected/amd.js b/test/form/samples/nested-member-access/_expected/amd.js new file mode 100644 index 00000000000..e2891a2991c --- /dev/null +++ b/test/form/samples/nested-member-access/_expected/amd.js @@ -0,0 +1,27 @@ +define(function () { 'use strict'; + + const retained1 = {}; + const retainedResult1 = retained1.foo.bar; + + const retained2 = new function () {}(); + const retainedResult2 = retained2.foo.bar; + + const retained3 = void {}; + const retainedResult3 = retained3.foo; + + let retained4a; + const retained4b = retained4a = undefined; + const retainedResult4 = retained4b.foo; + + const retained5 = 1 + 2; + const retainedResult5 = retained5.foo.bar; + + const retained6 = class {}; + const retainedResult6 = retained6.foo.bar; + + let retained7 = 3; + const retainedResult7 = (retained7++).foo.bar; + + const retained8 = globalVar.x; + +}); diff --git a/test/form/samples/nested-member-access/_expected/cjs.js b/test/form/samples/nested-member-access/_expected/cjs.js new file mode 100644 index 00000000000..dba1bd55247 --- /dev/null +++ b/test/form/samples/nested-member-access/_expected/cjs.js @@ -0,0 +1,25 @@ +'use strict'; + +const retained1 = {}; +const retainedResult1 = retained1.foo.bar; + +const retained2 = new function () {}(); +const retainedResult2 = retained2.foo.bar; + +const retained3 = void {}; +const retainedResult3 = retained3.foo; + +let retained4a; +const retained4b = retained4a = undefined; +const retainedResult4 = retained4b.foo; + +const retained5 = 1 + 2; +const retainedResult5 = retained5.foo.bar; + +const retained6 = class {}; +const retainedResult6 = retained6.foo.bar; + +let retained7 = 3; +const retainedResult7 = (retained7++).foo.bar; + +const retained8 = globalVar.x; diff --git a/test/form/samples/nested-member-access/_expected/es.js b/test/form/samples/nested-member-access/_expected/es.js new file mode 100644 index 00000000000..fb62410e135 --- /dev/null +++ b/test/form/samples/nested-member-access/_expected/es.js @@ -0,0 +1,23 @@ +const retained1 = {}; +const retainedResult1 = retained1.foo.bar; + +const retained2 = new function () {}(); +const retainedResult2 = retained2.foo.bar; + +const retained3 = void {}; +const retainedResult3 = retained3.foo; + +let retained4a; +const retained4b = retained4a = undefined; +const retainedResult4 = retained4b.foo; + +const retained5 = 1 + 2; +const retainedResult5 = retained5.foo.bar; + +const retained6 = class {}; +const retainedResult6 = retained6.foo.bar; + +let retained7 = 3; +const retainedResult7 = (retained7++).foo.bar; + +const retained8 = globalVar.x; diff --git a/test/form/samples/nested-member-access/_expected/iife.js b/test/form/samples/nested-member-access/_expected/iife.js new file mode 100644 index 00000000000..340e3bb0cbb --- /dev/null +++ b/test/form/samples/nested-member-access/_expected/iife.js @@ -0,0 +1,28 @@ +(function () { + 'use strict'; + + const retained1 = {}; + const retainedResult1 = retained1.foo.bar; + + const retained2 = new function () {}(); + const retainedResult2 = retained2.foo.bar; + + const retained3 = void {}; + const retainedResult3 = retained3.foo; + + let retained4a; + const retained4b = retained4a = undefined; + const retainedResult4 = retained4b.foo; + + const retained5 = 1 + 2; + const retainedResult5 = retained5.foo.bar; + + const retained6 = class {}; + const retainedResult6 = retained6.foo.bar; + + let retained7 = 3; + const retainedResult7 = (retained7++).foo.bar; + + const retained8 = globalVar.x; + +}()); diff --git a/test/form/samples/nested-member-access/_expected/umd.js b/test/form/samples/nested-member-access/_expected/umd.js new file mode 100644 index 00000000000..d41e21ee0f3 --- /dev/null +++ b/test/form/samples/nested-member-access/_expected/umd.js @@ -0,0 +1,31 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + const retained1 = {}; + const retainedResult1 = retained1.foo.bar; + + const retained2 = new function () {}(); + const retainedResult2 = retained2.foo.bar; + + const retained3 = void {}; + const retainedResult3 = retained3.foo; + + let retained4a; + const retained4b = retained4a = undefined; + const retainedResult4 = retained4b.foo; + + const retained5 = 1 + 2; + const retainedResult5 = retained5.foo.bar; + + const retained6 = class {}; + const retainedResult6 = retained6.foo.bar; + + let retained7 = 3; + const retainedResult7 = (retained7++).foo.bar; + + const retained8 = globalVar.x; + +}))); diff --git a/test/form/samples/nested-member-access/main.js b/test/form/samples/nested-member-access/main.js new file mode 100644 index 00000000000..0b4418d3c1b --- /dev/null +++ b/test/form/samples/nested-member-access/main.js @@ -0,0 +1,48 @@ +const removed1 = {}; +const removedResult1 = removed1.foo; + +const removed2 = { foo: {} }; +const removedResult2 = removed2.foo.bar; + +const removed3 = new function () {}(); +const removedResult3 = removed3.foo; + +const removed4 = !{}; +const removedResult4 = removed4.foo; + +let removed5a; +const removed5b = removed5a = {}; +const removedResult5 = removed5b.foo; + +const removed6 = 1 + 2; +const removedResult6 = removed6.foo; + +const removed7 = class {}; +const removedResult7 = removed7.foo; + +let removed8 = 3; +const removedResult8 = (removed8++).foo; + +const retained1 = {}; +const retainedResult1 = retained1.foo.bar; + +const retained2 = new function () {}(); +const retainedResult2 = retained2.foo.bar; + +const retained3 = void {}; +const retainedResult3 = retained3.foo; + +let retained4a; +const retained4b = retained4a = undefined; +const retainedResult4 = retained4b.foo; + +const retained5 = 1 + 2; +const retainedResult5 = retained5.foo.bar; + +const retained6 = class {}; +const retainedResult6 = retained6.foo.bar; + +let retained7 = 3; +const retainedResult7 = (retained7++).foo.bar; + +const retained8 = globalVar.x; From a891d8a9e088b389a6a701a39767d3fb243d6899 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Tue, 3 Oct 2017 12:17:50 +0200 Subject: [PATCH 26/76] Remove "hasEffectsAsExpressionStatement" now that global getters will be triggered due to the new "hasEffectsWhenAccessedAtPath" logic. --- src/ast/Node.js | 12 ------------ src/ast/nodes/AssignmentExpression.js | 4 ---- src/ast/nodes/AwaitExpression.js | 4 ---- src/ast/nodes/CallExpression.js | 4 ---- src/ast/nodes/ExpressionStatement.js | 4 ---- src/ast/nodes/Identifier.js | 4 ---- src/ast/nodes/UnaryExpression.js | 4 ---- src/ast/nodes/UpdateExpression.js | 4 ---- src/ast/nodes/YieldExpression.js | 4 ---- src/ast/nodes/shared/ClassNode.js | 4 ---- src/ast/nodes/shared/FunctionNode.js | 4 ---- src/ast/nodes/shared/UndefinedIdentifier.js | 4 ---- src/ast/nodes/shared/pureFunctions.js | 2 +- .../_expected/amd.js | 3 --- .../_expected/cjs.js | 3 --- .../_expected/es.js | 3 --- .../_expected/iife.js | 3 --- .../_expected/umd.js | 3 --- .../side-effects-expressions-as-statements/main.js | 3 --- 19 files changed, 1 insertion(+), 75 deletions(-) diff --git a/src/ast/Node.js b/src/ast/Node.js index 794298dd85c..df70108a3e0 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -67,18 +67,6 @@ export default class Node { || this.someChild( child => child.hasEffects( options ) ); } - /** - * Special make-shift logic to treat cases where apparently side-effect free statements - * are executed for side-effects. The most important case are getters with side-effects. - * Once we can reliably handle this case in member expressions, this function should - * probably be removed again. - * @param {ExecutionPathOptions} options - * @return {boolean} - */ - hasEffectsAsExpressionStatement () { - return true; - } - /** * @param {String[]} path * @param {ExecutionPathOptions} options diff --git a/src/ast/nodes/AssignmentExpression.js b/src/ast/nodes/AssignmentExpression.js index 99882df7da1..6a1f495a5d5 100644 --- a/src/ast/nodes/AssignmentExpression.js +++ b/src/ast/nodes/AssignmentExpression.js @@ -12,10 +12,6 @@ export default class AssignmentExpression extends Node { return super.hasEffects( options ) || this.left.hasEffectsWhenAssignedAtPath( [], options ); } - hasEffectsAsExpressionStatement ( options ) { - return this.hasEffects( options ); - } - hasEffectsWhenAccessedAtPath ( path, options ) { return this.right.hasEffectsWhenAccessedAtPath( path, options ); } diff --git a/src/ast/nodes/AwaitExpression.js b/src/ast/nodes/AwaitExpression.js index ed27f7b6367..f284cd15926 100644 --- a/src/ast/nodes/AwaitExpression.js +++ b/src/ast/nodes/AwaitExpression.js @@ -5,8 +5,4 @@ export default class AwaitExpression extends Node { return super.hasEffects( options ) || !options.ignoreReturnAwaitYield(); } - - hasEffectsAsExpressionStatement ( options ) { - return this.hasEffects( options ); - } } diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index 3e685259505..ad47fda85d7 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -30,8 +30,4 @@ export default class CallExpression extends Node { || this.arguments.some( child => child.hasEffects( options ) ) || this.callee.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions( this.callee ) ); } - - hasEffectsAsExpressionStatement ( options ) { - return this.hasEffects( options ); - } } diff --git a/src/ast/nodes/ExpressionStatement.js b/src/ast/nodes/ExpressionStatement.js index c2486738b29..5e19e87d031 100644 --- a/src/ast/nodes/ExpressionStatement.js +++ b/src/ast/nodes/ExpressionStatement.js @@ -1,10 +1,6 @@ import Statement from './shared/Statement.js'; export default class ExpressionStatement extends Statement { - hasEffects ( options ) { - return super.hasEffects( options ) || this.expression.hasEffectsAsExpressionStatement(options); - } - render ( code, es ) { super.render( code, es ); if ( this.included ) this.insertSemicolon( code ); diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index 4249b3ea01d..820846994fd 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -27,10 +27,6 @@ export default class Identifier extends Node { } } - hasEffectsAsExpressionStatement ( options ) { - return this.hasEffects( options ) || this.variable.isGlobal; - } - hasEffectsWhenAccessedAtPath ( path, options ) { return this.variable && this.variable.hasEffectsWhenAccessedAtPath( path, options ); diff --git a/src/ast/nodes/UnaryExpression.js b/src/ast/nodes/UnaryExpression.js index c8edc0bf702..924afa95fb4 100644 --- a/src/ast/nodes/UnaryExpression.js +++ b/src/ast/nodes/UnaryExpression.js @@ -36,10 +36,6 @@ export default class UnaryExpression extends Node { )); } - hasEffectsAsExpressionStatement ( options ) { - return this.hasEffects( options ); - } - hasEffectsWhenAccessedAtPath ( path ) { if ( this.operator === 'void' ) { return path.length > 0; diff --git a/src/ast/nodes/UpdateExpression.js b/src/ast/nodes/UpdateExpression.js index f5542f2a0b7..248c10dde0e 100644 --- a/src/ast/nodes/UpdateExpression.js +++ b/src/ast/nodes/UpdateExpression.js @@ -19,10 +19,6 @@ export default class UpdateExpression extends Node { || this.argument.hasEffectsWhenAssignedAtPath( [], options ); } - hasEffectsAsExpressionStatement ( options ) { - return this.hasEffects( options ); - } - hasEffectsWhenAccessedAtPath ( path ) { return path.length > 1; } diff --git a/src/ast/nodes/YieldExpression.js b/src/ast/nodes/YieldExpression.js index 99cd5398ea7..e822cb49267 100644 --- a/src/ast/nodes/YieldExpression.js +++ b/src/ast/nodes/YieldExpression.js @@ -5,8 +5,4 @@ export default class YieldExpression extends Node { return super.hasEffects( options ) || !options.ignoreReturnAwaitYield(); } - - hasEffectsAsExpressionStatement ( options ) { - return this.hasEffects( options ); - } } diff --git a/src/ast/nodes/shared/ClassNode.js b/src/ast/nodes/shared/ClassNode.js index 110c1f5bb32..c82e5213b0f 100644 --- a/src/ast/nodes/shared/ClassNode.js +++ b/src/ast/nodes/shared/ClassNode.js @@ -9,10 +9,6 @@ export default class ClassNode extends Node { this.body.bindCallAtPath( path, callOptions ); } - hasEffectsAsExpressionStatement ( options ) { - return this.hasEffects( options ); - } - hasEffectsWhenAccessedAtPath ( path ) { return path.length > 1; } diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index 1a59af1c9a2..e23d3bab4ea 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -20,10 +20,6 @@ export default class FunctionNode extends Node { return this.included || (this.id && this.id.hasEffects( options )); } - hasEffectsAsExpressionStatement ( options ) { - return this.hasEffects( options ); - } - hasEffectsWhenAccessedAtPath ( path, options ) { if ( path.length <= 1 ) { return false; diff --git a/src/ast/nodes/shared/UndefinedIdentifier.js b/src/ast/nodes/shared/UndefinedIdentifier.js index b0f45866659..4a1b616dbb9 100644 --- a/src/ast/nodes/shared/UndefinedIdentifier.js +++ b/src/ast/nodes/shared/UndefinedIdentifier.js @@ -5,10 +5,6 @@ export default class UndefinedIdentifier extends Node { return false; } - hasEffectsAsExpressionStatement () { - return false; - } - hasEffectsWhenAccessedAtPath ( path ) { return path.length > 0; } diff --git a/src/ast/nodes/shared/pureFunctions.js b/src/ast/nodes/shared/pureFunctions.js index 9112ec41972..8f91a16c604 100644 --- a/src/ast/nodes/shared/pureFunctions.js +++ b/src/ast/nodes/shared/pureFunctions.js @@ -15,7 +15,7 @@ simdTypes.forEach( t => { 'Error', 'EvalError', 'InternalError', 'RangeError', 'ReferenceError', 'SyntaxError', 'TypeError', 'URIError', 'isFinite', 'isNaN', 'parseFloat', 'parseInt', 'decodeURI', 'decodeURIComponent', 'encodeURI', 'encodeURIComponent', 'escape', 'unescape', 'Object', 'Object.create', 'Object.getNotifier', 'Object.getOwn', 'Object.getOwnPropertyDescriptor', 'Object.getOwnPropertyNames', 'Object.getOwnPropertySymbols', 'Object.getPrototypeOf', 'Object.is', 'Object.isExtensible', 'Object.isFrozen', 'Object.isSealed', 'Object.keys', - 'Function', 'Boolean', + 'Boolean', 'Number', 'Number.isFinite', 'Number.isInteger', 'Number.isNaN', 'Number.isSafeInteger', 'Number.parseFloat', 'Number.parseInt', 'Symbol', 'Symbol.for', 'Symbol.keyFor', 'Math.abs', 'Math.acos', 'Math.acosh', 'Math.asin', 'Math.asinh', 'Math.atan', 'Math.atan2', 'Math.atanh', 'Math.cbrt', 'Math.ceil', 'Math.clz32', 'Math.cos', 'Math.cosh', 'Math.exp', 'Math.expm1', 'Math.floor', 'Math.fround', 'Math.hypot', 'Math.imul', 'Math.log', 'Math.log10', 'Math.log1p', 'Math.log2', 'Math.max', 'Math.min', 'Math.pow', 'Math.random', 'Math.round', 'Math.sign', 'Math.sin', 'Math.sinh', 'Math.sqrt', 'Math.tan', 'Math.tanh', 'Math.trunc', diff --git a/test/form/samples/side-effects-expressions-as-statements/_expected/amd.js b/test/form/samples/side-effects-expressions-as-statements/_expected/amd.js index 602eaaee59e..97343e340d5 100644 --- a/test/form/samples/side-effects-expressions-as-statements/_expected/amd.js +++ b/test/form/samples/side-effects-expressions-as-statements/_expected/amd.js @@ -1,8 +1,5 @@ define(function () { 'use strict'; - // Use strict has an effect - 'use strict'; - // Access getters with side-effects to e.g. force DOM repaints globalVar.getter; globalVar && globalVar.member && globalVar.member.getter; diff --git a/test/form/samples/side-effects-expressions-as-statements/_expected/cjs.js b/test/form/samples/side-effects-expressions-as-statements/_expected/cjs.js index b8cb1664003..98b353fa085 100644 --- a/test/form/samples/side-effects-expressions-as-statements/_expected/cjs.js +++ b/test/form/samples/side-effects-expressions-as-statements/_expected/cjs.js @@ -1,8 +1,5 @@ 'use strict'; -// Use strict has an effect -'use strict'; - // Access getters with side-effects to e.g. force DOM repaints globalVar.getter; globalVar && globalVar.member && globalVar.member.getter; diff --git a/test/form/samples/side-effects-expressions-as-statements/_expected/es.js b/test/form/samples/side-effects-expressions-as-statements/_expected/es.js index be1099bff35..86f9cf0f080 100644 --- a/test/form/samples/side-effects-expressions-as-statements/_expected/es.js +++ b/test/form/samples/side-effects-expressions-as-statements/_expected/es.js @@ -1,6 +1,3 @@ -// Use strict has an effect -'use strict'; - // Access getters with side-effects to e.g. force DOM repaints globalVar.getter; globalVar && globalVar.member && globalVar.member.getter; diff --git a/test/form/samples/side-effects-expressions-as-statements/_expected/iife.js b/test/form/samples/side-effects-expressions-as-statements/_expected/iife.js index 0ae3c63b570..18a0f833c9a 100644 --- a/test/form/samples/side-effects-expressions-as-statements/_expected/iife.js +++ b/test/form/samples/side-effects-expressions-as-statements/_expected/iife.js @@ -1,9 +1,6 @@ (function () { 'use strict'; - // Use strict has an effect - 'use strict'; - // Access getters with side-effects to e.g. force DOM repaints globalVar.getter; globalVar && globalVar.member && globalVar.member.getter; diff --git a/test/form/samples/side-effects-expressions-as-statements/_expected/umd.js b/test/form/samples/side-effects-expressions-as-statements/_expected/umd.js index f6f96057500..0b1472f25ef 100644 --- a/test/form/samples/side-effects-expressions-as-statements/_expected/umd.js +++ b/test/form/samples/side-effects-expressions-as-statements/_expected/umd.js @@ -4,9 +4,6 @@ (factory()); }(this, (function () { 'use strict'; - // Use strict has an effect - 'use strict'; - // Access getters with side-effects to e.g. force DOM repaints globalVar.getter; globalVar && globalVar.member && globalVar.member.getter; diff --git a/test/form/samples/side-effects-expressions-as-statements/main.js b/test/form/samples/side-effects-expressions-as-statements/main.js index 6f939e1618a..3feecf7f6b2 100644 --- a/test/form/samples/side-effects-expressions-as-statements/main.js +++ b/test/form/samples/side-effects-expressions-as-statements/main.js @@ -1,6 +1,3 @@ -// Use strict has an effect -'use strict'; - // Access getters with side-effects to e.g. force DOM repaints globalVar.getter; globalVar && globalVar.member && globalVar.member.getter; From 1bbb5c112a0895a972ee83646f8738d1f6869c52 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Tue, 3 Oct 2017 21:56:28 +0200 Subject: [PATCH 27/76] Add proper support for getters in object expressions. --- src/ast/nodes/MemberExpression.js | 3 ++- src/ast/nodes/ObjectExpression.js | 24 +++++++++++-------- src/ast/nodes/Property.js | 10 ++++++++ .../_config.js | 3 +++ .../_expected/amd.js | 14 +++++++++++ .../_expected/cjs.js | 12 ++++++++++ .../_expected/es.js | 10 ++++++++ .../_expected/iife.js | 15 ++++++++++++ .../_expected/umd.js | 18 ++++++++++++++ .../side-effects-getters-and-setters/main.js | 24 +++++++++++++++++++ 10 files changed, 122 insertions(+), 11 deletions(-) create mode 100644 test/form/samples/side-effects-getters-and-setters/_config.js create mode 100644 test/form/samples/side-effects-getters-and-setters/_expected/amd.js create mode 100644 test/form/samples/side-effects-getters-and-setters/_expected/cjs.js create mode 100644 test/form/samples/side-effects-getters-and-setters/_expected/es.js create mode 100644 test/form/samples/side-effects-getters-and-setters/_expected/iife.js create mode 100644 test/form/samples/side-effects-getters-and-setters/_expected/umd.js create mode 100644 test/form/samples/side-effects-getters-and-setters/main.js diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index 30d6e00319c..68590d6b1b7 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -112,7 +112,8 @@ export default class MemberExpression extends Node { if ( this.variable ) { return this.variable.hasEffectsWhenAccessedAtPath( path, options ); } - return this.object.hasEffectsWhenAccessedAtPath( [ this.computed ? UNKNOWN_KEY : this.property.name, ...path ], options ); + return this.property.hasEffectsWhenAccessedAtPath( path, options ) + || this.object.hasEffectsWhenAccessedAtPath( [ this.computed ? UNKNOWN_KEY : this.property.name, ...path ], options ); } hasEffectsWhenAssignedAtPath ( path, options ) { diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index 7cf9f525f86..66db86dace5 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -1,12 +1,15 @@ import Node from '../Node.js'; import { UNKNOWN_KEY } from '../variables/DeepSet'; +const PROPERTY_KINDS_READ = [ 'init', 'get' ]; +const PROPERTY_KINDS_WRITE = [ 'init', 'set' ]; + export default class ObjectExpression extends Node { bindAssignmentAtPath ( path, expression ) { if ( path.length === 0 ) { return; } - this._getPossiblePropertiesWithName( path[ 0 ] ).properties.forEach( property => + this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_WRITE ).properties.forEach( property => property.bindAssignmentAtPath( path.slice( 1 ), expression ) ); } @@ -14,11 +17,11 @@ export default class ObjectExpression extends Node { if ( path.length === 0 ) { return; } - this._getPossiblePropertiesWithName( path[ 0 ] ).properties.forEach( property => + this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_READ ).properties.forEach( property => property.bindCallAtPath( path.slice( 1 ), callOptions ) ); } - _getPossiblePropertiesWithName ( name ) { + _getPossiblePropertiesWithName ( name, kinds ) { if ( name === UNKNOWN_KEY ) { return { properties: this.properties, hasCertainHit: false }; } @@ -27,6 +30,7 @@ export default class ObjectExpression extends Node { for ( let index = this.properties.length - 1; index >= 0; index-- ) { const property = this.properties[ index ]; + if ( kinds.indexOf( property.kind ) < 0 ) continue; if ( property.computed ) { properties.push( property ); } else if ( property.key.name === name ) { @@ -39,20 +43,20 @@ export default class ObjectExpression extends Node { } hasEffectsWhenAccessedAtPath ( path, options ) { - if ( path.length <= 1 ) { + if ( path.length === 0 ) { return false; } - const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ] ); + const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_READ ); - return !hasCertainHit || properties.some( property => - property.hasEffectsWhenAccessedAtPath( path.slice( 1 ), options ) ); + return (path.length > 1 && !hasCertainHit) + || properties.some( property => property.hasEffectsWhenAccessedAtPath( path.slice( 1 ), options ) ); } hasEffectsWhenAssignedAtPath ( path, options ) { if ( path.length <= 1 ) { return false; } - const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ] ); + const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_WRITE ); return !hasCertainHit || properties.some( property => property.hasEffectsWhenAssignedAtPath( path.slice( 1 ), options ) ); @@ -62,7 +66,7 @@ export default class ObjectExpression extends Node { if ( path.length === 0 ) { return true; } - const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ] ); + const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_READ ); return !hasCertainHit || properties.some( property => property.hasEffectsWhenCalledAtPath( path.slice( 1 ), options ) ); @@ -72,7 +76,7 @@ export default class ObjectExpression extends Node { if ( path.length === 0 ) { return false; } - const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ] ); + const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_READ ); return !hasCertainHit || properties.some( property => property.hasEffectsWhenMutatedAtPath( path.slice( 1 ), options ) ); diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index 2785f7b7c3f..e15f005fbb0 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -10,7 +10,17 @@ export default class Property extends Node { this.value.bindCallAtPath( path, callOptions ); } + hasEffects ( options ) { + return this.included + || this.key.hasEffects( options ) + || this.value.hasEffects( options ); + } + hasEffectsWhenAccessedAtPath ( path, options ) { + if ( this.kind === 'get' ) { + return path.length > 0 + || this.value.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions( this.value ) ); + } return this.value.hasEffectsWhenAccessedAtPath( path, options ); } diff --git a/test/form/samples/side-effects-getters-and-setters/_config.js b/test/form/samples/side-effects-getters-and-setters/_config.js new file mode 100644 index 00000000000..2ac2ab26753 --- /dev/null +++ b/test/form/samples/side-effects-getters-and-setters/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'treat getters and setters as function calls' +}; diff --git a/test/form/samples/side-effects-getters-and-setters/_expected/amd.js b/test/form/samples/side-effects-getters-and-setters/_expected/amd.js new file mode 100644 index 00000000000..a9920713ff3 --- /dev/null +++ b/test/form/samples/side-effects-getters-and-setters/_expected/amd.js @@ -0,0 +1,14 @@ +define(function () { 'use strict'; + + const retained1a = { + get effect () { + console.log( 'effect' ); + }, + get noEffect () { + + } + }; + + const retained1b = retained1a.effect; + +}); diff --git a/test/form/samples/side-effects-getters-and-setters/_expected/cjs.js b/test/form/samples/side-effects-getters-and-setters/_expected/cjs.js new file mode 100644 index 00000000000..77b7624ca21 --- /dev/null +++ b/test/form/samples/side-effects-getters-and-setters/_expected/cjs.js @@ -0,0 +1,12 @@ +'use strict'; + +const retained1a = { + get effect () { + console.log( 'effect' ); + }, + get noEffect () { + + } +}; + +const retained1b = retained1a.effect; diff --git a/test/form/samples/side-effects-getters-and-setters/_expected/es.js b/test/form/samples/side-effects-getters-and-setters/_expected/es.js new file mode 100644 index 00000000000..ec9d17a0151 --- /dev/null +++ b/test/form/samples/side-effects-getters-and-setters/_expected/es.js @@ -0,0 +1,10 @@ +const retained1a = { + get effect () { + console.log( 'effect' ); + }, + get noEffect () { + + } +}; + +const retained1b = retained1a.effect; diff --git a/test/form/samples/side-effects-getters-and-setters/_expected/iife.js b/test/form/samples/side-effects-getters-and-setters/_expected/iife.js new file mode 100644 index 00000000000..549c13e6075 --- /dev/null +++ b/test/form/samples/side-effects-getters-and-setters/_expected/iife.js @@ -0,0 +1,15 @@ +(function () { + 'use strict'; + + const retained1a = { + get effect () { + console.log( 'effect' ); + }, + get noEffect () { + + } + }; + + const retained1b = retained1a.effect; + +}()); diff --git a/test/form/samples/side-effects-getters-and-setters/_expected/umd.js b/test/form/samples/side-effects-getters-and-setters/_expected/umd.js new file mode 100644 index 00000000000..cc42c055fd4 --- /dev/null +++ b/test/form/samples/side-effects-getters-and-setters/_expected/umd.js @@ -0,0 +1,18 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + const retained1a = { + get effect () { + console.log( 'effect' ); + }, + get noEffect () { + + } + }; + + const retained1b = retained1a.effect; + +}))); diff --git a/test/form/samples/side-effects-getters-and-setters/main.js b/test/form/samples/side-effects-getters-and-setters/main.js new file mode 100644 index 00000000000..2ef6146707c --- /dev/null +++ b/test/form/samples/side-effects-getters-and-setters/main.js @@ -0,0 +1,24 @@ +const retained1a = { + get effect () { + console.log( 'effect' ); + }, + get noEffect () { + const x = 1; + } +}; + +const removed1 = retained1a.noEffect; +const retained1b = retained1a.effect; + +const removed2a = { + get shadowedEffect () { + console.log( 'effect' ); + }, + shadowedEffect: true, + set shadowedEffect ( value ) { + console.log( 'effect' ); + } +}; + +const removed2b = removed2a.shadowedEffect; +const removed2c = removed2a.missingProp; From bb59e9d99a8e1b8a8aba4496b5e6973323881405 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Wed, 4 Oct 2017 07:11:57 +0200 Subject: [PATCH 28/76] Add proper support for setters in object expressions. --- src/ast/nodes/MemberExpression.js | 8 +--- src/ast/nodes/ObjectExpression.js | 7 ++-- src/ast/nodes/Property.js | 10 +++++ .../_expected/amd.js | 24 +++++++++++ .../_expected/cjs.js | 24 +++++++++++ .../_expected/es.js | 24 +++++++++++ .../_expected/iife.js | 24 +++++++++++ .../_expected/umd.js | 24 +++++++++++ .../side-effects-getters-and-setters/main.js | 42 +++++++++++++++++++ 9 files changed, 178 insertions(+), 9 deletions(-) diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index 68590d6b1b7..bfa2d7460e9 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -112,18 +112,14 @@ export default class MemberExpression extends Node { if ( this.variable ) { return this.variable.hasEffectsWhenAccessedAtPath( path, options ); } - return this.property.hasEffectsWhenAccessedAtPath( path, options ) - || this.object.hasEffectsWhenAccessedAtPath( [ this.computed ? UNKNOWN_KEY : this.property.name, ...path ], options ); + return this.object.hasEffectsWhenAccessedAtPath( [ this.computed ? UNKNOWN_KEY : this.property.name, ...path ], options ); } hasEffectsWhenAssignedAtPath ( path, options ) { if ( this.variable ) { return this.variable.hasEffectsWhenAssignedAtPath( path, options ); } - if ( this.computed ) { - return path.length > 0 || this.object.hasEffectsWhenMutatedAtPath( [], options ); - } - return this.object.hasEffectsWhenAssignedAtPath( [ this.property.name, ...path ], options ); + return this.object.hasEffectsWhenAssignedAtPath( [ this.computed ? UNKNOWN_KEY : this.property.name, ...path ], options ); } hasEffectsWhenCalledAtPath ( path, options ) { diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index 66db86dace5..06e67368038 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -53,13 +53,14 @@ export default class ObjectExpression extends Node { } hasEffectsWhenAssignedAtPath ( path, options ) { - if ( path.length <= 1 ) { + if ( path.length === 0 ) { return false; } const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_WRITE ); - return !hasCertainHit || properties.some( property => - property.hasEffectsWhenAssignedAtPath( path.slice( 1 ), options ) ); + return (path.length > 1 && !hasCertainHit) + || properties.some( property => (path.length > 1 || property.kind === 'set') + && property.hasEffectsWhenAssignedAtPath( path.slice( 1 ), options ) ); } hasEffectsWhenCalledAtPath ( path, options ) { diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index e15f005fbb0..42dfd07bd8f 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -25,14 +25,24 @@ export default class Property extends Node { } hasEffectsWhenAssignedAtPath ( path, options ) { + if ( this.kind === 'set' ) { + return path.length > 0 + || this.value.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions( this.value ) ); + } return this.value.hasEffectsWhenAssignedAtPath( path, options ); } hasEffectsWhenCalledAtPath ( path, options ) { + if (this.kind === 'get') { + return true; + } return this.value.hasEffectsWhenCalledAtPath( path, options ); } hasEffectsWhenMutatedAtPath ( path, options ) { + if (this.kind === 'get') { + return true; + } return this.value.hasEffectsWhenMutatedAtPath( path, options ); } diff --git a/test/form/samples/side-effects-getters-and-setters/_expected/amd.js b/test/form/samples/side-effects-getters-and-setters/_expected/amd.js index a9920713ff3..dc9afd5b113 100644 --- a/test/form/samples/side-effects-getters-and-setters/_expected/amd.js +++ b/test/form/samples/side-effects-getters-and-setters/_expected/amd.js @@ -10,5 +10,29 @@ define(function () { 'use strict'; }; const retained1b = retained1a.effect; + const retained1c = retained1a[ 'eff' + 'ect' ]; + + const retained3 = { + set effect ( value ) { + console.log( value ); + } + }; + + retained3.effect = 'retained'; + + const retained4 = { + set effect ( value ) { + console.log( value ); + } + }; + + retained4[ 'eff' + 'ect' ] = 'retained'; + + const retained7 = { + foo: () => {}, + get foo () {} + }; + + retained7.foo(); }); diff --git a/test/form/samples/side-effects-getters-and-setters/_expected/cjs.js b/test/form/samples/side-effects-getters-and-setters/_expected/cjs.js index 77b7624ca21..0fd4b7afdd9 100644 --- a/test/form/samples/side-effects-getters-and-setters/_expected/cjs.js +++ b/test/form/samples/side-effects-getters-and-setters/_expected/cjs.js @@ -10,3 +10,27 @@ const retained1a = { }; const retained1b = retained1a.effect; +const retained1c = retained1a[ 'eff' + 'ect' ]; + +const retained3 = { + set effect ( value ) { + console.log( value ); + } +}; + +retained3.effect = 'retained'; + +const retained4 = { + set effect ( value ) { + console.log( value ); + } +}; + +retained4[ 'eff' + 'ect' ] = 'retained'; + +const retained7 = { + foo: () => {}, + get foo () {} +}; + +retained7.foo(); diff --git a/test/form/samples/side-effects-getters-and-setters/_expected/es.js b/test/form/samples/side-effects-getters-and-setters/_expected/es.js index ec9d17a0151..e9ffdee2609 100644 --- a/test/form/samples/side-effects-getters-and-setters/_expected/es.js +++ b/test/form/samples/side-effects-getters-and-setters/_expected/es.js @@ -8,3 +8,27 @@ const retained1a = { }; const retained1b = retained1a.effect; +const retained1c = retained1a[ 'eff' + 'ect' ]; + +const retained3 = { + set effect ( value ) { + console.log( value ); + } +}; + +retained3.effect = 'retained'; + +const retained4 = { + set effect ( value ) { + console.log( value ); + } +}; + +retained4[ 'eff' + 'ect' ] = 'retained'; + +const retained7 = { + foo: () => {}, + get foo () {} +}; + +retained7.foo(); diff --git a/test/form/samples/side-effects-getters-and-setters/_expected/iife.js b/test/form/samples/side-effects-getters-and-setters/_expected/iife.js index 549c13e6075..fba3f59051d 100644 --- a/test/form/samples/side-effects-getters-and-setters/_expected/iife.js +++ b/test/form/samples/side-effects-getters-and-setters/_expected/iife.js @@ -11,5 +11,29 @@ }; const retained1b = retained1a.effect; + const retained1c = retained1a[ 'eff' + 'ect' ]; + + const retained3 = { + set effect ( value ) { + console.log( value ); + } + }; + + retained3.effect = 'retained'; + + const retained4 = { + set effect ( value ) { + console.log( value ); + } + }; + + retained4[ 'eff' + 'ect' ] = 'retained'; + + const retained7 = { + foo: () => {}, + get foo () {} + }; + + retained7.foo(); }()); diff --git a/test/form/samples/side-effects-getters-and-setters/_expected/umd.js b/test/form/samples/side-effects-getters-and-setters/_expected/umd.js index cc42c055fd4..1f5631453f0 100644 --- a/test/form/samples/side-effects-getters-and-setters/_expected/umd.js +++ b/test/form/samples/side-effects-getters-and-setters/_expected/umd.js @@ -14,5 +14,29 @@ }; const retained1b = retained1a.effect; + const retained1c = retained1a[ 'eff' + 'ect' ]; + + const retained3 = { + set effect ( value ) { + console.log( value ); + } + }; + + retained3.effect = 'retained'; + + const retained4 = { + set effect ( value ) { + console.log( value ); + } + }; + + retained4[ 'eff' + 'ect' ] = 'retained'; + + const retained7 = { + foo: () => {}, + get foo () {} + }; + + retained7.foo(); }))); diff --git a/test/form/samples/side-effects-getters-and-setters/main.js b/test/form/samples/side-effects-getters-and-setters/main.js index 2ef6146707c..b394f37b2b6 100644 --- a/test/form/samples/side-effects-getters-and-setters/main.js +++ b/test/form/samples/side-effects-getters-and-setters/main.js @@ -9,6 +9,7 @@ const retained1a = { const removed1 = retained1a.noEffect; const retained1b = retained1a.effect; +const retained1c = retained1a[ 'eff' + 'ect' ]; const removed2a = { get shadowedEffect () { @@ -22,3 +23,44 @@ const removed2a = { const removed2b = removed2a.shadowedEffect; const removed2c = removed2a.missingProp; + +const retained3 = { + set effect ( value ) { + console.log( value ); + } +}; + +retained3.effect = 'retained'; + +const retained4 = { + set effect ( value ) { + console.log( value ); + } +}; + +retained4[ 'eff' + 'ect' ] = 'retained'; + +const removed5 = { + set noEffect ( value ) { + const x = value; + } +}; + +removed5.noEffect = 'removed'; + +const removed6 = { + set shadowedEffect ( value ) { + console.log( 'effect' ); + }, + shadowedEffect: true +}; + +removed6.shadowedEffect = true; +removed6.missingProp = true; + +const retained7 = { + foo: () => {}, + get foo () {} +}; + +retained7.foo(); From 983c0898f2e584b465170c6cb7a50db61d9ab9dc Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Fri, 6 Oct 2017 06:56:22 +0200 Subject: [PATCH 29/76] Rename DeepSet -> StructuredAssignmentTracker --- src/ast/nodes/MemberExpression.js | 2 +- src/ast/nodes/ObjectExpression.js | 2 +- src/ast/variables/LocalVariable.js | 8 ++++---- .../{DeepSet.js => StructuredAssignmentTracker.js} | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) rename src/ast/variables/{DeepSet.js => StructuredAssignmentTracker.js} (94%) diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index bfa2d7460e9..3d08c6f3766 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -1,7 +1,7 @@ import relativeId from '../../utils/relativeId.js'; import Node from '../Node.js'; import isReference from 'is-reference'; -import { UNKNOWN_KEY } from '../variables/DeepSet'; +import { UNKNOWN_KEY } from '../variables/StructuredAssignmentTracker'; const validProp = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index 06e67368038..ea76314d7f4 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -1,5 +1,5 @@ import Node from '../Node.js'; -import { UNKNOWN_KEY } from '../variables/DeepSet'; +import { UNKNOWN_KEY } from '../variables/StructuredAssignmentTracker'; const PROPERTY_KINDS_READ = [ 'init', 'get' ]; const PROPERTY_KINDS_WRITE = [ 'init', 'set' ]; diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index a3f94a69820..a7b11a405ab 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -1,7 +1,7 @@ import Variable from './Variable'; -import DeepSet from './DeepSet'; +import StructuredAssignmentTracker from './StructuredAssignmentTracker'; -// To avoid recursions +// To avoid infinite recursions const MAX_PATH_LENGTH = 8; export default class LocalVariable extends Variable { @@ -10,9 +10,9 @@ export default class LocalVariable extends Variable { this.isReassigned = false; this.exportName = null; this.declarations = new Set( declarator ? [ declarator ] : null ); - this.assignedExpressions = new DeepSet(); + this.assignedExpressions = new StructuredAssignmentTracker(); init && this.assignedExpressions.addAtPath( [], init ); - this.calls = new DeepSet(); + this.calls = new StructuredAssignmentTracker(); } addDeclaration ( identifier ) { diff --git a/src/ast/variables/DeepSet.js b/src/ast/variables/StructuredAssignmentTracker.js similarity index 94% rename from src/ast/variables/DeepSet.js rename to src/ast/variables/StructuredAssignmentTracker.js index 04761cedc5c..4336dddc0c7 100644 --- a/src/ast/variables/DeepSet.js +++ b/src/ast/variables/StructuredAssignmentTracker.js @@ -1,7 +1,7 @@ const SET_KEY = { type: 'SET_KEY' }; export const UNKNOWN_KEY = { type: 'UNKNOWN_KEY' }; -export default class DeepSet { +export default class StructuredAssignmentTracker { constructor () { this._assignments = new Map( [ [ SET_KEY, new Set() ] ] ); } @@ -12,7 +12,7 @@ export default class DeepSet { } else { const [ nextPath, ...remainingPath ] = path; if ( !this._assignments.has( nextPath ) ) { - this._assignments.set( nextPath, new DeepSet() ); + this._assignments.set( nextPath, new StructuredAssignmentTracker() ); } this._assignments.get( nextPath ).addAtPath( remainingPath, assignment ); } From 96d48b3507611ca190c76da0bd1060120d3bcfca Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Fri, 6 Oct 2017 09:59:05 +0200 Subject: [PATCH 30/76] Properly handle path access and calls in conditional expressions --- src/ast/nodes/ConditionalExpression.js | 87 ++++++++++++++++--- src/ast/nodes/MemberExpression.js | 7 +- .../conditional-expression-paths/_config.js | 3 + .../_expected/amd.js | 22 +++++ .../_expected/cjs.js | 20 +++++ .../_expected/es.js | 18 ++++ .../_expected/iife.js | 23 +++++ .../_expected/umd.js | 26 ++++++ .../conditional-expression-paths/main.js | 39 +++++++++ .../conditional-expression/_expected/amd.js | 5 ++ .../conditional-expression/_expected/cjs.js | 5 ++ .../conditional-expression/_expected/es.js | 5 ++ .../conditional-expression/_expected/iife.js | 5 ++ .../conditional-expression/_expected/umd.js | 5 ++ .../samples/conditional-expression/main.js | 11 ++- 15 files changed, 262 insertions(+), 19 deletions(-) create mode 100644 test/form/samples/conditional-expression-paths/_config.js create mode 100644 test/form/samples/conditional-expression-paths/_expected/amd.js create mode 100644 test/form/samples/conditional-expression-paths/_expected/cjs.js create mode 100644 test/form/samples/conditional-expression-paths/_expected/es.js create mode 100644 test/form/samples/conditional-expression-paths/_expected/iife.js create mode 100644 test/form/samples/conditional-expression-paths/_expected/umd.js create mode 100644 test/form/samples/conditional-expression-paths/main.js diff --git a/src/ast/nodes/ConditionalExpression.js b/src/ast/nodes/ConditionalExpression.js index 99368e2bcde..ff3ca1af4a0 100644 --- a/src/ast/nodes/ConditionalExpression.js +++ b/src/ast/nodes/ConditionalExpression.js @@ -2,21 +2,25 @@ import Node from '../Node.js'; import { UNKNOWN_VALUE } from '../values.js'; export default class ConditionalExpression extends Node { - initialiseChildren ( parentScope ) { - if ( this.module.bundle.treeshake ) { - this.testValue = this.test.getValue(); + bindAssignmentAtPath ( path, expression ) { + if ( this.testValue === UNKNOWN_VALUE ) { + this.consequent.bindAssignmentAtPath( path, expression ); + this.alternate.bindAssignmentAtPath( path, expression ); + } else { + this.testValue + ? this.consequent.bindAssignmentAtPath( path, expression ) + : this.alternate.bindAssignmentAtPath( path, expression ); + } + } - if ( this.testValue === UNKNOWN_VALUE ) { - super.initialiseChildren( parentScope ); - } else if ( this.testValue ) { - this.consequent.initialise( this.scope ); - this.alternate = null; - } else if ( this.alternate ) { - this.alternate.initialise( this.scope ); - this.consequent = null; - } + bindCallAtPath ( path, callOptions ) { + if ( this.testValue === UNKNOWN_VALUE ) { + this.consequent.bindCallAtPath( path, callOptions ); + this.alternate.bindCallAtPath( path, callOptions ); } else { - super.initialiseChildren( parentScope ); + this.testValue + ? this.consequent.bindCallAtPath( path, callOptions ) + : this.alternate.bindCallAtPath( path, callOptions ); } } @@ -36,6 +40,63 @@ export default class ConditionalExpression extends Node { ); } + hasEffectsWhenAccessedAtPath ( path, options ) { + return ( + this.testValue === UNKNOWN_VALUE && ( + this.consequent.hasEffectsWhenAccessedAtPath( path, options ) + || this.alternate.hasEffectsWhenAccessedAtPath( path, options ) + ) + ) || ( + this.testValue + ? this.consequent.hasEffectsWhenAccessedAtPath( path, options ) + : this.alternate.hasEffectsWhenAccessedAtPath( path, options ) + ); + } + + hasEffectsWhenAssignedAtPath ( path, options ) { + return ( + this.testValue === UNKNOWN_VALUE && ( + this.consequent.hasEffectsWhenAssignedAtPath( path, options ) + || this.alternate.hasEffectsWhenAssignedAtPath( path, options ) + ) + ) || ( + this.testValue + ? this.consequent.hasEffectsWhenAssignedAtPath( path, options ) + : this.alternate.hasEffectsWhenAssignedAtPath( path, options ) + ); + } + + hasEffectsWhenCalledAtPath ( path, options ) { + return ( + this.testValue === UNKNOWN_VALUE && ( + this.consequent.hasEffectsWhenCalledAtPath( path, options ) + || this.alternate.hasEffectsWhenCalledAtPath( path, options ) + ) + ) || ( + this.testValue + ? this.consequent.hasEffectsWhenCalledAtPath( path, options ) + : this.alternate.hasEffectsWhenCalledAtPath( path, options ) + ); + } + + initialiseChildren ( parentScope ) { + if ( this.module.bundle.treeshake ) { + this.testValue = this.test.getValue(); + + if ( this.testValue === UNKNOWN_VALUE ) { + super.initialiseChildren( parentScope ); + } else if ( this.testValue ) { + this.consequent.initialise( this.scope ); + this.alternate = null; + } else if ( this.alternate ) { + this.alternate.initialise( this.scope ); + this.consequent = null; + } + } else { + super.initialiseChildren( parentScope ); + } + } + render ( code, es ) { if ( !this.module.bundle.treeshake ) { super.render( code, es ); diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index 3d08c6f3766..0c044927bf3 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -1,6 +1,5 @@ import relativeId from '../../utils/relativeId.js'; import Node from '../Node.js'; -import isReference from 'is-reference'; import { UNKNOWN_KEY } from '../variables/StructuredAssignmentTracker'; const validProp = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; @@ -126,10 +125,8 @@ export default class MemberExpression extends Node { if ( this.variable ) { return this.variable.hasEffectsWhenCalledAtPath( path, options ); } - if ( !isReference( this ) || this.computed ) { - return true; - } - return this.object.hasEffectsWhenCalledAtPath( [ this.property.name, ...path ], options ); + return this.computed + || this.object.hasEffectsWhenCalledAtPath( [ this.property.name, ...path ], options ); } hasEffectsWhenMutatedAtPath ( path, options ) { diff --git a/test/form/samples/conditional-expression-paths/_config.js b/test/form/samples/conditional-expression-paths/_config.js new file mode 100644 index 00000000000..aadb52f877d --- /dev/null +++ b/test/form/samples/conditional-expression-paths/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'only retain branches with side-effects' +}; diff --git a/test/form/samples/conditional-expression-paths/_expected/amd.js b/test/form/samples/conditional-expression-paths/_expected/amd.js new file mode 100644 index 00000000000..d9eda46ad22 --- /dev/null +++ b/test/form/samples/conditional-expression-paths/_expected/amd.js @@ -0,0 +1,22 @@ +define(function () { 'use strict'; + + var unknownValue = globalFunction(); + + // unknown branch with side-effect + 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(); + + var bar4 = { x: () => console.log( 'effect' ) }; + var a4 = (bar4).y.z; + var b4 = (bar4).y.z; + var c4 = (bar4).x(); + var d4 = (bar4).x(); + var e4 = (bar4).y.z = 1; + var f4 = (bar4).y.z = 1; + +}); diff --git a/test/form/samples/conditional-expression-paths/_expected/cjs.js b/test/form/samples/conditional-expression-paths/_expected/cjs.js new file mode 100644 index 00000000000..9c6e7fb0c25 --- /dev/null +++ b/test/form/samples/conditional-expression-paths/_expected/cjs.js @@ -0,0 +1,20 @@ +'use strict'; + +var unknownValue = globalFunction(); + +// unknown branch with side-effect +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(); + +var bar4 = { x: () => console.log( 'effect' ) }; +var a4 = (bar4).y.z; +var b4 = (bar4).y.z; +var c4 = (bar4).x(); +var d4 = (bar4).x(); +var e4 = (bar4).y.z = 1; +var f4 = (bar4).y.z = 1; diff --git a/test/form/samples/conditional-expression-paths/_expected/es.js b/test/form/samples/conditional-expression-paths/_expected/es.js new file mode 100644 index 00000000000..2863cebd2b0 --- /dev/null +++ b/test/form/samples/conditional-expression-paths/_expected/es.js @@ -0,0 +1,18 @@ +var unknownValue = globalFunction(); + +// unknown branch with side-effect +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(); + +var bar4 = { x: () => console.log( 'effect' ) }; +var a4 = (bar4).y.z; +var b4 = (bar4).y.z; +var c4 = (bar4).x(); +var d4 = (bar4).x(); +var e4 = (bar4).y.z = 1; +var f4 = (bar4).y.z = 1; diff --git a/test/form/samples/conditional-expression-paths/_expected/iife.js b/test/form/samples/conditional-expression-paths/_expected/iife.js new file mode 100644 index 00000000000..d82e880c3de --- /dev/null +++ b/test/form/samples/conditional-expression-paths/_expected/iife.js @@ -0,0 +1,23 @@ +(function () { + 'use strict'; + + var unknownValue = globalFunction(); + + // unknown branch with side-effect + 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(); + + var bar4 = { x: () => console.log( 'effect' ) }; + var a4 = (bar4).y.z; + var b4 = (bar4).y.z; + var c4 = (bar4).x(); + var d4 = (bar4).x(); + var e4 = (bar4).y.z = 1; + var f4 = (bar4).y.z = 1; + +}()); diff --git a/test/form/samples/conditional-expression-paths/_expected/umd.js b/test/form/samples/conditional-expression-paths/_expected/umd.js new file mode 100644 index 00000000000..c5d7483058c --- /dev/null +++ b/test/form/samples/conditional-expression-paths/_expected/umd.js @@ -0,0 +1,26 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + var unknownValue = globalFunction(); + + // unknown branch with side-effect + 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(); + + var bar4 = { x: () => console.log( 'effect' ) }; + var a4 = (bar4).y.z; + var b4 = (bar4).y.z; + var c4 = (bar4).x(); + var d4 = (bar4).x(); + var e4 = (bar4).y.z = 1; + var f4 = (bar4).y.z = 1; + +}))); diff --git a/test/form/samples/conditional-expression-paths/main.js b/test/form/samples/conditional-expression-paths/main.js new file mode 100644 index 00000000000..ef824ad3f96 --- /dev/null +++ b/test/form/samples/conditional-expression-paths/main.js @@ -0,0 +1,39 @@ +var unknownValue = globalFunction(); + +// unknown branch without side-effects +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(); + +// unknown branch with side-effect +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(); + +// no side-effects +var foo3 = { x: () => {}, y: {} }; +var bar3 = { x: () => console.log( 'effect' ) }; +var a3 = (true ? foo3 : bar3).y.z; +var b3 = (false ? bar3 : foo3).y.z; +var c3 = (true ? foo3 : bar3).x(); +var d3 = (false ? bar3 : foo3).x(); +var e3 = (true ? foo3 : bar3).y.z = 1; +var f3 = (false ? bar3 : foo3).y.z = 1; + +// known side-effect +var foo4 = { x: () => {}, y: {} }; +var bar4 = { x: () => console.log( 'effect' ) }; +var a4 = (true ? bar4 : foo4).y.z; +var b4 = (false ? foo4 : bar4).y.z; +var c4 = (true ? bar4 : foo4).x(); +var d4 = (false ? foo4 : bar4).x(); +var e4 = (true ? bar4 : foo4).y.z = 1; +var f4 = (false ? foo4 : bar4).y.z = 1; diff --git a/test/form/samples/conditional-expression/_expected/amd.js b/test/form/samples/conditional-expression/_expected/amd.js index ebe3a3b3fa7..2bfcb484f92 100644 --- a/test/form/samples/conditional-expression/_expected/amd.js +++ b/test/form/samples/conditional-expression/_expected/amd.js @@ -8,9 +8,14 @@ define(function () { 'use strict'; // unknown branch with side-effect var c = unknownValue ? foo() : 2; var d = unknownValue ? 1 : foo(); + var d1 = function () {}; + var d2 = function () {this.x = 1;}; + (unknownValue ? d1 : d2)(); // known side-effect var h = foo(); + var h1 = (function () {this.x = 1;})(); var i = foo(); + var i1 = (function () {this.x = 1;})(); }); diff --git a/test/form/samples/conditional-expression/_expected/cjs.js b/test/form/samples/conditional-expression/_expected/cjs.js index 53d73e8a23e..8c7e1e41588 100644 --- a/test/form/samples/conditional-expression/_expected/cjs.js +++ b/test/form/samples/conditional-expression/_expected/cjs.js @@ -8,7 +8,12 @@ var unknownValue = bar(); // unknown branch with side-effect var c = unknownValue ? foo() : 2; var d = unknownValue ? 1 : foo(); +var d1 = function () {}; +var d2 = function () {this.x = 1;}; +(unknownValue ? d1 : d2)(); // known side-effect var h = foo(); +var h1 = (function () {this.x = 1;})(); var i = foo(); +var i1 = (function () {this.x = 1;})(); diff --git a/test/form/samples/conditional-expression/_expected/es.js b/test/form/samples/conditional-expression/_expected/es.js index 78ec545357e..fa9664fcf1b 100644 --- a/test/form/samples/conditional-expression/_expected/es.js +++ b/test/form/samples/conditional-expression/_expected/es.js @@ -6,7 +6,12 @@ var unknownValue = bar(); // unknown branch with side-effect var c = unknownValue ? foo() : 2; var d = unknownValue ? 1 : foo(); +var d1 = function () {}; +var d2 = function () {this.x = 1;}; +(unknownValue ? d1 : d2)(); // known side-effect var h = foo(); +var h1 = (function () {this.x = 1;})(); var i = foo(); +var i1 = (function () {this.x = 1;})(); diff --git a/test/form/samples/conditional-expression/_expected/iife.js b/test/form/samples/conditional-expression/_expected/iife.js index 85e8abd32cb..680881f7d21 100644 --- a/test/form/samples/conditional-expression/_expected/iife.js +++ b/test/form/samples/conditional-expression/_expected/iife.js @@ -9,9 +9,14 @@ // unknown branch with side-effect var c = unknownValue ? foo() : 2; var d = unknownValue ? 1 : foo(); + var d1 = function () {}; + var d2 = function () {this.x = 1;}; + (unknownValue ? d1 : d2)(); // known side-effect var h = foo(); + var h1 = (function () {this.x = 1;})(); var i = foo(); + var i1 = (function () {this.x = 1;})(); }()); diff --git a/test/form/samples/conditional-expression/_expected/umd.js b/test/form/samples/conditional-expression/_expected/umd.js index 41c39819cbb..1d972649d27 100644 --- a/test/form/samples/conditional-expression/_expected/umd.js +++ b/test/form/samples/conditional-expression/_expected/umd.js @@ -12,9 +12,14 @@ // unknown branch with side-effect var c = unknownValue ? foo() : 2; var d = unknownValue ? 1 : foo(); + var d1 = function () {}; + var d2 = function () {this.x = 1;}; + (unknownValue ? d1 : d2)(); // known side-effect var h = foo(); + var h1 = (function () {this.x = 1;})(); var i = foo(); + var i1 = (function () {this.x = 1;})(); }))); diff --git a/test/form/samples/conditional-expression/main.js b/test/form/samples/conditional-expression/main.js index 0eb71136dd9..de0d2ad5bf7 100644 --- a/test/form/samples/conditional-expression/main.js +++ b/test/form/samples/conditional-expression/main.js @@ -5,17 +5,26 @@ var unknownValue = bar(); // unknown branch without side-effects var b = unknownValue ? 1 : 2; +var b1 = function () {}; +var b2 = function () {this.x = 1;}; +new (unknownValue ? b1 : b2)(); // unknown branch with side-effect var c = unknownValue ? foo() : 2; var d = unknownValue ? 1 : foo(); +var d1 = function () {}; +var d2 = function () {this.x = 1;}; +(unknownValue ? d1 : d2)(); // no side-effects var e = true ? 1 : foo(); +var e1 = (true ? function () {} : function () {this.x = 1;})(); var f = false ? foo() : 2; +var f1 = (false ? function () {this.x = 1;} : function () {})(); var g = true ? 1 : 2; // known side-effect var h = true ? foo() : 2; +var h1 = (true ? function () {this.x = 1;} : function () {})(); var i = false ? 1 : foo(); - +var i1 = (false ? function () {} : function () {this.x = 1;})(); From 41b6a94c85c6575c797a566ef4ad6082610b0b1e Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Fri, 6 Oct 2017 23:29:52 +0200 Subject: [PATCH 31/76] Radically simplify call logic as there is just too much unknown stuff related to "this". Now we no longer "bind" calls, and "this" is no longer a real variable. We just regard two cases: * If we call something with new and it is a function node, we assume that this is some kind of object and everything is safe that can be done with an empty object * For all other calls, we treat "this" like "undefined" (or rather, any unknown node) --- src/ast/ExecutionPathOptions.js | 49 ++++++++++++++++--- src/ast/Node.js | 10 ---- src/ast/nodes/ArrowFunctionExpression.js | 3 -- src/ast/nodes/CallExpression.js | 1 - src/ast/nodes/ClassBody.js | 6 --- src/ast/nodes/ConditionalExpression.js | 11 ----- src/ast/nodes/Identifier.js | 7 --- src/ast/nodes/MemberExpression.js | 13 ----- src/ast/nodes/MethodDefinition.js | 4 -- src/ast/nodes/NewExpression.js | 7 +-- src/ast/nodes/ObjectExpression.js | 8 --- src/ast/nodes/Property.js | 4 -- src/ast/nodes/TaggedTemplateExpression.js | 1 - src/ast/nodes/ThisExpression.js | 17 ++----- src/ast/nodes/shared/ClassNode.js | 7 --- src/ast/nodes/shared/FunctionNode.js | 17 ++----- src/ast/scopes/FunctionScope.js | 2 - src/ast/scopes/ModuleScope.js | 3 -- src/ast/values.js | 1 - src/ast/variables/LocalVariable.js | 20 ++------ src/ast/variables/Variable.js | 2 - .../samples/prototype-functions/_config.js | 3 ++ .../prototype-functions/_expected/amd.js | 15 ++++++ .../prototype-functions/_expected/cjs.js | 13 +++++ .../prototype-functions/_expected/es.js | 11 +++++ .../prototype-functions/_expected/iife.js | 16 ++++++ .../prototype-functions/_expected/umd.js | 19 +++++++ test/form/samples/prototype-functions/main.js | 11 +++++ 28 files changed, 143 insertions(+), 138 deletions(-) create mode 100644 test/form/samples/prototype-functions/_config.js create mode 100644 test/form/samples/prototype-functions/_expected/amd.js create mode 100644 test/form/samples/prototype-functions/_expected/cjs.js create mode 100644 test/form/samples/prototype-functions/_expected/es.js create mode 100644 test/form/samples/prototype-functions/_expected/iife.js create mode 100644 test/form/samples/prototype-functions/_expected/umd.js create mode 100644 test/form/samples/prototype-functions/main.js diff --git a/src/ast/ExecutionPathOptions.js b/src/ast/ExecutionPathOptions.js index 08af4123824..2146942591b 100644 --- a/src/ast/ExecutionPathOptions.js +++ b/src/ast/ExecutionPathOptions.js @@ -2,10 +2,12 @@ import Immutable from 'immutable'; const OPTION_IGNORE_BREAK_STATEMENTS = 'IGNORE_BREAK_STATEMENTS'; const OPTION_IGNORE_RETURN_AWAIT_YIELD = 'IGNORE_RETURN_AWAIT_YIELD'; -const OPTION_IGNORE_SAFE_THIS_MUTATIONS = 'IGNORE_SAFE_THIS_MUTATIONS'; +const OPTION_HAS_SAFE_THIS = 'HAS_SAFE_THIS'; +const OPTION_CALLED_WITH_NEW = 'CALLED_WITH_NEW'; const OPTION_ACCESSED_NODES = 'ACCESSED_NODES'; const OPTION_ASSIGNED_NODES = 'ASSIGNED_NODES'; const OPTION_CALLED_NODES = 'CALLED_NODES'; +const OPTION_NODES_CALLED_WITH_NEW = 'NODES_CALLED_WITH_NEW'; const OPTION_MUTATED_NODES = 'MUTATED_NODES'; const IGNORED_LABELS = 'IGNORED_LABELS'; @@ -103,16 +105,31 @@ export default class ExecutionPathOptions { /** * @return {boolean} */ - ignoreSafeThisMutations () { - return this.get( OPTION_IGNORE_SAFE_THIS_MUTATIONS ); + hasSafeThis () { + return this.get( OPTION_HAS_SAFE_THIS ); } /** * @param {boolean} [value=true] * @return {ExecutionPathOptions} */ - setIgnoreSafeThisMutations ( value = true ) { - return this.set( OPTION_IGNORE_SAFE_THIS_MUTATIONS, value ); + setHasSafeThis ( value = true ) { + return this.set( OPTION_HAS_SAFE_THIS, value ); + } + + /** + * @return {boolean} + */ + calledWithNew () { + return this.get( OPTION_CALLED_WITH_NEW ); + } + + /** + * @param {boolean} [value=true] + * @return {ExecutionPathOptions} + */ + setCalledWithNew ( value = true ) { + return this.set( OPTION_CALLED_WITH_NEW, value ); } /** @@ -183,15 +200,33 @@ export default class ExecutionPathOptions { return this._optionValues.getIn( [ OPTION_CALLED_NODES, node ] ); } + /** + * @param {Node} node + * @return {ExecutionPathOptions} + */ + addNodeCalledWithNew ( node ) { + return this.setIn( [ OPTION_NODES_CALLED_WITH_NEW, node ], true ); + } + + /** + * @param {Node} node + * @return {boolean} + */ + hasNodeBeenCalledWithNew ( node ) { + return this._optionValues.getIn( [ OPTION_NODES_CALLED_WITH_NEW, node ] ); + } + /** * @param {Node} calledNode + * @param {boolean} withNew * @return {ExecutionPathOptions} */ - getHasEffectsWhenCalledOptions ( calledNode ) { + getHasEffectsWhenCalledOptions ( calledNode, { withNew = false } = {} ) { return this .addCalledNode( calledNode ) .setIgnoreReturnAwaitYield() .setIgnoreBreakStatements( false ) - .setIgnoreNoLabels(); + .setIgnoreNoLabels() + .setCalledWithNew( withNew ); } } diff --git a/src/ast/Node.js b/src/ast/Node.js index df70108a3e0..3043bf8c6c7 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -26,16 +26,6 @@ export default class Node { */ bindAssignmentAtPath () {} - /** - * Binds ways a node is called to a node given a path. Current options are: - * - withNew: boolean - Did this call use the "new" operator - * The default noop implementation is ok as long as hasEffectsWhenCalledAtPath - * always returns true for this node. Otherwise it should be overridden. - * @param {String[]} path - * @param callOptions - */ - bindCallAtPath () {} - eachChild ( callback ) { this.keys.forEach( key => { const value = this[ key ]; diff --git a/src/ast/nodes/ArrowFunctionExpression.js b/src/ast/nodes/ArrowFunctionExpression.js index 54e8db6a116..1b8df457ef9 100644 --- a/src/ast/nodes/ArrowFunctionExpression.js +++ b/src/ast/nodes/ArrowFunctionExpression.js @@ -2,9 +2,6 @@ import Node from '../Node'; import Scope from '../scopes/Scope.js'; export default class ArrowFunctionExpression extends Node { - // Should receive an implementation once we start tracking parameter values - bindCallAtPath () {} - hasEffects () { return this.included; } diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index ad47fda85d7..a76394b4ebe 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -22,7 +22,6 @@ export default class CallExpression extends Node { } super.bind(); - this.callee.bindCallAtPath( [], { withNew: false } ); } hasEffects ( options ) { diff --git a/src/ast/nodes/ClassBody.js b/src/ast/nodes/ClassBody.js index cd7d71cf060..eca01e30b83 100644 --- a/src/ast/nodes/ClassBody.js +++ b/src/ast/nodes/ClassBody.js @@ -1,12 +1,6 @@ import Node from '../Node'; export default class ClassBody extends Node { - bindCallAtPath ( path, callOptions ) { - if ( this.classConstructor && path.length === 0 ) { - this.classConstructor.bindCallAtPath( [], callOptions ); - } - } - hasEffectsWhenCalledAtPath ( path, options ) { if ( path.length > 0 ) { return true; diff --git a/src/ast/nodes/ConditionalExpression.js b/src/ast/nodes/ConditionalExpression.js index ff3ca1af4a0..1da92459e77 100644 --- a/src/ast/nodes/ConditionalExpression.js +++ b/src/ast/nodes/ConditionalExpression.js @@ -13,17 +13,6 @@ export default class ConditionalExpression extends Node { } } - bindCallAtPath ( path, callOptions ) { - if ( this.testValue === UNKNOWN_VALUE ) { - this.consequent.bindCallAtPath( path, callOptions ); - this.alternate.bindCallAtPath( path, callOptions ); - } else { - this.testValue - ? this.consequent.bindCallAtPath( path, callOptions ) - : this.alternate.bindCallAtPath( path, callOptions ); - } - } - getValue () { const testValue = this.test.getValue(); if ( testValue === UNKNOWN_VALUE ) return UNKNOWN_VALUE; diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index 820846994fd..006b1a14532 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -13,13 +13,6 @@ export default class Identifier extends Node { } } - bindCallAtPath ( path, callOptions ) { - this._bindVariableIfMissing(); - if ( this.variable ) { - this.variable.addCallAtPath( path, callOptions ); - } - } - _bindVariableIfMissing () { if ( !this.variable && isReference( this, this.parent ) ) { this.variable = this.scope.findVariable( this.name ); diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index 0c044927bf3..b164d905440 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -89,19 +89,6 @@ export default class MemberExpression extends Node { } } - bindCallAtPath ( path, callOptions ) { - if ( !this._bound ) { - this.bind(); - } - if ( this.variable ) { - this.variable.addCallAtPath( path, callOptions ); - } else if ( this.computed ) { - this.object.bindCallAtPath( [ UNKNOWN_KEY, ...path ], callOptions ); - } else { - this.object.bindCallAtPath( [ this.property.name, ...path ], callOptions ); - } - } - hasEffects ( options ) { return super.hasEffects( options ) || this.object.hasEffectsWhenAccessedAtPath( [ this.computed ? UNKNOWN_KEY : this.property.name ], options ); diff --git a/src/ast/nodes/MethodDefinition.js b/src/ast/nodes/MethodDefinition.js index 7427e87995b..c439b5caf33 100644 --- a/src/ast/nodes/MethodDefinition.js +++ b/src/ast/nodes/MethodDefinition.js @@ -1,10 +1,6 @@ import Node from '../Node'; export default class MethodDefinition extends Node { - bindCallAtPath ( path, callOptions ) { - this.value.bindCallAtPath( path, callOptions ); - } - hasEffects ( options ) { return this.key.hasEffects( options ); } diff --git a/src/ast/nodes/NewExpression.js b/src/ast/nodes/NewExpression.js index c6e5a51e852..294a39db3de 100644 --- a/src/ast/nodes/NewExpression.js +++ b/src/ast/nodes/NewExpression.js @@ -1,15 +1,10 @@ import Node from '../Node.js'; export default class NewExpression extends Node { - bind () { - super.bind(); - this.callee.bindCallAtPath( [], { withNew: true } ); - } - hasEffects ( options ) { return this.included || this.arguments.some( child => child.hasEffects( options ) ) - || this.callee.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions( this.callee ) ); + || this.callee.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions( this.callee, { withNew: true } ) ); } hasEffectsWhenAccessedAtPath ( path ) { diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index ea76314d7f4..2d48265b3f3 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -13,14 +13,6 @@ export default class ObjectExpression extends Node { 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 ) ); - } - _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 42dfd07bd8f..6aca1538853 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -6,10 +6,6 @@ export default class Property extends Node { this.value.bindAssignmentAtPath( path, expression ); } - bindCallAtPath ( path, callOptions ) { - this.value.bindCallAtPath( path, callOptions ); - } - hasEffects ( options ) { return this.included || this.key.hasEffects( options ) diff --git a/src/ast/nodes/TaggedTemplateExpression.js b/src/ast/nodes/TaggedTemplateExpression.js index 81431bd710b..dd1aa384e11 100644 --- a/src/ast/nodes/TaggedTemplateExpression.js +++ b/src/ast/nodes/TaggedTemplateExpression.js @@ -22,7 +22,6 @@ export default class TaggedTemplateExpression extends Node { } super.bind(); - this.tag.bindCallAtPath( [], { withNew: false } ); } hasEffects ( options ) { diff --git a/src/ast/nodes/ThisExpression.js b/src/ast/nodes/ThisExpression.js index 230ce6f60b2..3d5f935ca1b 100644 --- a/src/ast/nodes/ThisExpression.js +++ b/src/ast/nodes/ThisExpression.js @@ -16,26 +16,17 @@ export default class ThisExpression extends Node { } } - bind () { - this.variable = this.scope.findVariable( 'this' ); - } - hasEffectsWhenAccessedAtPath ( path, options ) { - return this.variable.hasEffectsWhenAccessedAtPath( path, options ); + return !(path.length === 0 + || (path.length === 1 && options.hasSafeThis())); } hasEffectsWhenAssignedAtPath ( path, options ) { - if ( path.length === 0 ) { - return true; - } - return this.hasEffectsWhenMutatedAtPath( path.slice( 1 ), options ); + return !(path.length === 1 && options.hasSafeThis()); } hasEffectsWhenMutatedAtPath ( path, options ) { - if ( path.length === 0 ) { - return !options.ignoreSafeThisMutations() || this.variable.hasEffectsWhenMutatedAtPath( [], options ); - } - return this.variable.hasEffectsWhenMutatedAtPath( path, options ); + return !(path.length === 0 && options.hasSafeThis()); } render ( code ) { diff --git a/src/ast/nodes/shared/ClassNode.js b/src/ast/nodes/shared/ClassNode.js index c82e5213b0f..43efe1f83e0 100644 --- a/src/ast/nodes/shared/ClassNode.js +++ b/src/ast/nodes/shared/ClassNode.js @@ -2,13 +2,6 @@ import Node from '../../Node.js'; import Scope from '../../scopes/Scope'; export default class ClassNode extends Node { - bindCallAtPath ( path, callOptions ) { - if ( this.superClass ) { - this.superClass.bindCallAtPath( path, callOptions ); - } - this.body.bindCallAtPath( path, callOptions ); - } - hasEffectsWhenAccessedAtPath ( path ) { return path.length > 1; } diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index e23d3bab4ea..77274ab3ad7 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -1,21 +1,8 @@ import Node from '../../Node.js'; import FunctionScope from '../../scopes/FunctionScope'; -import { UNKNOWN_ASSIGNMENT } from '../../values'; import VirtualObjectExpression from './VirtualObjectExpression'; export default class FunctionNode extends Node { - bindCallAtPath ( path, { withNew } ) { - if ( path.length === 0 ) { - const thisVariable = this.scope.findVariable( 'this' ); - - if ( withNew ) { - thisVariable.assignExpressionAtPath( [], new VirtualObjectExpression() ); - } else { - thisVariable.assignExpressionAtPath( [], UNKNOWN_ASSIGNMENT ); - } - } - } - hasEffects ( options ) { return this.included || (this.id && this.id.hasEffects( options )); } @@ -44,7 +31,9 @@ export default class FunctionNode extends Node { if ( path.length > 0 ) { return true; } - const innerOptions = options.setIgnoreSafeThisMutations(); + const innerOptions = options.calledWithNew() + ? options.setHasSafeThis( true ) + : options.setHasSafeThis( false ); return this.params.some( param => param.hasEffects( innerOptions ) ) || this.body.hasEffects( innerOptions ); } diff --git a/src/ast/scopes/FunctionScope.js b/src/ast/scopes/FunctionScope.js index b00791ac6dc..170ff8ec2b5 100644 --- a/src/ast/scopes/FunctionScope.js +++ b/src/ast/scopes/FunctionScope.js @@ -1,12 +1,10 @@ import Scope from './Scope'; -import LocalVariable from '../variables/LocalVariable'; import ParameterVariable from '../variables/ParameterVariable'; export default class FunctionScope extends Scope { constructor ( options = {} ) { super( options ); this.variables.arguments = new ParameterVariable( 'arguments' ); - this.variables.this = new LocalVariable( 'this', null, null ); } findLexicalBoundary () { diff --git a/src/ast/scopes/ModuleScope.js b/src/ast/scopes/ModuleScope.js index ebdc5c58cfd..05049ec21a5 100644 --- a/src/ast/scopes/ModuleScope.js +++ b/src/ast/scopes/ModuleScope.js @@ -1,8 +1,6 @@ import { forOwn } from '../../utils/object.js'; import relativeId from '../../utils/relativeId.js'; import Scope from './Scope.js'; -import LocalVariable from '../variables/LocalVariable'; -import UndefinedIdentifier from '../nodes/shared/UndefinedIdentifier'; export default class ModuleScope extends Scope { constructor ( module ) { @@ -12,7 +10,6 @@ export default class ModuleScope extends Scope { } ); this.module = module; - this.variables.this = new LocalVariable( 'this', null, new UndefinedIdentifier() ); } deshadow ( names ) { diff --git a/src/ast/values.js b/src/ast/values.js index 171b69678b0..4279b88bc04 100644 --- a/src/ast/values.js +++ b/src/ast/values.js @@ -3,7 +3,6 @@ export const UNKNOWN_VALUE = { toString: () => '[[UNKNOWN]]' }; export const UNKNOWN_ASSIGNMENT = { type: 'UNKNOWN', bindAssignmentAtPath: () => {}, - bindCallAtPath: () => {}, hasEffectsWhenAccessedAtPath: path => path.length > 0, hasEffectsWhenAssignedAtPath: () => true, hasEffectsWhenCalledAtPath: () => true, diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index a7b11a405ab..45797b33b53 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -12,23 +12,12 @@ export default class LocalVariable extends Variable { this.declarations = new Set( declarator ? [ declarator ] : null ); this.assignedExpressions = new StructuredAssignmentTracker(); init && this.assignedExpressions.addAtPath( [], init ); - this.calls = new StructuredAssignmentTracker(); } addDeclaration ( identifier ) { this.declarations.add( identifier ); } - addCallAtPath ( path, callOptions ) { - if ( path.length > MAX_PATH_LENGTH ) { - return; - } - if ( this.calls.hasAtPath( path, callOptions ) ) return; - this.calls.addAtPath( path, callOptions ); - this.assignedExpressions.forEachAtPath( path, ( relativePath, node ) => - node.bindCallAtPath( relativePath, callOptions ) ); - } - assignExpressionAtPath ( path, expression ) { if ( path.length > MAX_PATH_LENGTH ) { return; @@ -39,8 +28,6 @@ export default class LocalVariable extends Variable { this.assignedExpressions.forEachAtPath( path.slice( 0, -1 ), ( relativePath, node ) => node.bindAssignmentAtPath( [ ...relativePath, ...path.slice( -1 ) ], expression ) ); } - this.calls.forEachAtPath( path, ( relativePath, callOptions ) => - expression.bindCallAtPath( relativePath, callOptions ) ); if ( path.length === 0 ) { this.isReassigned = true; } @@ -80,8 +67,11 @@ export default class LocalVariable extends Variable { } return this.assignedExpressions.someAtPath( path, ( relativePath, node ) => { if ( relativePath.length === 0 ) { - return !options.hasNodeBeenCalled( node ) - && node.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions( node ) ); + return options.calledWithNew() + ? !options.hasNodeBeenCalledWithNew( node ) + && node.hasEffectsWhenCalledAtPath( [], options.addNodeCalledWithNew( node ) ) + : !options.hasNodeBeenCalled( node ) + && node.hasEffectsWhenCalledAtPath( [], options.addCalledNode( node ) ); } return node.hasEffectsWhenCalledAtPath( relativePath, options ); } ); diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index 657c31cb891..40a8adab77a 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -3,8 +3,6 @@ export default class Variable { this.name = name; } - addCallAtPath () {} - addReference () {} assignExpressionAtPath () {} diff --git a/test/form/samples/prototype-functions/_config.js b/test/form/samples/prototype-functions/_config.js new file mode 100644 index 00000000000..620b36d549c --- /dev/null +++ b/test/form/samples/prototype-functions/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'properly includes prototype functions' +}; diff --git a/test/form/samples/prototype-functions/_expected/amd.js b/test/form/samples/prototype-functions/_expected/amd.js new file mode 100644 index 00000000000..f622d596793 --- /dev/null +++ b/test/form/samples/prototype-functions/_expected/amd.js @@ -0,0 +1,15 @@ +define(function () { 'use strict'; + + function Foo () { + this.doIt(); + } + + Foo.prototype.doIt = function () { + this.foo.tooDeep = 1; + this.foo['b' + 'ar'] = 2; + this.doesNotExist(); + }; + + const foo = new Foo(); + +}); diff --git a/test/form/samples/prototype-functions/_expected/cjs.js b/test/form/samples/prototype-functions/_expected/cjs.js new file mode 100644 index 00000000000..aad510a6db3 --- /dev/null +++ b/test/form/samples/prototype-functions/_expected/cjs.js @@ -0,0 +1,13 @@ +'use strict'; + +function Foo () { + this.doIt(); +} + +Foo.prototype.doIt = function () { + this.foo.tooDeep = 1; + this.foo['b' + 'ar'] = 2; + this.doesNotExist(); +}; + +const foo = new Foo(); diff --git a/test/form/samples/prototype-functions/_expected/es.js b/test/form/samples/prototype-functions/_expected/es.js new file mode 100644 index 00000000000..40a5a9dba8f --- /dev/null +++ b/test/form/samples/prototype-functions/_expected/es.js @@ -0,0 +1,11 @@ +function Foo () { + this.doIt(); +} + +Foo.prototype.doIt = function () { + this.foo.tooDeep = 1; + this.foo['b' + 'ar'] = 2; + this.doesNotExist(); +}; + +const foo = new Foo(); diff --git a/test/form/samples/prototype-functions/_expected/iife.js b/test/form/samples/prototype-functions/_expected/iife.js new file mode 100644 index 00000000000..60dd1b584be --- /dev/null +++ b/test/form/samples/prototype-functions/_expected/iife.js @@ -0,0 +1,16 @@ +(function () { + 'use strict'; + + function Foo () { + this.doIt(); + } + + Foo.prototype.doIt = function () { + this.foo.tooDeep = 1; + this.foo['b' + 'ar'] = 2; + this.doesNotExist(); + }; + + const foo = new Foo(); + +}()); diff --git a/test/form/samples/prototype-functions/_expected/umd.js b/test/form/samples/prototype-functions/_expected/umd.js new file mode 100644 index 00000000000..baca4088c9e --- /dev/null +++ b/test/form/samples/prototype-functions/_expected/umd.js @@ -0,0 +1,19 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + function Foo () { + this.doIt(); + } + + Foo.prototype.doIt = function () { + this.foo.tooDeep = 1; + this.foo['b' + 'ar'] = 2; + this.doesNotExist(); + }; + + const foo = new Foo(); + +}))); diff --git a/test/form/samples/prototype-functions/main.js b/test/form/samples/prototype-functions/main.js new file mode 100644 index 00000000000..40a5a9dba8f --- /dev/null +++ b/test/form/samples/prototype-functions/main.js @@ -0,0 +1,11 @@ +function Foo () { + this.doIt(); +} + +Foo.prototype.doIt = function () { + this.foo.tooDeep = 1; + this.foo['b' + 'ar'] = 2; + this.doesNotExist(); +}; + +const foo = new Foo(); From 9fe013fae4ee4bb379837e44227b32a475c45f8f Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Sat, 7 Oct 2017 11:06:03 +0200 Subject: [PATCH 32/76] Improve JSDoc comments for Node and add comments to Variable. --- .gitignore | 4 +- src/ast/Node.js | 24 +++++----- src/ast/variables/ExportDefaultVariable.js | 6 +-- src/ast/variables/ExternalVariable.js | 4 +- src/ast/variables/GlobalVariable.js | 4 +- src/ast/variables/NamespaceVariable.js | 4 +- src/ast/variables/Variable.js | 55 +++++++++++++++++++--- 7 files changed, 74 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index e19d49a9cf5..e5d345401df 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ coverage .commithash .idea bin/rollup -test/_tmp \ No newline at end of file +test/_tmp +test/hooks/tmp +test/tmp diff --git a/src/ast/Node.js b/src/ast/Node.js index 3043bf8c6c7..cf6dacca8b0 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + import { locate } from 'locate-character'; import { UNKNOWN_VALUE } from './values.js'; import ExecutionPathOptions from './ExecutionPathOptions'; @@ -9,7 +11,7 @@ export default class Node { /** * Called once all nodes have been initialised and the scopes have been populated. - * Use this to bind assignments and calls to variables. + * Use this to bind assignments to variables. */ bind () { this.eachChild( child => child.bind() ); @@ -17,14 +19,14 @@ 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 if otherNode + * E.g., node.bindAssignmentAtPath(['x', 'y'], otherNode) is called when otherNode * 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. * @param {String[]} path * @param {Node} expression */ - bindAssignmentAtPath () {} + bindAssignmentAtPath ( path, expression ) {} eachChild ( callback ) { this.keys.forEach( key => { @@ -62,7 +64,7 @@ export default class Node { * @param {ExecutionPathOptions} options * @return {boolean} */ - hasEffectsWhenAccessedAtPath ( path ) { + hasEffectsWhenAccessedAtPath ( path, options ) { return path.length > 0; } @@ -71,7 +73,7 @@ export default class Node { * @param {ExecutionPathOptions} options * @return {boolean} */ - hasEffectsWhenAssignedAtPath () { + hasEffectsWhenAssignedAtPath ( path, options ) { return true; } @@ -80,7 +82,7 @@ export default class Node { * @param {ExecutionPathOptions} options * @return {boolean} */ - hasEffectsWhenCalledAtPath () { + hasEffectsWhenCalledAtPath ( path, options ) { return true; } @@ -89,14 +91,14 @@ export default class Node { * @param {ExecutionPathOptions} options * @return {boolean} */ - hasEffectsWhenMutatedAtPath () { + hasEffectsWhenMutatedAtPath ( path, options ) { return true; } /** * Includes the node in the bundle. Children are usually included if they are * necessary for this node (e.g. a function body) or if they have effects. - * Necessary variables should be included as well. Should return true if any + * Necessary variables need to be included as well. Should return true if any * nodes or variables have been added that were missing before. * @return {boolean} */ @@ -144,7 +146,7 @@ export default class Node { * Override to change how and with what scopes children are initialised * @param {Scope} parentScope */ - initialiseChildren () { + initialiseChildren ( parentScope ) { this.eachChild( child => child.initialise( this.scope ) ); } @@ -152,7 +154,7 @@ export default class Node { * Override to perform special initialisation steps after the scope is initialised * @param {Scope} parentScope */ - initialiseNode () {} + initialiseNode ( parentScope ) {} /** * Override if this scope should receive a different scope than the parent scope. @@ -173,7 +175,7 @@ export default class Node { * been included. * @param {Scope} parentScope */ - isFullyIncluded () { + isFullyIncluded ( parentScope ) { if ( this._fullyIncluded ) { return true; } diff --git a/src/ast/variables/ExportDefaultVariable.js b/src/ast/variables/ExportDefaultVariable.js index cb4fe3f6627..3ab2fea21c9 100644 --- a/src/ast/variables/ExportDefaultVariable.js +++ b/src/ast/variables/ExportDefaultVariable.js @@ -7,10 +7,10 @@ export default class ExportDefaultVariable extends LocalVariable { this.hasId = !!exportDefaultDeclaration.declaration.id; } - addReference ( reference ) { - this.name = reference.name; + addReference ( identifier ) { + this.name = identifier.name; if ( this._original ) { - this._original.addReference( reference ); + this._original.addReference( identifier ); } } diff --git a/src/ast/variables/ExternalVariable.js b/src/ast/variables/ExternalVariable.js index 097a79b3516..16af67dca17 100644 --- a/src/ast/variables/ExternalVariable.js +++ b/src/ast/variables/ExternalVariable.js @@ -9,9 +9,9 @@ export default class ExternalVariable extends Variable { this.isNamespace = name === '*'; } - addReference ( reference ) { + addReference ( identifier ) { if ( this.name === 'default' || this.name === '*' ) { - this.module.suggestName( reference.name ); + this.module.suggestName( identifier.name ); } } diff --git a/src/ast/variables/GlobalVariable.js b/src/ast/variables/GlobalVariable.js index 21979658d52..a5e53f7f128 100644 --- a/src/ast/variables/GlobalVariable.js +++ b/src/ast/variables/GlobalVariable.js @@ -10,8 +10,8 @@ export default class GlobalVariable extends Variable { this.included = true; } - addReference ( reference ) { - if ( reference.isReassignment ) this.isReassigned = true; + addReference ( identifier ) { + if ( identifier.isReassignment ) this.isReassigned = true; } hasEffectsWhenAccessedAtPath ( path ) { diff --git a/src/ast/variables/NamespaceVariable.js b/src/ast/variables/NamespaceVariable.js index 194cf4f6b63..e16ad8a52d1 100644 --- a/src/ast/variables/NamespaceVariable.js +++ b/src/ast/variables/NamespaceVariable.js @@ -15,8 +15,8 @@ export default class NamespaceVariable extends Variable { } ); } - addReference ( node ) { - this.name = node.name; + addReference ( identifier ) { + this.name = identifier.name; } includeVariable () { diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index 40a8adab77a..5f37c30d1fc 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -1,32 +1,75 @@ +/* eslint-disable no-unused-vars */ + export default class Variable { constructor ( name ) { this.name = name; } - addReference () {} + /** + * Binds identifiers that reference this variable to this variable. + * Necessary to be able to change variable names. + * @param {Identifier} identifier + */ + addReference ( identifier ) {} - assignExpressionAtPath () {} + /** + * 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 + */ + assignExpressionAtPath ( path, expression ) {} + /** + * @returns {String} + */ getName () { return this.name; } - hasEffectsWhenAccessedAtPath ( path ) { + /** + * @param {String[]} path + * @param {ExecutionPathOptions} options + * @return {boolean} + */ + hasEffectsWhenAccessedAtPath ( path, options ) { return path.length > 0; } - hasEffectsWhenAssignedAtPath () { + /** + * @param {String[]} path + * @param {ExecutionPathOptions} options + * @return {boolean} + */ + hasEffectsWhenAssignedAtPath ( path, options ) { return true; } - hasEffectsWhenCalledAtPath () { + /** + * @param {String[]} path + * @param {ExecutionPathOptions} options + * @return {boolean} + */ + hasEffectsWhenCalledAtPath ( path, options ) { return true; } - hasEffectsWhenMutatedAtPath () { + /** + * @param {String[]} path + * @param {ExecutionPathOptions} options + * @return {boolean} + */ + hasEffectsWhenMutatedAtPath ( path, options ) { return true; } + /** + * Marks this variable as being part of the bundle, which is usually the case when one of + * its identifiers becomes part of the bundle. Returns true if it has not been included + * previously. + * Once a variable is included, it should take care all its declarations are included. + * @returns {boolean} + */ includeVariable () { if ( this.included ) { return false; From aca8a76257f881d6744b2c4255356738bac68a53 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Wed, 11 Oct 2017 06:44:04 +0200 Subject: [PATCH 33/76] Split call options from execution path options to prepare for improved "this"-handling and later parameter handling. --- src/ast/ExecutionPathOptions.js | 57 +++++-------------- src/ast/Node.js | 3 +- src/ast/nodes/CallExpression.js | 2 +- src/ast/nodes/ClassBody.js | 4 +- src/ast/nodes/ConditionalExpression.js | 10 ++-- src/ast/nodes/ExportDefaultDeclaration.js | 4 +- src/ast/nodes/Identifier.js | 4 +- src/ast/nodes/MemberExpression.js | 6 +- src/ast/nodes/MethodDefinition.js | 4 +- src/ast/nodes/NewExpression.js | 2 +- src/ast/nodes/ObjectExpression.js | 4 +- src/ast/nodes/Property.js | 12 ++-- src/ast/nodes/TaggedTemplateExpression.js | 2 +- src/ast/nodes/shared/ClassNode.js | 6 +- src/ast/nodes/shared/FunctionNode.js | 6 +- src/ast/variables/LocalVariable.js | 11 ++-- src/ast/variables/Variable.js | 3 +- .../samples/proper-this-context/_config.js | 4 ++ .../proper-this-context/_expected/amd.js | 14 +++++ .../proper-this-context/_expected/cjs.js | 12 ++++ .../proper-this-context/_expected/es.js | 8 +++ .../proper-this-context/_expected/iife.js | 15 +++++ .../proper-this-context/_expected/umd.js | 18 ++++++ test/form/samples/proper-this-context/main.js | 9 +++ 24 files changed, 133 insertions(+), 87 deletions(-) create mode 100644 test/form/samples/proper-this-context/_config.js create mode 100644 test/form/samples/proper-this-context/_expected/amd.js create mode 100644 test/form/samples/proper-this-context/_expected/cjs.js create mode 100644 test/form/samples/proper-this-context/_expected/es.js create mode 100644 test/form/samples/proper-this-context/_expected/iife.js create mode 100644 test/form/samples/proper-this-context/_expected/umd.js create mode 100644 test/form/samples/proper-this-context/main.js diff --git a/src/ast/ExecutionPathOptions.js b/src/ast/ExecutionPathOptions.js index 2146942591b..69bd573a2af 100644 --- a/src/ast/ExecutionPathOptions.js +++ b/src/ast/ExecutionPathOptions.js @@ -3,16 +3,16 @@ import Immutable from 'immutable'; const OPTION_IGNORE_BREAK_STATEMENTS = 'IGNORE_BREAK_STATEMENTS'; const OPTION_IGNORE_RETURN_AWAIT_YIELD = 'IGNORE_RETURN_AWAIT_YIELD'; const OPTION_HAS_SAFE_THIS = 'HAS_SAFE_THIS'; -const OPTION_CALLED_WITH_NEW = 'CALLED_WITH_NEW'; const OPTION_ACCESSED_NODES = 'ACCESSED_NODES'; const OPTION_ASSIGNED_NODES = 'ASSIGNED_NODES'; -const OPTION_CALLED_NODES = 'CALLED_NODES'; -const OPTION_NODES_CALLED_WITH_NEW = 'NODES_CALLED_WITH_NEW'; +const OPTION_NODES_CALLED_WITH_OPTIONS = 'OPTION_NODES_CALLED_WITH_OPTIONS'; const OPTION_MUTATED_NODES = 'MUTATED_NODES'; const IGNORED_LABELS = 'IGNORED_LABELS'; const RESULT_KEY = {}; +const areCallOptionsEqual = ( options, otherOptions ) => options.withNew === otherOptions.withNew; + /** Wrapper to ensure immutability */ export default class ExecutionPathOptions { /** @@ -117,21 +117,6 @@ export default class ExecutionPathOptions { return this.set( OPTION_HAS_SAFE_THIS, value ); } - /** - * @return {boolean} - */ - calledWithNew () { - return this.get( OPTION_CALLED_WITH_NEW ); - } - - /** - * @param {boolean} [value=true] - * @return {ExecutionPathOptions} - */ - setCalledWithNew ( value = true ) { - return this.set( OPTION_CALLED_WITH_NEW, value ); - } - /** * @param {String[]} path * @param {Node} node @@ -186,47 +171,31 @@ export default class ExecutionPathOptions { /** * @param {Node} node + * @param {Object} callOptions * @return {ExecutionPathOptions} */ - addCalledNode ( node ) { - return this.setIn( [ OPTION_CALLED_NODES, node ], true ); - } - - /** - * @param {Node} node - * @return {boolean} - */ - hasNodeBeenCalled ( node ) { - return this._optionValues.getIn( [ OPTION_CALLED_NODES, node ] ); - } - - /** - * @param {Node} node - * @return {ExecutionPathOptions} - */ - addNodeCalledWithNew ( node ) { - return this.setIn( [ OPTION_NODES_CALLED_WITH_NEW, node ], true ); + addNodeCalledWithOptions ( node, callOptions ) { + return this.setIn( [ OPTION_NODES_CALLED_WITH_OPTIONS, node, callOptions ], true ); } /** * @param {Node} node + * @param {Object} callOptions * @return {boolean} */ - hasNodeBeenCalledWithNew ( node ) { - return this._optionValues.getIn( [ OPTION_NODES_CALLED_WITH_NEW, node ] ); + hasNodeBeenCalledWithOptions ( node, callOptions ) { + return this._optionValues.hasIn( [ OPTION_NODES_CALLED_WITH_OPTIONS, node ] ) + && this._optionValues.getIn( [ OPTION_NODES_CALLED_WITH_OPTIONS, node ] ) + .find( ( _, options ) => areCallOptionsEqual( options, callOptions ) ); } /** - * @param {Node} calledNode - * @param {boolean} withNew * @return {ExecutionPathOptions} */ - getHasEffectsWhenCalledOptions ( calledNode, { withNew = false } = {} ) { + getHasEffectsWhenCalledOptions () { return this - .addCalledNode( calledNode ) .setIgnoreReturnAwaitYield() .setIgnoreBreakStatements( false ) - .setIgnoreNoLabels() - .setCalledWithNew( withNew ); + .setIgnoreNoLabels(); } } diff --git a/src/ast/Node.js b/src/ast/Node.js index cf6dacca8b0..207a3961a71 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -80,9 +80,10 @@ export default class Node { /** * @param {String[]} path * @param {ExecutionPathOptions} options + * @param {Object} callOptions * @return {boolean} */ - hasEffectsWhenCalledAtPath ( path, options ) { + hasEffectsWhenCalledAtPath ( path, options, callOptions ) { return true; } diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index a76394b4ebe..4f1053ed127 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -27,6 +27,6 @@ export default class CallExpression extends Node { hasEffects ( options ) { return this.included || this.arguments.some( child => child.hasEffects( options ) ) - || this.callee.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions( this.callee ) ); + || this.callee.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions(), { withNew: false } ); } } diff --git a/src/ast/nodes/ClassBody.js b/src/ast/nodes/ClassBody.js index eca01e30b83..0ea2658316c 100644 --- a/src/ast/nodes/ClassBody.js +++ b/src/ast/nodes/ClassBody.js @@ -1,12 +1,12 @@ import Node from '../Node'; export default class ClassBody extends Node { - hasEffectsWhenCalledAtPath ( path, options ) { + hasEffectsWhenCalledAtPath ( path, options, callOptions ) { if ( path.length > 0 ) { return true; } if ( this.classConstructor ) { - return this.classConstructor.hasEffectsWhenCalledAtPath( [], options ); + return this.classConstructor.hasEffectsWhenCalledAtPath( [], options, callOptions ); } return false; } diff --git a/src/ast/nodes/ConditionalExpression.js b/src/ast/nodes/ConditionalExpression.js index 1da92459e77..2e2e40f8d01 100644 --- a/src/ast/nodes/ConditionalExpression.js +++ b/src/ast/nodes/ConditionalExpression.js @@ -55,16 +55,16 @@ export default class ConditionalExpression extends Node { ); } - hasEffectsWhenCalledAtPath ( path, options ) { + hasEffectsWhenCalledAtPath ( path, options, callOptions ) { return ( this.testValue === UNKNOWN_VALUE && ( - this.consequent.hasEffectsWhenCalledAtPath( path, options ) - || this.alternate.hasEffectsWhenCalledAtPath( path, options ) + this.consequent.hasEffectsWhenCalledAtPath( path, options, callOptions ) + || this.alternate.hasEffectsWhenCalledAtPath( path, options, callOptions ) ) ) || ( this.testValue - ? this.consequent.hasEffectsWhenCalledAtPath( path, options ) - : this.alternate.hasEffectsWhenCalledAtPath( path, options ) + ? this.consequent.hasEffectsWhenCalledAtPath( path, options, callOptions ) + : this.alternate.hasEffectsWhenCalledAtPath( path, options, callOptions ) ); } diff --git a/src/ast/nodes/ExportDefaultDeclaration.js b/src/ast/nodes/ExportDefaultDeclaration.js index e253b84421c..15bfc034665 100644 --- a/src/ast/nodes/ExportDefaultDeclaration.js +++ b/src/ast/nodes/ExportDefaultDeclaration.js @@ -11,8 +11,8 @@ export default class ExportDefaultDeclaration extends Node { this.declaration.bind(); } - hasEffectsWhenCalledAtPath ( path, options ) { - return this.declaration.hasEffectsWhenCalledAtPath( path, options ); + hasEffectsWhenCalledAtPath ( path, options, callOptions ) { + return this.declaration.hasEffectsWhenCalledAtPath( path, options, callOptions ); } includeDefaultExport () { diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index 006b1a14532..c40201cbb2d 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -30,9 +30,9 @@ export default class Identifier extends Node { || this.variable.hasEffectsWhenAssignedAtPath( path, options ); } - hasEffectsWhenCalledAtPath ( path, options ) { + hasEffectsWhenCalledAtPath ( path, options, callOptions ) { return !this.variable - || this.variable.hasEffectsWhenCalledAtPath( path, options ); + || this.variable.hasEffectsWhenCalledAtPath( path, options, callOptions ); } hasEffectsWhenMutatedAtPath ( path, options ) { diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index b164d905440..d99511c6dc7 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -108,12 +108,12 @@ export default class MemberExpression extends Node { return this.object.hasEffectsWhenAssignedAtPath( [ this.computed ? UNKNOWN_KEY : this.property.name, ...path ], options ); } - hasEffectsWhenCalledAtPath ( path, options ) { + hasEffectsWhenCalledAtPath ( path, options, callOptions ) { if ( this.variable ) { - return this.variable.hasEffectsWhenCalledAtPath( path, options ); + return this.variable.hasEffectsWhenCalledAtPath( path, options, callOptions ); } return this.computed - || this.object.hasEffectsWhenCalledAtPath( [ this.property.name, ...path ], options ); + || this.object.hasEffectsWhenCalledAtPath( [ this.property.name, ...path ], options, callOptions ); } hasEffectsWhenMutatedAtPath ( path, options ) { diff --git a/src/ast/nodes/MethodDefinition.js b/src/ast/nodes/MethodDefinition.js index c439b5caf33..fd661cbc0f6 100644 --- a/src/ast/nodes/MethodDefinition.js +++ b/src/ast/nodes/MethodDefinition.js @@ -5,10 +5,10 @@ export default class MethodDefinition extends Node { return this.key.hasEffects( options ); } - hasEffectsWhenCalledAtPath ( path, options ) { + hasEffectsWhenCalledAtPath ( path, options, callOptions ) { if ( path.length > 0 ) { return true; } - return this.value.hasEffectsWhenCalledAtPath( [], options ); + return this.value.hasEffectsWhenCalledAtPath( [], options, callOptions ); } } diff --git a/src/ast/nodes/NewExpression.js b/src/ast/nodes/NewExpression.js index 294a39db3de..0dc1c103957 100644 --- a/src/ast/nodes/NewExpression.js +++ b/src/ast/nodes/NewExpression.js @@ -4,7 +4,7 @@ export default class NewExpression extends Node { hasEffects ( options ) { return this.included || this.arguments.some( child => child.hasEffects( options ) ) - || this.callee.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions( this.callee, { withNew: true } ) ); + || this.callee.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions(), { withNew: true } ); } hasEffectsWhenAccessedAtPath ( path ) { diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index 2d48265b3f3..9c3d973cb5e 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -55,14 +55,14 @@ export default class ObjectExpression extends Node { && property.hasEffectsWhenAssignedAtPath( path.slice( 1 ), options ) ); } - hasEffectsWhenCalledAtPath ( path, options ) { + hasEffectsWhenCalledAtPath ( path, options, callOptions ) { 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 ), options ) ); + property.hasEffectsWhenCalledAtPath( path.slice( 1 ), options, callOptions ) ); } hasEffectsWhenMutatedAtPath ( path, options ) { diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index 6aca1538853..8cb1b69236d 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -15,7 +15,7 @@ export default class Property extends Node { hasEffectsWhenAccessedAtPath ( path, options ) { if ( this.kind === 'get' ) { return path.length > 0 - || this.value.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions( this.value ) ); + || this.value.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions(), { withNew: false } ); } return this.value.hasEffectsWhenAccessedAtPath( path, options ); } @@ -23,20 +23,20 @@ export default class Property extends Node { hasEffectsWhenAssignedAtPath ( path, options ) { if ( this.kind === 'set' ) { return path.length > 0 - || this.value.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions( this.value ) ); + || this.value.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions(), { withNew: false } ); } return this.value.hasEffectsWhenAssignedAtPath( path, options ); } - hasEffectsWhenCalledAtPath ( path, options ) { - if (this.kind === 'get') { + hasEffectsWhenCalledAtPath ( path, options, callOptions ) { + if ( this.kind === 'get' ) { return true; } - return this.value.hasEffectsWhenCalledAtPath( path, options ); + return this.value.hasEffectsWhenCalledAtPath( path, options, callOptions ); } hasEffectsWhenMutatedAtPath ( path, options ) { - if (this.kind === 'get') { + if ( this.kind === 'get' ) { return true; } return this.value.hasEffectsWhenMutatedAtPath( path, options ); diff --git a/src/ast/nodes/TaggedTemplateExpression.js b/src/ast/nodes/TaggedTemplateExpression.js index dd1aa384e11..9999d2fc74e 100644 --- a/src/ast/nodes/TaggedTemplateExpression.js +++ b/src/ast/nodes/TaggedTemplateExpression.js @@ -26,6 +26,6 @@ export default class TaggedTemplateExpression extends Node { hasEffects ( options ) { return super.hasEffects( options ) - || this.tag.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions( this.tag ) ); + || this.tag.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions(), { withNew: false } ); } } diff --git a/src/ast/nodes/shared/ClassNode.js b/src/ast/nodes/shared/ClassNode.js index 43efe1f83e0..3455a217c35 100644 --- a/src/ast/nodes/shared/ClassNode.js +++ b/src/ast/nodes/shared/ClassNode.js @@ -10,9 +10,9 @@ export default class ClassNode extends Node { return path.length > 1; } - hasEffectsWhenCalledAtPath ( path, options ) { - return this.body.hasEffectsWhenCalledAtPath( path, options ) - || ( this.superClass && this.superClass.hasEffectsWhenCalledAtPath( path, options ) ); + hasEffectsWhenCalledAtPath ( path, options, callOptions ) { + return this.body.hasEffectsWhenCalledAtPath( path, options, callOptions ) + || ( this.superClass && this.superClass.hasEffectsWhenCalledAtPath( path, options, callOptions ) ); } initialiseChildren () { diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index 77274ab3ad7..74e2b99ad1f 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -27,13 +27,11 @@ export default class FunctionNode extends Node { return true; } - hasEffectsWhenCalledAtPath ( path, options ) { + hasEffectsWhenCalledAtPath ( path, options, { withNew } ) { if ( path.length > 0 ) { return true; } - const innerOptions = options.calledWithNew() - ? options.setHasSafeThis( true ) - : options.setHasSafeThis( false ); + const innerOptions = options.setHasSafeThis( withNew ); return this.params.some( param => param.hasEffects( innerOptions ) ) || this.body.hasEffects( innerOptions ); } diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index 45797b33b53..0c1cb865df8 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -61,19 +61,16 @@ export default class LocalVariable extends Variable { && node.hasEffectsWhenAssignedAtPath( relativePath, options.addAssignedNodeAtPath( relativePath, node ) ) ); } - hasEffectsWhenCalledAtPath ( path, options ) { + hasEffectsWhenCalledAtPath ( path, options, callOptions ) { if ( path.length > MAX_PATH_LENGTH ) { return true; } return this.assignedExpressions.someAtPath( path, ( relativePath, node ) => { if ( relativePath.length === 0 ) { - return options.calledWithNew() - ? !options.hasNodeBeenCalledWithNew( node ) - && node.hasEffectsWhenCalledAtPath( [], options.addNodeCalledWithNew( node ) ) - : !options.hasNodeBeenCalled( node ) - && node.hasEffectsWhenCalledAtPath( [], options.addCalledNode( node ) ); + return !options.hasNodeBeenCalledWithOptions( node, callOptions ) + && node.hasEffectsWhenCalledAtPath( [], options.addNodeCalledWithOptions( node, callOptions ), callOptions ); } - return node.hasEffectsWhenCalledAtPath( relativePath, options ); + return node.hasEffectsWhenCalledAtPath( relativePath, options, callOptions ); } ); } diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index 5f37c30d1fc..c1eb5e40f44 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -48,9 +48,10 @@ export default class Variable { /** * @param {String[]} path * @param {ExecutionPathOptions} options + * @param {Object} callOptions * @return {boolean} */ - hasEffectsWhenCalledAtPath ( path, options ) { + hasEffectsWhenCalledAtPath ( path, options, callOptions ) { return true; } diff --git a/test/form/samples/proper-this-context/_config.js b/test/form/samples/proper-this-context/_config.js new file mode 100644 index 00000000000..cbad0901015 --- /dev/null +++ b/test/form/samples/proper-this-context/_config.js @@ -0,0 +1,4 @@ +module.exports = { + description: 'make sure "this" respects the context for arrow functions', + solo: false +}; diff --git a/test/form/samples/proper-this-context/_expected/amd.js b/test/form/samples/proper-this-context/_expected/amd.js new file mode 100644 index 00000000000..77c8c89fe85 --- /dev/null +++ b/test/form/samples/proper-this-context/_expected/amd.js @@ -0,0 +1,14 @@ +define(['exports'], function (exports) { 'use strict'; + + var buffer = new ArrayBuffer( 8 ); + + var view8 = new Int8Array( buffer ); + var view16 = new Int16Array( buffer ); + + view16[ 0 ] = 3; + + exports.view8 = view8; + + Object.defineProperty(exports, '__esModule', { value: true }); + +}); diff --git a/test/form/samples/proper-this-context/_expected/cjs.js b/test/form/samples/proper-this-context/_expected/cjs.js new file mode 100644 index 00000000000..1c6f45c06e4 --- /dev/null +++ b/test/form/samples/proper-this-context/_expected/cjs.js @@ -0,0 +1,12 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +var buffer = new ArrayBuffer( 8 ); + +var view8 = new Int8Array( buffer ); +var view16 = new Int16Array( buffer ); + +view16[ 0 ] = 3; + +exports.view8 = view8; diff --git a/test/form/samples/proper-this-context/_expected/es.js b/test/form/samples/proper-this-context/_expected/es.js new file mode 100644 index 00000000000..1227bc79aeb --- /dev/null +++ b/test/form/samples/proper-this-context/_expected/es.js @@ -0,0 +1,8 @@ +var buffer = new ArrayBuffer( 8 ); + +var view8 = new Int8Array( buffer ); +var view16 = new Int16Array( buffer ); + +view16[ 0 ] = 3; + +export { view8 }; diff --git a/test/form/samples/proper-this-context/_expected/iife.js b/test/form/samples/proper-this-context/_expected/iife.js new file mode 100644 index 00000000000..382aa202b72 --- /dev/null +++ b/test/form/samples/proper-this-context/_expected/iife.js @@ -0,0 +1,15 @@ +var bundle = (function (exports) { + 'use strict'; + + var buffer = new ArrayBuffer( 8 ); + + var view8 = new Int8Array( buffer ); + var view16 = new Int16Array( buffer ); + + view16[ 0 ] = 3; + + exports.view8 = view8; + + return exports; + +}({})); diff --git a/test/form/samples/proper-this-context/_expected/umd.js b/test/form/samples/proper-this-context/_expected/umd.js new file mode 100644 index 00000000000..0096b40667d --- /dev/null +++ b/test/form/samples/proper-this-context/_expected/umd.js @@ -0,0 +1,18 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (factory((global.bundle = {}))); +}(this, (function (exports) { 'use strict'; + + var buffer = new ArrayBuffer( 8 ); + + var view8 = new Int8Array( buffer ); + var view16 = new Int16Array( buffer ); + + view16[ 0 ] = 3; + + exports.view8 = view8; + + Object.defineProperty(exports, '__esModule', { value: true }); + +}))); diff --git a/test/form/samples/proper-this-context/main.js b/test/form/samples/proper-this-context/main.js new file mode 100644 index 00000000000..8754912c1b6 --- /dev/null +++ b/test/form/samples/proper-this-context/main.js @@ -0,0 +1,9 @@ +const mutateThis = () => { + this.x = 1; +}; + +function Test () { + mutateThis(); +} + +const test = new Test(); From 91e8a1dee1153803cf85ba9ab12c28d9d3ab2cf3 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Wed, 11 Oct 2017 08:22:57 +0200 Subject: [PATCH 34/76] * Treat "this" again like a real variable to fix an issue where "this" was inappropriately assumed to be a valid object. * To reflect that the value of "this" depends on the way a function is called, allow for adjusting the initial value of "this" via execution path options --- src/ast/ExecutionPathOptions.js | 34 ++++++++++--------- src/ast/nodes/ThisExpression.js | 11 +++--- src/ast/nodes/shared/FunctionNode.js | 9 ++++- src/ast/scopes/FunctionScope.js | 2 ++ src/ast/scopes/ModuleScope.js | 3 ++ ...laceableInitStructuredAssignmentTracker.js | 27 +++++++++++++++ src/ast/variables/ThisVariable.js | 34 +++++++++++++++++++ .../samples/proper-this-context/_config.js | 3 +- .../proper-this-context/_expected/amd.js | 17 +++++----- .../proper-this-context/_expected/cjs.js | 15 ++++---- .../proper-this-context/_expected/es.js | 13 +++---- .../proper-this-context/_expected/iife.js | 19 +++++------ .../proper-this-context/_expected/umd.js | 23 ++++++------- 13 files changed, 142 insertions(+), 68 deletions(-) create mode 100644 src/ast/variables/ReplaceableInitStructuredAssignmentTracker.js create mode 100644 src/ast/variables/ThisVariable.js diff --git a/src/ast/ExecutionPathOptions.js b/src/ast/ExecutionPathOptions.js index 69bd573a2af..86557d34e09 100644 --- a/src/ast/ExecutionPathOptions.js +++ b/src/ast/ExecutionPathOptions.js @@ -2,11 +2,11 @@ import Immutable from 'immutable'; const OPTION_IGNORE_BREAK_STATEMENTS = 'IGNORE_BREAK_STATEMENTS'; const OPTION_IGNORE_RETURN_AWAIT_YIELD = 'IGNORE_RETURN_AWAIT_YIELD'; -const OPTION_HAS_SAFE_THIS = 'HAS_SAFE_THIS'; const OPTION_ACCESSED_NODES = 'ACCESSED_NODES'; const OPTION_ASSIGNED_NODES = 'ASSIGNED_NODES'; const OPTION_NODES_CALLED_WITH_OPTIONS = 'OPTION_NODES_CALLED_WITH_OPTIONS'; const OPTION_MUTATED_NODES = 'MUTATED_NODES'; +const OPTION_VALID_THIS_VARIABLES = 'VALID_THIS_VARIABLES'; const IGNORED_LABELS = 'IGNORED_LABELS'; const RESULT_KEY = {}; @@ -102,21 +102,6 @@ export default class ExecutionPathOptions { return this.set( OPTION_IGNORE_RETURN_AWAIT_YIELD, value ); } - /** - * @return {boolean} - */ - hasSafeThis () { - return this.get( OPTION_HAS_SAFE_THIS ); - } - - /** - * @param {boolean} [value=true] - * @return {ExecutionPathOptions} - */ - setHasSafeThis ( value = true ) { - return this.set( OPTION_HAS_SAFE_THIS, value ); - } - /** * @param {String[]} path * @param {Node} node @@ -198,4 +183,21 @@ export default class ExecutionPathOptions { .setIgnoreBreakStatements( false ) .setIgnoreNoLabels(); } + + /** + * @param {ThisVariable} thisVariable + * @param {Node} init + * @return {ExecutionPathOptions} + */ + replaceThisInit ( thisVariable, init ) { + return this.setIn( [ OPTION_VALID_THIS_VARIABLES, thisVariable ], init ); + } + + /** + * @param {ThisVariable} thisVariable + * @returns {Node} + */ + getReplacedThisInit ( thisVariable ) { + return this._optionValues.getIn( [ OPTION_VALID_THIS_VARIABLES, thisVariable ] ); + } } diff --git a/src/ast/nodes/ThisExpression.js b/src/ast/nodes/ThisExpression.js index 3d5f935ca1b..f704fa00310 100644 --- a/src/ast/nodes/ThisExpression.js +++ b/src/ast/nodes/ThisExpression.js @@ -16,17 +16,20 @@ export default class ThisExpression extends Node { } } + bind () { + this.variable = this.scope.findVariable( 'this' ); + } + hasEffectsWhenAccessedAtPath ( path, options ) { - return !(path.length === 0 - || (path.length === 1 && options.hasSafeThis())); + return this.variable.hasEffectsWhenAccessedAtPath( path, options ); } hasEffectsWhenAssignedAtPath ( path, options ) { - return !(path.length === 1 && options.hasSafeThis()); + return this.variable.hasEffectsWhenAssignedAtPath( path, options ); } hasEffectsWhenMutatedAtPath ( path, options ) { - return !(path.length === 0 && options.hasSafeThis()); + return this.variable.hasEffectsWhenMutatedAtPath( path, options ); } render ( code ) { diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index 74e2b99ad1f..797f90258c1 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -1,8 +1,14 @@ import Node from '../../Node.js'; import FunctionScope from '../../scopes/FunctionScope'; import VirtualObjectExpression from './VirtualObjectExpression'; +import { UNKNOWN_ASSIGNMENT } from '../../values'; export default class FunctionNode extends Node { + bind () { + super.bind(); + this.thisVariable = this.scope.findVariable( 'this' ); + } + hasEffects ( options ) { return this.included || (this.id && this.id.hasEffects( options )); } @@ -31,7 +37,8 @@ export default class FunctionNode extends Node { if ( path.length > 0 ) { return true; } - const innerOptions = options.setHasSafeThis( withNew ); + const innerOptions = options.replaceThisInit( this.thisVariable, + withNew ? new VirtualObjectExpression() : UNKNOWN_ASSIGNMENT ); return this.params.some( param => param.hasEffects( innerOptions ) ) || this.body.hasEffects( innerOptions ); } diff --git a/src/ast/scopes/FunctionScope.js b/src/ast/scopes/FunctionScope.js index 170ff8ec2b5..916dcc246c3 100644 --- a/src/ast/scopes/FunctionScope.js +++ b/src/ast/scopes/FunctionScope.js @@ -1,10 +1,12 @@ import Scope from './Scope'; import ParameterVariable from '../variables/ParameterVariable'; +import ThisVariable from '../variables/ThisVariable'; export default class FunctionScope extends Scope { constructor ( options = {} ) { super( options ); this.variables.arguments = new ParameterVariable( 'arguments' ); + this.variables.this = new ThisVariable(); } findLexicalBoundary () { diff --git a/src/ast/scopes/ModuleScope.js b/src/ast/scopes/ModuleScope.js index 05049ec21a5..ebdc5c58cfd 100644 --- a/src/ast/scopes/ModuleScope.js +++ b/src/ast/scopes/ModuleScope.js @@ -1,6 +1,8 @@ import { forOwn } from '../../utils/object.js'; import relativeId from '../../utils/relativeId.js'; import Scope from './Scope.js'; +import LocalVariable from '../variables/LocalVariable'; +import UndefinedIdentifier from '../nodes/shared/UndefinedIdentifier'; export default class ModuleScope extends Scope { constructor ( module ) { @@ -10,6 +12,7 @@ export default class ModuleScope extends Scope { } ); this.module = module; + this.variables.this = new LocalVariable( 'this', null, new UndefinedIdentifier() ); } deshadow ( names ) { diff --git a/src/ast/variables/ReplaceableInitStructuredAssignmentTracker.js b/src/ast/variables/ReplaceableInitStructuredAssignmentTracker.js new file mode 100644 index 00000000000..611489d1ca6 --- /dev/null +++ b/src/ast/variables/ReplaceableInitStructuredAssignmentTracker.js @@ -0,0 +1,27 @@ +import StructuredAssignmentTracker from './StructuredAssignmentTracker'; + +export default class ReplaceableInitStructuredAssignmentTracker extends StructuredAssignmentTracker { + constructor ( init ) { + super(); + this._init = init; + } + + forEachAtPath ( path, callback ) { + callback( path, this._init ); + super.forEachAtPath( path, callback ); + } + + hasAtPath ( path, assignment ) { + return (path.length === 0 && assignment === this._init) + || super.hasAtPath( path, assignment ); + } + + setInit ( init ) { + this._init = init; + } + + someAtPath ( path, predicateFunction ) { + return predicateFunction( path, this._init ) + || super.someAtPath( path, predicateFunction ); + } +} diff --git a/src/ast/variables/ThisVariable.js b/src/ast/variables/ThisVariable.js new file mode 100644 index 00000000000..d0025a0e3ea --- /dev/null +++ b/src/ast/variables/ThisVariable.js @@ -0,0 +1,34 @@ +import LocalVariable from './LocalVariable'; +import ReplaceableInitStructuredAssignmentTracker from './ReplaceableInitStructuredAssignmentTracker'; +import { UNKNOWN_ASSIGNMENT } from '../values'; + +export default class ThisVariable extends LocalVariable { + constructor () { + super( 'this', null, null ); + this.assignedExpressions = new ReplaceableInitStructuredAssignmentTracker( UNKNOWN_ASSIGNMENT ); + } + + hasEffectsWhenAccessedAtPath ( path, options ) { + this._updateInit( options ); + return super.hasEffectsWhenAccessedAtPath( path, options ); + } + + hasEffectsWhenAssignedAtPath ( path, options ) { + this._updateInit( options ); + return super.hasEffectsWhenAssignedAtPath( path, options ); + } + + hasEffectsWhenCalledAtPath ( path, options, callOptions ) { + this._updateInit( options ); + return super.hasEffectsWhenCalledAtPath( path, options, callOptions ); + } + + hasEffectsWhenMutatedAtPath ( path, options ) { + this._updateInit( options ); + return super.hasEffectsWhenMutatedAtPath( path, options ); + } + + _updateInit ( options ) { + this.assignedExpressions.setInit( options.getReplacedThisInit( this ) || UNKNOWN_ASSIGNMENT ); + } +} diff --git a/test/form/samples/proper-this-context/_config.js b/test/form/samples/proper-this-context/_config.js index cbad0901015..261658effc3 100644 --- a/test/form/samples/proper-this-context/_config.js +++ b/test/form/samples/proper-this-context/_config.js @@ -1,4 +1,3 @@ module.exports = { - description: 'make sure "this" respects the context for arrow functions', - solo: false + description: 'make sure "this" respects the context for arrow functions' }; diff --git a/test/form/samples/proper-this-context/_expected/amd.js b/test/form/samples/proper-this-context/_expected/amd.js index 77c8c89fe85..bfe6aab64ae 100644 --- a/test/form/samples/proper-this-context/_expected/amd.js +++ b/test/form/samples/proper-this-context/_expected/amd.js @@ -1,14 +1,13 @@ -define(['exports'], function (exports) { 'use strict'; +define(function () { 'use strict'; - var buffer = new ArrayBuffer( 8 ); + const mutateThis = () => { + undefined.x = 1; + }; - var view8 = new Int8Array( buffer ); - var view16 = new Int16Array( buffer ); + function Test () { + mutateThis(); + } - view16[ 0 ] = 3; - - exports.view8 = view8; - - Object.defineProperty(exports, '__esModule', { value: true }); + const test = new Test(); }); diff --git a/test/form/samples/proper-this-context/_expected/cjs.js b/test/form/samples/proper-this-context/_expected/cjs.js index 1c6f45c06e4..ad5a19f8603 100644 --- a/test/form/samples/proper-this-context/_expected/cjs.js +++ b/test/form/samples/proper-this-context/_expected/cjs.js @@ -1,12 +1,11 @@ 'use strict'; -Object.defineProperty(exports, '__esModule', { value: true }); +const mutateThis = () => { + undefined.x = 1; +}; -var buffer = new ArrayBuffer( 8 ); +function Test () { + mutateThis(); +} -var view8 = new Int8Array( buffer ); -var view16 = new Int16Array( buffer ); - -view16[ 0 ] = 3; - -exports.view8 = view8; +const test = new Test(); diff --git a/test/form/samples/proper-this-context/_expected/es.js b/test/form/samples/proper-this-context/_expected/es.js index 1227bc79aeb..127ee2295d0 100644 --- a/test/form/samples/proper-this-context/_expected/es.js +++ b/test/form/samples/proper-this-context/_expected/es.js @@ -1,8 +1,9 @@ -var buffer = new ArrayBuffer( 8 ); +const mutateThis = () => { + undefined.x = 1; +}; -var view8 = new Int8Array( buffer ); -var view16 = new Int16Array( buffer ); +function Test () { + mutateThis(); +} -view16[ 0 ] = 3; - -export { view8 }; +const test = new Test(); diff --git a/test/form/samples/proper-this-context/_expected/iife.js b/test/form/samples/proper-this-context/_expected/iife.js index 382aa202b72..1bf1632a0cc 100644 --- a/test/form/samples/proper-this-context/_expected/iife.js +++ b/test/form/samples/proper-this-context/_expected/iife.js @@ -1,15 +1,14 @@ -var bundle = (function (exports) { +(function () { 'use strict'; - var buffer = new ArrayBuffer( 8 ); + const mutateThis = () => { + undefined.x = 1; + }; - var view8 = new Int8Array( buffer ); - var view16 = new Int16Array( buffer ); + function Test () { + mutateThis(); + } - view16[ 0 ] = 3; + const test = new Test(); - exports.view8 = view8; - - return exports; - -}({})); +}()); diff --git a/test/form/samples/proper-this-context/_expected/umd.js b/test/form/samples/proper-this-context/_expected/umd.js index 0096b40667d..4ec019cc050 100644 --- a/test/form/samples/proper-this-context/_expected/umd.js +++ b/test/form/samples/proper-this-context/_expected/umd.js @@ -1,18 +1,17 @@ (function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : - typeof define === 'function' && define.amd ? define(['exports'], factory) : - (factory((global.bundle = {}))); -}(this, (function (exports) { 'use strict'; + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; - var buffer = new ArrayBuffer( 8 ); + const mutateThis = () => { + undefined.x = 1; + }; - var view8 = new Int8Array( buffer ); - var view16 = new Int16Array( buffer ); + function Test () { + mutateThis(); + } - view16[ 0 ] = 3; - - exports.view8 = view8; - - Object.defineProperty(exports, '__esModule', { value: true }); + const test = new Test(); }))); From 796e7a5dfd790aec33e33443028ea4fb19c67c2a Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Tue, 10 Oct 2017 07:13:09 +0200 Subject: [PATCH 35/76] Implement basic return value handling for call expressions and body-less arrow functions. --- src/ast/Node.js | 14 ++++++++++++++ src/ast/nodes/ArrowFunctionExpression.js | 8 ++++++++ src/ast/nodes/CallExpression.js | 11 +++++++++++ .../arrow-function-return-values/_config.js | 3 +++ .../arrow-function-return-values/_expected/amd.js | 7 +++++++ .../arrow-function-return-values/_expected/cjs.js | 5 +++++ .../arrow-function-return-values/_expected/es.js | 3 +++ .../arrow-function-return-values/_expected/iife.js | 8 ++++++++ .../arrow-function-return-values/_expected/umd.js | 11 +++++++++++ .../samples/arrow-function-return-values/main.js | 5 +++++ 10 files changed, 75 insertions(+) create mode 100644 test/form/samples/arrow-function-return-values/_config.js create mode 100644 test/form/samples/arrow-function-return-values/_expected/amd.js create mode 100644 test/form/samples/arrow-function-return-values/_expected/cjs.js create mode 100644 test/form/samples/arrow-function-return-values/_expected/es.js create mode 100644 test/form/samples/arrow-function-return-values/_expected/iife.js create mode 100644 test/form/samples/arrow-function-return-values/_expected/umd.js create mode 100644 test/form/samples/arrow-function-return-values/main.js diff --git a/src/ast/Node.js b/src/ast/Node.js index 207a3961a71..a9fd5ccc2de 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -3,6 +3,7 @@ import { locate } from 'locate-character'; import { UNKNOWN_VALUE } from './values.js'; import ExecutionPathOptions from './ExecutionPathOptions'; +import { UNKNOWN_ASSIGNMENT } from './values'; export default class Node { constructor () { @@ -219,6 +220,19 @@ export default class Node { } ); } + /** + * Returns true if some possible return expression when called at the given + * path returns true. predicateFunction receives a `relativePath` and an `expression` + * which is called at this relative path as parameters. + * @param {String[]} path + * @param {Function} predicateFunction + * @returns {boolean} + */ + someReturnExpressionAtPath ( path, predicateFunction ) { + console.log( 'Node someReturnExpressionAtPath', path ); + return predicateFunction( path, UNKNOWN_ASSIGNMENT ); + } + toString () { return this.module.code.slice( this.start, this.end ); } diff --git a/src/ast/nodes/ArrowFunctionExpression.js b/src/ast/nodes/ArrowFunctionExpression.js index 1b8df457ef9..91f9c3238db 100644 --- a/src/ast/nodes/ArrowFunctionExpression.js +++ b/src/ast/nodes/ArrowFunctionExpression.js @@ -1,5 +1,6 @@ import Node from '../Node'; import Scope from '../scopes/Scope.js'; +import { UNKNOWN_ASSIGNMENT } from '../values'; export default class ArrowFunctionExpression extends Node { hasEffects () { @@ -41,4 +42,11 @@ export default class ArrowFunctionExpression extends Node { initialiseScope ( parentScope ) { this.scope = new Scope( { parent: parentScope } ); } + + someReturnExpressionAtPath ( path, predicateFunction ) { + if ( this.body.type !== 'BlockStatement' ) { + return predicateFunction( path, this.body ); + } + return predicateFunction( path, UNKNOWN_ASSIGNMENT ); + } } diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index 4f1053ed127..db8d6112b26 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -29,4 +29,15 @@ export default class CallExpression extends Node { || this.arguments.some( child => child.hasEffects( options ) ) || this.callee.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions(), { withNew: false } ); } + + hasEffectsWhenCalledAtPath ( path, options ) { + return this.callee.someReturnExpressionAtPath( path, ( relativePath, node ) => + node.hasEffectsWhenCalledAtPath( relativePath, options ) ); + } + + someReturnExpressionAtPath ( path, predicateFunction ) { + return this.callee.someReturnExpressionAtPath( path, ( relativePath, node ) => + node.someReturnExpressionAtPath( relativePath, ( relativeSubPath, subNode ) => + predicateFunction( relativeSubPath, subNode ) ) ); + } } diff --git a/test/form/samples/arrow-function-return-values/_config.js b/test/form/samples/arrow-function-return-values/_config.js new file mode 100644 index 00000000000..164c6b26455 --- /dev/null +++ b/test/form/samples/arrow-function-return-values/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'forwards return values of body-less arrow functions' +}; diff --git a/test/form/samples/arrow-function-return-values/_expected/amd.js b/test/form/samples/arrow-function-return-values/_expected/amd.js new file mode 100644 index 00000000000..654cebe9b35 --- /dev/null +++ b/test/form/samples/arrow-function-return-values/_expected/amd.js @@ -0,0 +1,7 @@ +define(function () { 'use strict'; + + (() => () => console.log( 'effect' ))()(); + + (() => () => () => console.log( 'effect' ))()()(); + +}); diff --git a/test/form/samples/arrow-function-return-values/_expected/cjs.js b/test/form/samples/arrow-function-return-values/_expected/cjs.js new file mode 100644 index 00000000000..40ec5181e21 --- /dev/null +++ b/test/form/samples/arrow-function-return-values/_expected/cjs.js @@ -0,0 +1,5 @@ +'use strict'; + +(() => () => console.log( 'effect' ))()(); + +(() => () => () => console.log( 'effect' ))()()(); diff --git a/test/form/samples/arrow-function-return-values/_expected/es.js b/test/form/samples/arrow-function-return-values/_expected/es.js new file mode 100644 index 00000000000..c979edda709 --- /dev/null +++ b/test/form/samples/arrow-function-return-values/_expected/es.js @@ -0,0 +1,3 @@ +(() => () => console.log( 'effect' ))()(); + +(() => () => () => console.log( 'effect' ))()()(); diff --git a/test/form/samples/arrow-function-return-values/_expected/iife.js b/test/form/samples/arrow-function-return-values/_expected/iife.js new file mode 100644 index 00000000000..bda88b58966 --- /dev/null +++ b/test/form/samples/arrow-function-return-values/_expected/iife.js @@ -0,0 +1,8 @@ +(function () { + 'use strict'; + + (() => () => console.log( 'effect' ))()(); + + (() => () => () => console.log( 'effect' ))()()(); + +}()); diff --git a/test/form/samples/arrow-function-return-values/_expected/umd.js b/test/form/samples/arrow-function-return-values/_expected/umd.js new file mode 100644 index 00000000000..f6bf7f2cb4d --- /dev/null +++ b/test/form/samples/arrow-function-return-values/_expected/umd.js @@ -0,0 +1,11 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + (() => () => console.log( 'effect' ))()(); + + (() => () => () => console.log( 'effect' ))()()(); + +}))); diff --git a/test/form/samples/arrow-function-return-values/main.js b/test/form/samples/arrow-function-return-values/main.js new file mode 100644 index 00000000000..e5ebfe3f82b --- /dev/null +++ b/test/form/samples/arrow-function-return-values/main.js @@ -0,0 +1,5 @@ +(() => () => {})()(); +(() => () => console.log( 'effect' ))()(); + +(() => () => () => {})()()(); +(() => () => () => console.log( 'effect' ))()()(); From 0b1e2095d1b5ff389949f0248b88199531a26283 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Tue, 10 Oct 2017 07:44:58 +0200 Subject: [PATCH 36/76] Add handling for identifiers and variables. TODO: Infinite recursion prevention. --- src/ast/Node.js | 1 - src/ast/nodes/CallExpression.js | 3 +-- src/ast/nodes/Identifier.js | 8 ++++++++ src/ast/variables/LocalVariable.js | 8 ++++++++ src/ast/variables/Variable.js | 11 +++++++++++ .../arrow-function-return-values/_expected/amd.js | 3 +++ .../arrow-function-return-values/_expected/cjs.js | 3 +++ .../arrow-function-return-values/_expected/es.js | 3 +++ .../arrow-function-return-values/_expected/iife.js | 3 +++ .../arrow-function-return-values/_expected/umd.js | 3 +++ .../form/samples/arrow-function-return-values/main.js | 5 +++++ 11 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/ast/Node.js b/src/ast/Node.js index a9fd5ccc2de..85f82edfa2f 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -229,7 +229,6 @@ export default class Node { * @returns {boolean} */ someReturnExpressionAtPath ( path, predicateFunction ) { - console.log( 'Node someReturnExpressionAtPath', path ); return predicateFunction( path, UNKNOWN_ASSIGNMENT ); } diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index db8d6112b26..7d9e13eb9c6 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -37,7 +37,6 @@ export default class CallExpression extends Node { someReturnExpressionAtPath ( path, predicateFunction ) { return this.callee.someReturnExpressionAtPath( path, ( relativePath, node ) => - node.someReturnExpressionAtPath( relativePath, ( relativeSubPath, subNode ) => - predicateFunction( relativeSubPath, subNode ) ) ); + node.someReturnExpressionAtPath( relativePath, predicateFunction ) ); } } diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index c40201cbb2d..aebcf2a236a 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -1,5 +1,6 @@ import Node from '../Node.js'; import isReference from 'is-reference'; +import { UNKNOWN_ASSIGNMENT } from '../values'; export default class Identifier extends Node { bind () { @@ -80,4 +81,11 @@ export default class Identifier extends Node { } } } + + someReturnExpressionAtPath ( path, predicateFunction ) { + if ( this.variable ) { + return this.variable.someReturnExpressionAtPath( path, predicateFunction ); + } + return predicateFunction( path, UNKNOWN_ASSIGNMENT ); + } } diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index 0c1cb865df8..9e8032abd38 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -96,6 +96,14 @@ export default class LocalVariable extends Variable { return hasBeenIncluded; } + someReturnExpressionAtPath ( path, predicateFunction ) { + if ( path.length > MAX_PATH_LENGTH ) { + return true; + } + return this.assignedExpressions.someAtPath( path, ( relativePath, node ) => + node.someReturnExpressionAtPath( relativePath, predicateFunction ) ); + } + toString () { return this.name; } diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index c1eb5e40f44..52480235427 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -1,5 +1,7 @@ /* eslint-disable no-unused-vars */ +import { UNKNOWN_ASSIGNMENT } from '../values'; + export default class Variable { constructor ( name ) { this.name = name; @@ -78,4 +80,13 @@ export default class Variable { this.included = true; return true; } + + /** + * @param {String[]} path + * @param {Function} predicateFunction + * @returns {boolean} + */ + someReturnExpressionAtPath ( path, predicateFunction ) { + return predicateFunction( path, UNKNOWN_ASSIGNMENT ); + } } diff --git a/test/form/samples/arrow-function-return-values/_expected/amd.js b/test/form/samples/arrow-function-return-values/_expected/amd.js index 654cebe9b35..1745d81b7ab 100644 --- a/test/form/samples/arrow-function-return-values/_expected/amd.js +++ b/test/form/samples/arrow-function-return-values/_expected/amd.js @@ -4,4 +4,7 @@ define(function () { 'use strict'; (() => () => () => console.log( 'effect' ))()()(); + const bar = () => () => console.log('effect'); + bar()(); + }); diff --git a/test/form/samples/arrow-function-return-values/_expected/cjs.js b/test/form/samples/arrow-function-return-values/_expected/cjs.js index 40ec5181e21..61f8287a0f2 100644 --- a/test/form/samples/arrow-function-return-values/_expected/cjs.js +++ b/test/form/samples/arrow-function-return-values/_expected/cjs.js @@ -3,3 +3,6 @@ (() => () => console.log( 'effect' ))()(); (() => () => () => console.log( 'effect' ))()()(); + +const bar = () => () => console.log('effect'); +bar()(); diff --git a/test/form/samples/arrow-function-return-values/_expected/es.js b/test/form/samples/arrow-function-return-values/_expected/es.js index c979edda709..b56ce30916a 100644 --- a/test/form/samples/arrow-function-return-values/_expected/es.js +++ b/test/form/samples/arrow-function-return-values/_expected/es.js @@ -1,3 +1,6 @@ (() => () => console.log( 'effect' ))()(); (() => () => () => console.log( 'effect' ))()()(); + +const bar = () => () => console.log('effect'); +bar()(); diff --git a/test/form/samples/arrow-function-return-values/_expected/iife.js b/test/form/samples/arrow-function-return-values/_expected/iife.js index bda88b58966..1bbd4b26cc5 100644 --- a/test/form/samples/arrow-function-return-values/_expected/iife.js +++ b/test/form/samples/arrow-function-return-values/_expected/iife.js @@ -5,4 +5,7 @@ (() => () => () => console.log( 'effect' ))()()(); + const bar = () => () => console.log('effect'); + bar()(); + }()); diff --git a/test/form/samples/arrow-function-return-values/_expected/umd.js b/test/form/samples/arrow-function-return-values/_expected/umd.js index f6bf7f2cb4d..7a697563c11 100644 --- a/test/form/samples/arrow-function-return-values/_expected/umd.js +++ b/test/form/samples/arrow-function-return-values/_expected/umd.js @@ -8,4 +8,7 @@ (() => () => () => console.log( 'effect' ))()()(); + const bar = () => () => console.log('effect'); + bar()(); + }))); diff --git a/test/form/samples/arrow-function-return-values/main.js b/test/form/samples/arrow-function-return-values/main.js index e5ebfe3f82b..8135004c12a 100644 --- a/test/form/samples/arrow-function-return-values/main.js +++ b/test/form/samples/arrow-function-return-values/main.js @@ -3,3 +3,8 @@ (() => () => () => {})()()(); (() => () => () => console.log( 'effect' ))()()(); + +const foo = () => () => {}; +foo()(); +const bar = () => () => console.log('effect'); +bar()(); From 58f32b42b83be07032ec2c004bb2fe3cd90a4661 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Wed, 11 Oct 2017 21:31:59 +0200 Subject: [PATCH 37/76] Add callOptions to someReturnExpressionAtPath and adjust argument order for hasEffectsWhenCalledAtPath --- src/ast/Node.js | 7 ++++--- src/ast/nodes/ArrowFunctionExpression.js | 4 ++-- src/ast/nodes/CallExpression.js | 14 +++++++------- src/ast/nodes/ClassBody.js | 4 ++-- src/ast/nodes/ConditionalExpression.js | 10 +++++----- src/ast/nodes/ExportDefaultDeclaration.js | 4 ++-- src/ast/nodes/Identifier.js | 8 ++++---- src/ast/nodes/MemberExpression.js | 6 +++--- src/ast/nodes/MethodDefinition.js | 4 ++-- src/ast/nodes/NewExpression.js | 2 +- src/ast/nodes/ObjectExpression.js | 4 ++-- src/ast/nodes/Property.js | 8 ++++---- src/ast/nodes/TaggedTemplateExpression.js | 2 +- src/ast/nodes/shared/ClassNode.js | 6 +++--- src/ast/nodes/shared/FunctionNode.js | 2 +- src/ast/variables/LocalVariable.js | 10 +++++----- src/ast/variables/ThisVariable.js | 4 ++-- src/ast/variables/Variable.js | 5 +++-- 18 files changed, 53 insertions(+), 51 deletions(-) diff --git a/src/ast/Node.js b/src/ast/Node.js index 85f82edfa2f..a1cff3d0c18 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -80,11 +80,11 @@ export default class Node { /** * @param {String[]} path - * @param {ExecutionPathOptions} options * @param {Object} callOptions + * @param {ExecutionPathOptions} options * @return {boolean} */ - hasEffectsWhenCalledAtPath ( path, options, callOptions ) { + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { return true; } @@ -225,10 +225,11 @@ export default class Node { * path returns true. predicateFunction receives a `relativePath` and an `expression` * which is called at this relative path as parameters. * @param {String[]} path + * @param {Object} callOptions * @param {Function} predicateFunction * @returns {boolean} */ - someReturnExpressionAtPath ( path, predicateFunction ) { + someReturnExpressionAtPath ( path, callOptions, predicateFunction ) { return predicateFunction( path, UNKNOWN_ASSIGNMENT ); } diff --git a/src/ast/nodes/ArrowFunctionExpression.js b/src/ast/nodes/ArrowFunctionExpression.js index 91f9c3238db..d82a2e16d05 100644 --- a/src/ast/nodes/ArrowFunctionExpression.js +++ b/src/ast/nodes/ArrowFunctionExpression.js @@ -18,7 +18,7 @@ export default class ArrowFunctionExpression extends Node { return this.hasEffectsWhenMutatedAtPath( path.slice( 1 ) ); } - hasEffectsWhenCalledAtPath ( path, options ) { + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { if ( path.length > 0 ) { return true; } @@ -43,7 +43,7 @@ export default class ArrowFunctionExpression extends Node { this.scope = new Scope( { parent: parentScope } ); } - someReturnExpressionAtPath ( path, predicateFunction ) { + someReturnExpressionAtPath ( path, callOptions, predicateFunction ) { if ( this.body.type !== 'BlockStatement' ) { return predicateFunction( path, this.body ); } diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index 7d9e13eb9c6..eacf6b0ca93 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -27,16 +27,16 @@ export default class CallExpression extends Node { hasEffects ( options ) { return this.included || this.arguments.some( child => child.hasEffects( options ) ) - || this.callee.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions(), { withNew: false } ); + || this.callee.hasEffectsWhenCalledAtPath( [], { withNew: false }, options.getHasEffectsWhenCalledOptions() ); } - hasEffectsWhenCalledAtPath ( path, options ) { - return this.callee.someReturnExpressionAtPath( path, ( relativePath, node ) => - node.hasEffectsWhenCalledAtPath( relativePath, options ) ); + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { + return this.callee.someReturnExpressionAtPath( path, { withNew: false }, ( relativePath, node ) => + node.hasEffectsWhenCalledAtPath( relativePath, callOptions, options ) ); } - someReturnExpressionAtPath ( path, predicateFunction ) { - return this.callee.someReturnExpressionAtPath( path, ( relativePath, node ) => - node.someReturnExpressionAtPath( relativePath, predicateFunction ) ); + someReturnExpressionAtPath ( path, callOptions, predicateFunction ) { + return this.callee.someReturnExpressionAtPath( path, { withNew: false }, ( relativePath, node ) => + node.someReturnExpressionAtPath( relativePath, callOptions, predicateFunction ) ); } } diff --git a/src/ast/nodes/ClassBody.js b/src/ast/nodes/ClassBody.js index 0ea2658316c..dab3873e2bd 100644 --- a/src/ast/nodes/ClassBody.js +++ b/src/ast/nodes/ClassBody.js @@ -1,12 +1,12 @@ import Node from '../Node'; export default class ClassBody extends Node { - hasEffectsWhenCalledAtPath ( path, options, callOptions ) { + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { if ( path.length > 0 ) { return true; } if ( this.classConstructor ) { - return this.classConstructor.hasEffectsWhenCalledAtPath( [], options, callOptions ); + return this.classConstructor.hasEffectsWhenCalledAtPath( [], callOptions, options ); } return false; } diff --git a/src/ast/nodes/ConditionalExpression.js b/src/ast/nodes/ConditionalExpression.js index 2e2e40f8d01..3272a894e25 100644 --- a/src/ast/nodes/ConditionalExpression.js +++ b/src/ast/nodes/ConditionalExpression.js @@ -55,16 +55,16 @@ export default class ConditionalExpression extends Node { ); } - hasEffectsWhenCalledAtPath ( path, options, callOptions ) { + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { return ( this.testValue === UNKNOWN_VALUE && ( - this.consequent.hasEffectsWhenCalledAtPath( path, options, callOptions ) - || this.alternate.hasEffectsWhenCalledAtPath( path, options, callOptions ) + this.consequent.hasEffectsWhenCalledAtPath( path, callOptions, options ) + || this.alternate.hasEffectsWhenCalledAtPath( path, callOptions, options ) ) ) || ( this.testValue - ? this.consequent.hasEffectsWhenCalledAtPath( path, options, callOptions ) - : this.alternate.hasEffectsWhenCalledAtPath( path, options, callOptions ) + ? this.consequent.hasEffectsWhenCalledAtPath( path, callOptions, options ) + : this.alternate.hasEffectsWhenCalledAtPath( path, callOptions, options ) ); } diff --git a/src/ast/nodes/ExportDefaultDeclaration.js b/src/ast/nodes/ExportDefaultDeclaration.js index 15bfc034665..39ff2af7bcf 100644 --- a/src/ast/nodes/ExportDefaultDeclaration.js +++ b/src/ast/nodes/ExportDefaultDeclaration.js @@ -11,8 +11,8 @@ export default class ExportDefaultDeclaration extends Node { this.declaration.bind(); } - hasEffectsWhenCalledAtPath ( path, options, callOptions ) { - return this.declaration.hasEffectsWhenCalledAtPath( path, options, callOptions ); + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { + return this.declaration.hasEffectsWhenCalledAtPath( path, callOptions, options ); } includeDefaultExport () { diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index aebcf2a236a..d76f19bfc27 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -31,9 +31,9 @@ export default class Identifier extends Node { || this.variable.hasEffectsWhenAssignedAtPath( path, options ); } - hasEffectsWhenCalledAtPath ( path, options, callOptions ) { + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { return !this.variable - || this.variable.hasEffectsWhenCalledAtPath( path, options, callOptions ); + || this.variable.hasEffectsWhenCalledAtPath( path, callOptions, options ); } hasEffectsWhenMutatedAtPath ( path, options ) { @@ -82,9 +82,9 @@ export default class Identifier extends Node { } } - someReturnExpressionAtPath ( path, predicateFunction ) { + someReturnExpressionAtPath ( path, callOptions, predicateFunction ) { if ( this.variable ) { - return this.variable.someReturnExpressionAtPath( path, predicateFunction ); + return this.variable.someReturnExpressionAtPath( path, callOptions, predicateFunction ); } return predicateFunction( path, UNKNOWN_ASSIGNMENT ); } diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index d99511c6dc7..dd834327177 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -108,12 +108,12 @@ export default class MemberExpression extends Node { return this.object.hasEffectsWhenAssignedAtPath( [ this.computed ? UNKNOWN_KEY : this.property.name, ...path ], options ); } - hasEffectsWhenCalledAtPath ( path, options, callOptions ) { + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { if ( this.variable ) { - return this.variable.hasEffectsWhenCalledAtPath( path, options, callOptions ); + return this.variable.hasEffectsWhenCalledAtPath( path, callOptions, options ); } return this.computed - || this.object.hasEffectsWhenCalledAtPath( [ this.property.name, ...path ], options, callOptions ); + || this.object.hasEffectsWhenCalledAtPath( [ this.property.name, ...path ], callOptions, options ); } hasEffectsWhenMutatedAtPath ( path, options ) { diff --git a/src/ast/nodes/MethodDefinition.js b/src/ast/nodes/MethodDefinition.js index fd661cbc0f6..e14ad731f18 100644 --- a/src/ast/nodes/MethodDefinition.js +++ b/src/ast/nodes/MethodDefinition.js @@ -5,10 +5,10 @@ export default class MethodDefinition extends Node { return this.key.hasEffects( options ); } - hasEffectsWhenCalledAtPath ( path, options, callOptions ) { + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { if ( path.length > 0 ) { return true; } - return this.value.hasEffectsWhenCalledAtPath( [], options, callOptions ); + return this.value.hasEffectsWhenCalledAtPath( [], callOptions, options ); } } diff --git a/src/ast/nodes/NewExpression.js b/src/ast/nodes/NewExpression.js index 0dc1c103957..8777c9db033 100644 --- a/src/ast/nodes/NewExpression.js +++ b/src/ast/nodes/NewExpression.js @@ -4,7 +4,7 @@ export default class NewExpression extends Node { hasEffects ( options ) { return this.included || this.arguments.some( child => child.hasEffects( options ) ) - || this.callee.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions(), { withNew: true } ); + || this.callee.hasEffectsWhenCalledAtPath( [], { withNew: true }, options.getHasEffectsWhenCalledOptions() ); } hasEffectsWhenAccessedAtPath ( path ) { diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index 9c3d973cb5e..8fa618e4576 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -55,14 +55,14 @@ export default class ObjectExpression extends Node { && property.hasEffectsWhenAssignedAtPath( path.slice( 1 ), options ) ); } - hasEffectsWhenCalledAtPath ( path, options, callOptions ) { + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { 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 ), options, callOptions ) ); + property.hasEffectsWhenCalledAtPath( path.slice( 1 ), callOptions, options ) ); } hasEffectsWhenMutatedAtPath ( path, options ) { diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index 8cb1b69236d..1a740a416c9 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -15,7 +15,7 @@ export default class Property extends Node { hasEffectsWhenAccessedAtPath ( path, options ) { if ( this.kind === 'get' ) { return path.length > 0 - || this.value.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions(), { withNew: false } ); + || this.value.hasEffectsWhenCalledAtPath( [], { withNew: false }, options.getHasEffectsWhenCalledOptions() ); } return this.value.hasEffectsWhenAccessedAtPath( path, options ); } @@ -23,16 +23,16 @@ export default class Property extends Node { hasEffectsWhenAssignedAtPath ( path, options ) { if ( this.kind === 'set' ) { return path.length > 0 - || this.value.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions(), { withNew: false } ); + || this.value.hasEffectsWhenCalledAtPath( [], { withNew: false }, options.getHasEffectsWhenCalledOptions() ); } return this.value.hasEffectsWhenAssignedAtPath( path, options ); } - hasEffectsWhenCalledAtPath ( path, options, callOptions ) { + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { if ( this.kind === 'get' ) { return true; } - return this.value.hasEffectsWhenCalledAtPath( path, options, callOptions ); + return this.value.hasEffectsWhenCalledAtPath( path, callOptions, options ); } hasEffectsWhenMutatedAtPath ( path, options ) { diff --git a/src/ast/nodes/TaggedTemplateExpression.js b/src/ast/nodes/TaggedTemplateExpression.js index 9999d2fc74e..da92ff9a224 100644 --- a/src/ast/nodes/TaggedTemplateExpression.js +++ b/src/ast/nodes/TaggedTemplateExpression.js @@ -26,6 +26,6 @@ export default class TaggedTemplateExpression extends Node { hasEffects ( options ) { return super.hasEffects( options ) - || this.tag.hasEffectsWhenCalledAtPath( [], options.getHasEffectsWhenCalledOptions(), { withNew: false } ); + || this.tag.hasEffectsWhenCalledAtPath( [], { withNew: false }, options.getHasEffectsWhenCalledOptions() ); } } diff --git a/src/ast/nodes/shared/ClassNode.js b/src/ast/nodes/shared/ClassNode.js index 3455a217c35..c7fe7773d2f 100644 --- a/src/ast/nodes/shared/ClassNode.js +++ b/src/ast/nodes/shared/ClassNode.js @@ -10,9 +10,9 @@ export default class ClassNode extends Node { return path.length > 1; } - hasEffectsWhenCalledAtPath ( path, options, callOptions ) { - return this.body.hasEffectsWhenCalledAtPath( path, options, callOptions ) - || ( this.superClass && this.superClass.hasEffectsWhenCalledAtPath( path, options, callOptions ) ); + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { + return this.body.hasEffectsWhenCalledAtPath( path, callOptions, options ) + || ( this.superClass && this.superClass.hasEffectsWhenCalledAtPath( path, callOptions, options ) ); } initialiseChildren () { diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index 797f90258c1..8fd1a502c45 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -33,7 +33,7 @@ export default class FunctionNode extends Node { return true; } - hasEffectsWhenCalledAtPath ( path, options, { withNew } ) { + hasEffectsWhenCalledAtPath ( path, { withNew }, options ) { if ( path.length > 0 ) { return true; } diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index 9e8032abd38..023c5a411b6 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -61,16 +61,16 @@ export default class LocalVariable extends Variable { && node.hasEffectsWhenAssignedAtPath( relativePath, options.addAssignedNodeAtPath( relativePath, node ) ) ); } - hasEffectsWhenCalledAtPath ( path, options, callOptions ) { + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { if ( path.length > MAX_PATH_LENGTH ) { return true; } return this.assignedExpressions.someAtPath( path, ( relativePath, node ) => { if ( relativePath.length === 0 ) { return !options.hasNodeBeenCalledWithOptions( node, callOptions ) - && node.hasEffectsWhenCalledAtPath( [], options.addNodeCalledWithOptions( node, callOptions ), callOptions ); + && node.hasEffectsWhenCalledAtPath( [], callOptions, options.addNodeCalledWithOptions( node, callOptions ) ); } - return node.hasEffectsWhenCalledAtPath( relativePath, options, callOptions ); + return node.hasEffectsWhenCalledAtPath( relativePath, callOptions, options ); } ); } @@ -96,12 +96,12 @@ export default class LocalVariable extends Variable { return hasBeenIncluded; } - someReturnExpressionAtPath ( path, predicateFunction ) { + someReturnExpressionAtPath ( path, callOptions, predicateFunction ) { if ( path.length > MAX_PATH_LENGTH ) { return true; } return this.assignedExpressions.someAtPath( path, ( relativePath, node ) => - node.someReturnExpressionAtPath( relativePath, predicateFunction ) ); + node.someReturnExpressionAtPath( relativePath, callOptions, predicateFunction ) ); } toString () { diff --git a/src/ast/variables/ThisVariable.js b/src/ast/variables/ThisVariable.js index d0025a0e3ea..f42308aab42 100644 --- a/src/ast/variables/ThisVariable.js +++ b/src/ast/variables/ThisVariable.js @@ -18,9 +18,9 @@ export default class ThisVariable extends LocalVariable { return super.hasEffectsWhenAssignedAtPath( path, options ); } - hasEffectsWhenCalledAtPath ( path, options, callOptions ) { + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { this._updateInit( options ); - return super.hasEffectsWhenCalledAtPath( path, options, callOptions ); + return super.hasEffectsWhenCalledAtPath( path, callOptions, options ); } hasEffectsWhenMutatedAtPath ( path, options ) { diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index 52480235427..b5554e0b8de 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -53,7 +53,7 @@ export default class Variable { * @param {Object} callOptions * @return {boolean} */ - hasEffectsWhenCalledAtPath ( path, options, callOptions ) { + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { return true; } @@ -83,10 +83,11 @@ export default class Variable { /** * @param {String[]} path + * @param {Object} callOptions * @param {Function} predicateFunction * @returns {boolean} */ - someReturnExpressionAtPath ( path, predicateFunction ) { + someReturnExpressionAtPath ( path, callOptions, predicateFunction ) { return predicateFunction( path, UNKNOWN_ASSIGNMENT ); } } From ee3cb659a9c8b8148003c6f85799ec102135a95c Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Thu, 12 Oct 2017 08:44:00 +0200 Subject: [PATCH 38/76] Prevent some type of infinite recursion for CallExpressions. TODO: Do the same for other kinds of calls, awaiting better testability --- src/ast/ExecutionPathOptions.js | 7 +++++-- src/ast/nodes/CallExpression.js | 14 +++++++++++--- src/ast/nodes/NewExpression.js | 7 ++++++- src/ast/nodes/Property.js | 11 +++++++++-- src/ast/nodes/TaggedTemplateExpression.js | 7 ++++++- test/form/samples/recursive-calls/_config.js | 3 +++ test/form/samples/recursive-calls/_expected/amd.js | 5 +++++ test/form/samples/recursive-calls/_expected/cjs.js | 2 ++ test/form/samples/recursive-calls/_expected/es.js | 1 + .../form/samples/recursive-calls/_expected/iife.js | 6 ++++++ test/form/samples/recursive-calls/_expected/umd.js | 9 +++++++++ test/form/samples/recursive-calls/main.js | 5 +++++ 12 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 test/form/samples/recursive-calls/_config.js create mode 100644 test/form/samples/recursive-calls/_expected/amd.js create mode 100644 test/form/samples/recursive-calls/_expected/cjs.js create mode 100644 test/form/samples/recursive-calls/_expected/es.js create mode 100644 test/form/samples/recursive-calls/_expected/iife.js create mode 100644 test/form/samples/recursive-calls/_expected/umd.js create mode 100644 test/form/samples/recursive-calls/main.js diff --git a/src/ast/ExecutionPathOptions.js b/src/ast/ExecutionPathOptions.js index 86557d34e09..9873300bcf4 100644 --- a/src/ast/ExecutionPathOptions.js +++ b/src/ast/ExecutionPathOptions.js @@ -175,13 +175,16 @@ export default class ExecutionPathOptions { } /** + * @param {Node} node + * @param {Object} callOptions * @return {ExecutionPathOptions} */ - getHasEffectsWhenCalledOptions () { + getHasEffectsWhenCalledOptions ( node, callOptions ) { return this .setIgnoreReturnAwaitYield() .setIgnoreBreakStatements( false ) - .setIgnoreNoLabels(); + .setIgnoreNoLabels() + .addNodeCalledWithOptions( node, callOptions ); } /** diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index eacf6b0ca93..e4016fce0fb 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -27,16 +27,24 @@ export default class CallExpression extends Node { hasEffects ( options ) { return this.included || this.arguments.some( child => child.hasEffects( options ) ) - || this.callee.hasEffectsWhenCalledAtPath( [], { withNew: false }, options.getHasEffectsWhenCalledOptions() ); + || ( + !options.hasNodeBeenCalledWithOptions( this, this._callOptions ) + && this.callee.hasEffectsWhenCalledAtPath( [], this._callOptions, + options.getHasEffectsWhenCalledOptions( this, this._callOptions ) ) + ); } hasEffectsWhenCalledAtPath ( path, callOptions, options ) { - return this.callee.someReturnExpressionAtPath( path, { withNew: false }, ( relativePath, node ) => + return this.callee.someReturnExpressionAtPath( path, this._callOptions, ( relativePath, node ) => node.hasEffectsWhenCalledAtPath( relativePath, callOptions, options ) ); } + initialiseNode () { + this._callOptions = { withNew: false }; + } + someReturnExpressionAtPath ( path, callOptions, predicateFunction ) { - return this.callee.someReturnExpressionAtPath( path, { withNew: false }, ( relativePath, node ) => + return this.callee.someReturnExpressionAtPath( path, this._callOptions, ( relativePath, node ) => node.someReturnExpressionAtPath( relativePath, callOptions, predicateFunction ) ); } } diff --git a/src/ast/nodes/NewExpression.js b/src/ast/nodes/NewExpression.js index 8777c9db033..df791075e22 100644 --- a/src/ast/nodes/NewExpression.js +++ b/src/ast/nodes/NewExpression.js @@ -4,10 +4,15 @@ export default class NewExpression extends Node { hasEffects ( options ) { return this.included || this.arguments.some( child => child.hasEffects( options ) ) - || this.callee.hasEffectsWhenCalledAtPath( [], { withNew: true }, options.getHasEffectsWhenCalledOptions() ); + || this.callee.hasEffectsWhenCalledAtPath( [], this._callOptions, + options.getHasEffectsWhenCalledOptions( this, this._callOptions ) ); } hasEffectsWhenAccessedAtPath ( path ) { return path.length > 1; } + + initialiseNode () { + this._callOptions = { withNew: true }; + } } diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index 1a740a416c9..bddd4bdab6d 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -15,7 +15,8 @@ export default class Property extends Node { hasEffectsWhenAccessedAtPath ( path, options ) { if ( this.kind === 'get' ) { return path.length > 0 - || this.value.hasEffectsWhenCalledAtPath( [], { withNew: false }, options.getHasEffectsWhenCalledOptions() ); + || this.value.hasEffectsWhenCalledAtPath( [], this._callOptions, + options.getHasEffectsWhenCalledOptions( this, this._callOptions ) ); } return this.value.hasEffectsWhenAccessedAtPath( path, options ); } @@ -23,7 +24,8 @@ export default class Property extends Node { hasEffectsWhenAssignedAtPath ( path, options ) { if ( this.kind === 'set' ) { return path.length > 0 - || this.value.hasEffectsWhenCalledAtPath( [], { withNew: false }, options.getHasEffectsWhenCalledOptions() ); + || this.value.hasEffectsWhenCalledAtPath( [], this._callOptions, + options.getHasEffectsWhenCalledOptions( this, this._callOptions ) ); } return this.value.hasEffectsWhenAssignedAtPath( path, options ); } @@ -44,10 +46,15 @@ export default class Property extends Node { initialiseAndDeclare ( parentScope, kind ) { this.initialiseScope( parentScope ); + this.initialiseNode( parentScope ); this.key.initialise( parentScope ); this.value.initialiseAndDeclare( parentScope, kind, UNKNOWN_ASSIGNMENT ); } + initialiseNode () { + this._callOptions = { withNew: false }; + } + render ( code, es ) { if ( !this.shorthand ) { this.key.render( code, es ); diff --git a/src/ast/nodes/TaggedTemplateExpression.js b/src/ast/nodes/TaggedTemplateExpression.js index da92ff9a224..83f3ecba8de 100644 --- a/src/ast/nodes/TaggedTemplateExpression.js +++ b/src/ast/nodes/TaggedTemplateExpression.js @@ -26,6 +26,11 @@ export default class TaggedTemplateExpression extends Node { hasEffects ( options ) { return super.hasEffects( options ) - || this.tag.hasEffectsWhenCalledAtPath( [], { withNew: false }, options.getHasEffectsWhenCalledOptions() ); + || this.tag.hasEffectsWhenCalledAtPath( [], this._callOptions, + options.getHasEffectsWhenCalledOptions( this, this._callOptions ) ); + } + + initialiseNode () { + this._callOptions = { withNew: false }; } } diff --git a/test/form/samples/recursive-calls/_config.js b/test/form/samples/recursive-calls/_config.js new file mode 100644 index 00000000000..7258d5addec --- /dev/null +++ b/test/form/samples/recursive-calls/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'do not fail for recursive calls' +}; diff --git a/test/form/samples/recursive-calls/_expected/amd.js b/test/form/samples/recursive-calls/_expected/amd.js new file mode 100644 index 00000000000..f9f8229aa40 --- /dev/null +++ b/test/form/samples/recursive-calls/_expected/amd.js @@ -0,0 +1,5 @@ +define(function () { 'use strict'; + + + +}); diff --git a/test/form/samples/recursive-calls/_expected/cjs.js b/test/form/samples/recursive-calls/_expected/cjs.js new file mode 100644 index 00000000000..eb109abbed0 --- /dev/null +++ b/test/form/samples/recursive-calls/_expected/cjs.js @@ -0,0 +1,2 @@ +'use strict'; + diff --git a/test/form/samples/recursive-calls/_expected/es.js b/test/form/samples/recursive-calls/_expected/es.js new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/test/form/samples/recursive-calls/_expected/es.js @@ -0,0 +1 @@ + diff --git a/test/form/samples/recursive-calls/_expected/iife.js b/test/form/samples/recursive-calls/_expected/iife.js new file mode 100644 index 00000000000..43ef5426880 --- /dev/null +++ b/test/form/samples/recursive-calls/_expected/iife.js @@ -0,0 +1,6 @@ +(function () { + 'use strict'; + + + +}()); diff --git a/test/form/samples/recursive-calls/_expected/umd.js b/test/form/samples/recursive-calls/_expected/umd.js new file mode 100644 index 00000000000..07ce27e42f1 --- /dev/null +++ b/test/form/samples/recursive-calls/_expected/umd.js @@ -0,0 +1,9 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + + +}))); diff --git a/test/form/samples/recursive-calls/main.js b/test/form/samples/recursive-calls/main.js new file mode 100644 index 00000000000..ba0d2a89876 --- /dev/null +++ b/test/form/samples/recursive-calls/main.js @@ -0,0 +1,5 @@ +const removed1 = () => removed1(); +removed1(); + +const removed2 = () => () => removed2()(); +removed2()(); From b683e847625f438db11cb9b9a539c369ef38be07 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Fri, 13 Oct 2017 07:50:28 +0200 Subject: [PATCH 39/76] Extract return values from function scopes. --- src/ast/nodes/CallExpression.js | 15 +++++ src/ast/nodes/ReturnStatement.js | 4 ++ src/ast/nodes/shared/FunctionNode.js | 9 +++ src/ast/scopes/FunctionScope.js | 4 +- src/ast/scopes/ReturnValueScope.js | 17 ++++++ src/ast/scopes/Scope.js | 4 ++ .../function-body-return-values/_config.js | 3 + .../_expected/amd.js | 42 ++++++++++++++ .../_expected/cjs.js | 40 +++++++++++++ .../_expected/es.js | 38 +++++++++++++ .../_expected/iife.js | 43 ++++++++++++++ .../_expected/umd.js | 46 +++++++++++++++ .../function-body-return-values/main.js | 56 +++++++++++++++++++ 13 files changed, 319 insertions(+), 2 deletions(-) create mode 100644 src/ast/scopes/ReturnValueScope.js create mode 100644 test/form/samples/function-body-return-values/_config.js create mode 100644 test/form/samples/function-body-return-values/_expected/amd.js create mode 100644 test/form/samples/function-body-return-values/_expected/cjs.js create mode 100644 test/form/samples/function-body-return-values/_expected/es.js create mode 100644 test/form/samples/function-body-return-values/_expected/iife.js create mode 100644 test/form/samples/function-body-return-values/_expected/umd.js create mode 100644 test/form/samples/function-body-return-values/main.js diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index e4016fce0fb..24eac537c7c 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -34,11 +34,26 @@ export default class CallExpression extends Node { ); } + hasEffectsWhenAccessedAtPath ( path, options ) { + return this.callee.someReturnExpressionAtPath( path, this._callOptions, ( relativePath, node ) => + node.hasEffectsWhenAccessedAtPath( relativePath, options ) ); + } + + hasEffectsWhenAssignedAtPath ( path, options ) { + return this.callee.someReturnExpressionAtPath( path, this._callOptions, ( relativePath, node ) => + node.hasEffectsWhenAssignedAtPath( relativePath, options ) ); + } + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { return this.callee.someReturnExpressionAtPath( path, this._callOptions, ( relativePath, node ) => node.hasEffectsWhenCalledAtPath( relativePath, callOptions, options ) ); } + hasEffectsWhenMutatedAtPath ( path, options ) { + return this.callee.someReturnExpressionAtPath( path, this._callOptions, ( relativePath, node ) => + node.hasEffectsWhenMutatedAtPath( relativePath, options ) ); + } + initialiseNode () { this._callOptions = { withNew: false }; } diff --git a/src/ast/nodes/ReturnStatement.js b/src/ast/nodes/ReturnStatement.js index c893281b2b8..b869575c1dc 100644 --- a/src/ast/nodes/ReturnStatement.js +++ b/src/ast/nodes/ReturnStatement.js @@ -5,4 +5,8 @@ export default class ReturnStatement extends Statement { return super.hasEffects( options ) || !options.ignoreReturnAwaitYield(); } + + initialiseNode () { + this.scope.addReturnExpression( this.argument ); + } } diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index 8fd1a502c45..6caa0be976e 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -1,6 +1,7 @@ import Node from '../../Node.js'; import FunctionScope from '../../scopes/FunctionScope'; import VirtualObjectExpression from './VirtualObjectExpression'; +import UndefinedIdentifier from './UndefinedIdentifier'; import { UNKNOWN_ASSIGNMENT } from '../../values'; export default class FunctionNode extends Node { @@ -55,9 +56,17 @@ export default class FunctionNode extends Node { initialiseNode () { this.prototypeObject = new VirtualObjectExpression(); + const lastBodyStatement = this.body.body[ this.body.body.length - 1 ]; + if ( !lastBodyStatement || lastBodyStatement.type !== 'ReturnStatement' ) { + this.scope.addReturnExpression( new UndefinedIdentifier() ); + } } initialiseScope ( parentScope ) { this.scope = new FunctionScope( { parent: parentScope } ); } + + someReturnExpressionAtPath ( path, callOptions, predicateFunction ) { + return this.scope.someReturnExpressionAtPath( path, callOptions, predicateFunction ); + } } diff --git a/src/ast/scopes/FunctionScope.js b/src/ast/scopes/FunctionScope.js index 916dcc246c3..b26678adb8e 100644 --- a/src/ast/scopes/FunctionScope.js +++ b/src/ast/scopes/FunctionScope.js @@ -1,8 +1,8 @@ -import Scope from './Scope'; +import ReturnValueScope from './ReturnValueScope'; import ParameterVariable from '../variables/ParameterVariable'; import ThisVariable from '../variables/ThisVariable'; -export default class FunctionScope extends Scope { +export default class FunctionScope extends ReturnValueScope { constructor ( options = {} ) { super( options ); this.variables.arguments = new ParameterVariable( 'arguments' ); diff --git a/src/ast/scopes/ReturnValueScope.js b/src/ast/scopes/ReturnValueScope.js new file mode 100644 index 00000000000..ee074166958 --- /dev/null +++ b/src/ast/scopes/ReturnValueScope.js @@ -0,0 +1,17 @@ +import Scope from './Scope'; + +export default class ReturnValueScope extends Scope { + constructor ( options = {} ) { + super( options ); + this._returnExpressions = new Set(); + } + + addReturnExpression ( expression ) { + this._returnExpressions.add( expression ); + } + + someReturnExpressionAtPath ( path, callOptions, predicateFunction ) { + return Array.from( this._returnExpressions ).some( returnExpression => + predicateFunction( path, returnExpression ) ); + } +} diff --git a/src/ast/scopes/Scope.js b/src/ast/scopes/Scope.js index c067a8af554..7e958734e3a 100644 --- a/src/ast/scopes/Scope.js +++ b/src/ast/scopes/Scope.js @@ -45,6 +45,10 @@ export default class Scope { return this.variables[ name ]; } + addReturnExpression ( expression ) { + this.parent && this.parent.addReturnExpression( expression ); + } + contains ( name ) { return !!this.variables[ name ] || ( this.parent ? this.parent.contains( name ) : false ); diff --git a/test/form/samples/function-body-return-values/_config.js b/test/form/samples/function-body-return-values/_config.js new file mode 100644 index 00000000000..4fd56e83a8a --- /dev/null +++ b/test/form/samples/function-body-return-values/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'properly extract return values from function bodies' +}; diff --git a/test/form/samples/function-body-return-values/_expected/amd.js b/test/form/samples/function-body-return-values/_expected/amd.js new file mode 100644 index 00000000000..c9cae50e93a --- /dev/null +++ b/test/form/samples/function-body-return-values/_expected/amd.js @@ -0,0 +1,42 @@ +define(function () { 'use strict'; + + function retained1 () { + return () => console.log( 'effect' ); + } + + retained1()(); + + function retained2 () { + if ( globalCondition ) { + return () => console.log( 'effect' ); + } + return () => {}; + } + + retained2()(); + + function retained3 () { + if ( globalCondition ) { + return () => {}; + } + } + + retained3()(); + + function retained4 () {} + + retained4()(); + + function retained5 () { + return {}; + } + + retained5().x.y = 1; + + function retained6 () { + return { x: () => console.log('effect') }; + } + + retained6().x(); + +}); diff --git a/test/form/samples/function-body-return-values/_expected/cjs.js b/test/form/samples/function-body-return-values/_expected/cjs.js new file mode 100644 index 00000000000..a8bf384102c --- /dev/null +++ b/test/form/samples/function-body-return-values/_expected/cjs.js @@ -0,0 +1,40 @@ +'use strict'; + +function retained1 () { + return () => console.log( 'effect' ); +} + +retained1()(); + +function retained2 () { + if ( globalCondition ) { + return () => console.log( 'effect' ); + } + return () => {}; +} + +retained2()(); + +function retained3 () { + if ( globalCondition ) { + return () => {}; + } +} + +retained3()(); + +function retained4 () {} + +retained4()(); + +function retained5 () { + return {}; +} + +retained5().x.y = 1; + +function retained6 () { + return { x: () => console.log('effect') }; +} + +retained6().x(); diff --git a/test/form/samples/function-body-return-values/_expected/es.js b/test/form/samples/function-body-return-values/_expected/es.js new file mode 100644 index 00000000000..e1646edce5d --- /dev/null +++ b/test/form/samples/function-body-return-values/_expected/es.js @@ -0,0 +1,38 @@ +function retained1 () { + return () => console.log( 'effect' ); +} + +retained1()(); + +function retained2 () { + if ( globalCondition ) { + return () => console.log( 'effect' ); + } + return () => {}; +} + +retained2()(); + +function retained3 () { + if ( globalCondition ) { + return () => {}; + } +} + +retained3()(); + +function retained4 () {} + +retained4()(); + +function retained5 () { + return {}; +} + +retained5().x.y = 1; + +function retained6 () { + return { x: () => console.log('effect') }; +} + +retained6().x(); diff --git a/test/form/samples/function-body-return-values/_expected/iife.js b/test/form/samples/function-body-return-values/_expected/iife.js new file mode 100644 index 00000000000..bd70f35e821 --- /dev/null +++ b/test/form/samples/function-body-return-values/_expected/iife.js @@ -0,0 +1,43 @@ +(function () { + 'use strict'; + + function retained1 () { + return () => console.log( 'effect' ); + } + + retained1()(); + + function retained2 () { + if ( globalCondition ) { + return () => console.log( 'effect' ); + } + return () => {}; + } + + retained2()(); + + function retained3 () { + if ( globalCondition ) { + return () => {}; + } + } + + retained3()(); + + function retained4 () {} + + retained4()(); + + function retained5 () { + return {}; + } + + retained5().x.y = 1; + + function retained6 () { + return { x: () => console.log('effect') }; + } + + retained6().x(); + +}()); diff --git a/test/form/samples/function-body-return-values/_expected/umd.js b/test/form/samples/function-body-return-values/_expected/umd.js new file mode 100644 index 00000000000..30bd6862c9f --- /dev/null +++ b/test/form/samples/function-body-return-values/_expected/umd.js @@ -0,0 +1,46 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + function retained1 () { + return () => console.log( 'effect' ); + } + + retained1()(); + + function retained2 () { + if ( globalCondition ) { + return () => console.log( 'effect' ); + } + return () => {}; + } + + retained2()(); + + function retained3 () { + if ( globalCondition ) { + return () => {}; + } + } + + retained3()(); + + function retained4 () {} + + retained4()(); + + function retained5 () { + return {}; + } + + retained5().x.y = 1; + + function retained6 () { + return { x: () => console.log('effect') }; + } + + retained6().x(); + +}))); diff --git a/test/form/samples/function-body-return-values/main.js b/test/form/samples/function-body-return-values/main.js new file mode 100644 index 00000000000..24e2a8a0611 --- /dev/null +++ b/test/form/samples/function-body-return-values/main.js @@ -0,0 +1,56 @@ +function removed1 () { + return () => {}; +} + +removed1()(); + +function removed2 () { + return { x: {} }; +} + +removed2().x.y = 1; + +function removed3 () { + return { x: () => {} }; +} + +removed3().x(); + +function retained1 () { + return () => console.log( 'effect' ); +} + +retained1()(); + +function retained2 () { + if ( globalCondition ) { + return () => console.log( 'effect' ); + } + return () => {}; +} + +retained2()(); + +function retained3 () { + if ( globalCondition ) { + return () => {}; + } +} + +retained3()(); + +function retained4 () {} + +retained4()(); + +function retained5 () { + return {}; +} + +retained5().x.y = 1; + +function retained6 () { + return { x: () => console.log('effect') }; +} + +retained6().x(); From b106a56dd813b5e469256eb99a57bab6ee5053a6 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Fri, 13 Oct 2017 08:43:01 +0200 Subject: [PATCH 40/76] Retire hasEffectsWhenMutated as everything can be done better via hasEffectsWhenAssigned. --- src/ast/Node.js | 9 --------- src/ast/nodes/ArrowFunctionExpression.js | 9 +-------- src/ast/nodes/CallExpression.js | 11 +++++------ src/ast/nodes/Identifier.js | 5 ----- src/ast/nodes/Literal.js | 7 ------- src/ast/nodes/LogicalExpression.js | 16 ++++------------ src/ast/nodes/MemberExpression.js | 10 ---------- src/ast/nodes/ObjectExpression.js | 10 ---------- src/ast/nodes/Property.js | 7 ------- src/ast/nodes/ThisExpression.js | 4 ---- src/ast/nodes/UnaryExpression.js | 5 +---- src/ast/nodes/shared/FunctionNode.js | 10 ---------- src/ast/nodes/shared/UndefinedIdentifier.js | 4 ---- src/ast/nodes/shared/VirtualNumberLiteral.js | 4 ---- src/ast/nodes/shared/VirtualObjectExpression.js | 4 ---- src/ast/values.js | 1 - src/ast/variables/LocalVariable.js | 17 +---------------- src/ast/variables/ThisVariable.js | 5 ----- src/ast/variables/Variable.js | 9 --------- 19 files changed, 12 insertions(+), 135 deletions(-) diff --git a/src/ast/Node.js b/src/ast/Node.js index a1cff3d0c18..6623680cbda 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -88,15 +88,6 @@ export default class Node { return true; } - /** - * @param {String[]} path - * @param {ExecutionPathOptions} options - * @return {boolean} - */ - hasEffectsWhenMutatedAtPath ( path, options ) { - return true; - } - /** * Includes the node in the bundle. Children are usually included if they are * necessary for this node (e.g. a function body) or if they have effects. diff --git a/src/ast/nodes/ArrowFunctionExpression.js b/src/ast/nodes/ArrowFunctionExpression.js index d82a2e16d05..b9555be498f 100644 --- a/src/ast/nodes/ArrowFunctionExpression.js +++ b/src/ast/nodes/ArrowFunctionExpression.js @@ -12,10 +12,7 @@ export default class ArrowFunctionExpression extends Node { } hasEffectsWhenAssignedAtPath ( path ) { - if ( path.length === 0 ) { - return true; - } - return this.hasEffectsWhenMutatedAtPath( path.slice( 1 ) ); + return path.length > 1; } hasEffectsWhenCalledAtPath ( path, callOptions, options ) { @@ -26,10 +23,6 @@ export default class ArrowFunctionExpression extends Node { || this.body.hasEffects( options ); } - hasEffectsWhenMutatedAtPath ( path ) { - return this.included || path.length > 0; - } - initialiseChildren () { this.params.forEach( param => param.initialiseAndDeclare( this.scope, 'parameter' ) ); if ( this.body.initialiseAndReplaceScope ) { diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index 24eac537c7c..ab9f8111779 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -35,8 +35,12 @@ export default class CallExpression extends Node { } hasEffectsWhenAccessedAtPath ( path, options ) { + if ( options.hasNodeBeenAccessedAtPath( path, this ) ) { + return false; + } + const innerOptions = options.addAccessedNodeAtPath( path, this ); return this.callee.someReturnExpressionAtPath( path, this._callOptions, ( relativePath, node ) => - node.hasEffectsWhenAccessedAtPath( relativePath, options ) ); + node.hasEffectsWhenAccessedAtPath( relativePath, innerOptions ) ); } hasEffectsWhenAssignedAtPath ( path, options ) { @@ -49,11 +53,6 @@ export default class CallExpression extends Node { node.hasEffectsWhenCalledAtPath( relativePath, callOptions, options ) ); } - hasEffectsWhenMutatedAtPath ( path, options ) { - return this.callee.someReturnExpressionAtPath( path, this._callOptions, ( relativePath, node ) => - node.hasEffectsWhenMutatedAtPath( relativePath, options ) ); - } - initialiseNode () { this._callOptions = { withNew: false }; } diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index d76f19bfc27..5d91442a53c 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -36,11 +36,6 @@ export default class Identifier extends Node { || this.variable.hasEffectsWhenCalledAtPath( path, callOptions, options ); } - hasEffectsWhenMutatedAtPath ( path, options ) { - return !this.variable - || this.variable.hasEffectsWhenMutatedAtPath( path, options ); - } - includeInBundle () { if ( this.included ) return false; this.included = true; diff --git a/src/ast/nodes/Literal.js b/src/ast/nodes/Literal.js index b8b1277763f..f91cc4ce88f 100644 --- a/src/ast/nodes/Literal.js +++ b/src/ast/nodes/Literal.js @@ -19,13 +19,6 @@ export default class Literal extends Node { return path.length > 1; } - hasEffectsWhenMutatedAtPath ( path ) { - if (this.value === null) { - return true; - } - return path.length > 0; - } - render ( code ) { if ( typeof this.value === 'string' ) { code.indentExclusionRanges.push( [ this.start + 1, this.end - 1 ] ); diff --git a/src/ast/nodes/LogicalExpression.js b/src/ast/nodes/LogicalExpression.js index 6fcb733dbc0..dcd7e2e33f8 100644 --- a/src/ast/nodes/LogicalExpression.js +++ b/src/ast/nodes/LogicalExpression.js @@ -18,9 +18,6 @@ export default class LogicalExpression extends Node { } hasEffectsWhenAccessedAtPath ( path, options ) { - if (path.length === 0) { - return false; - } const leftValue = this.left.getValue(); if ( leftValue === UNKNOWN_VALUE ) { return this.left.hasEffectsWhenAccessedAtPath( path, options ) @@ -33,19 +30,14 @@ export default class LogicalExpression extends Node { } hasEffectsWhenAssignedAtPath ( path, options ) { - return path.length === 0 - || this.hasEffectsWhenMutatedAtPath( path.slice( 1 ), options ); - } - - hasEffectsWhenMutatedAtPath ( path, options ) { const leftValue = this.left.getValue(); if ( leftValue === UNKNOWN_VALUE ) { - return this.left.hasEffectsWhenMutatedAtPath( path, options ) - || this.right.hasEffectsWhenMutatedAtPath( path, options ); + return this.left.hasEffectsWhenAssignedAtPath( path, options ) + || this.right.hasEffectsWhenAssignedAtPath( path, options ); } if ( (leftValue && this.operator === '||') || (!leftValue && this.operator === '&&') ) { - return this.left.hasEffectsWhenMutatedAtPath( path, options ); + return this.left.hasEffectsWhenAssignedAtPath( path, options ); } - return this.right.hasEffectsWhenMutatedAtPath( path, options ); + return this.right.hasEffectsWhenAssignedAtPath( path, options ); } } diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index dd834327177..5095ad553d1 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -116,16 +116,6 @@ export default class MemberExpression extends Node { || this.object.hasEffectsWhenCalledAtPath( [ this.property.name, ...path ], callOptions, options ); } - hasEffectsWhenMutatedAtPath ( path, options ) { - if ( this.variable ) { - return this.variable.hasEffectsWhenMutatedAtPath( path, options ); - } - if ( this.computed ) { - return path.length > 0 || this.object.hasEffectsWhenMutatedAtPath( [], options ); - } - return this.object.hasEffectsWhenMutatedAtPath( [ this.property.name, ...path ], options ); - } - includeInBundle () { let addedNewNodes = super.includeInBundle(); if ( this.variable && !this.variable.included ) { diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index 8fa618e4576..f959479f9cf 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -64,14 +64,4 @@ export default class ObjectExpression extends Node { return !hasCertainHit || properties.some( property => property.hasEffectsWhenCalledAtPath( path.slice( 1 ), callOptions, options ) ); } - - hasEffectsWhenMutatedAtPath ( path, options ) { - if ( path.length === 0 ) { - return false; - } - const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_READ ); - - return !hasCertainHit || properties.some( property => - property.hasEffectsWhenMutatedAtPath( path.slice( 1 ), options ) ); - } } diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index bddd4bdab6d..14f578fca85 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -37,13 +37,6 @@ export default class Property extends Node { return this.value.hasEffectsWhenCalledAtPath( path, callOptions, options ); } - hasEffectsWhenMutatedAtPath ( path, options ) { - if ( this.kind === 'get' ) { - return true; - } - return this.value.hasEffectsWhenMutatedAtPath( path, options ); - } - initialiseAndDeclare ( parentScope, kind ) { this.initialiseScope( parentScope ); this.initialiseNode( parentScope ); diff --git a/src/ast/nodes/ThisExpression.js b/src/ast/nodes/ThisExpression.js index f704fa00310..2a03d9ae32a 100644 --- a/src/ast/nodes/ThisExpression.js +++ b/src/ast/nodes/ThisExpression.js @@ -28,10 +28,6 @@ export default class ThisExpression extends Node { return this.variable.hasEffectsWhenAssignedAtPath( path, options ); } - hasEffectsWhenMutatedAtPath ( path, options ) { - return this.variable.hasEffectsWhenMutatedAtPath( path, options ); - } - render ( code ) { if ( this.alias ) { code.overwrite( this.start, this.end, this.alias, { storeName: true, contentOnly: false } ); diff --git a/src/ast/nodes/UnaryExpression.js b/src/ast/nodes/UnaryExpression.js index 924afa95fb4..48c5ba9c0e4 100644 --- a/src/ast/nodes/UnaryExpression.js +++ b/src/ast/nodes/UnaryExpression.js @@ -30,10 +30,7 @@ export default class UnaryExpression extends Node { hasEffects ( options ) { return this.included || this.argument.hasEffects( options ) - || (this.operator === 'delete' && ( - this.argument.type !== 'MemberExpression' - || this.argument.object.hasEffectsWhenMutatedAtPath( [], options ) - )); + || (this.operator === 'delete' && this.argument.hasEffectsWhenAssignedAtPath( [], options )); } hasEffectsWhenAccessedAtPath ( path ) { diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index 6caa0be976e..3da07d33b97 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -44,16 +44,6 @@ export default class FunctionNode extends Node { || this.body.hasEffects( innerOptions ); } - hasEffectsWhenMutatedAtPath ( path, options ) { - if ( path.length === 0 ) { - return false; - } - if ( path[ 0 ] === 'prototype' ) { - return this.prototypeObject.hasEffectsWhenMutatedAtPath( path.slice( 1 ), options ); - } - return true; - } - initialiseNode () { this.prototypeObject = new VirtualObjectExpression(); const lastBodyStatement = this.body.body[ this.body.body.length - 1 ]; diff --git a/src/ast/nodes/shared/UndefinedIdentifier.js b/src/ast/nodes/shared/UndefinedIdentifier.js index 4a1b616dbb9..46a561a09f6 100644 --- a/src/ast/nodes/shared/UndefinedIdentifier.js +++ b/src/ast/nodes/shared/UndefinedIdentifier.js @@ -13,10 +13,6 @@ export default class UndefinedIdentifier extends Node { return path.length > 0; } - hasEffectsWhenMutatedAtPath ( path ) { - return path.length > 0; - } - toString () { return 'undefined'; } diff --git a/src/ast/nodes/shared/VirtualNumberLiteral.js b/src/ast/nodes/shared/VirtualNumberLiteral.js index e2951151f6e..cd04544aeb6 100644 --- a/src/ast/nodes/shared/VirtualNumberLiteral.js +++ b/src/ast/nodes/shared/VirtualNumberLiteral.js @@ -9,10 +9,6 @@ export default class VirtualNumberLiteral extends Node { return path.length > 1; } - hasEffectsWhenMutatedAtPath ( path ) { - return path.length > 0; - } - toString () { return '[[VIRTUAL NUMBER]]'; } diff --git a/src/ast/nodes/shared/VirtualObjectExpression.js b/src/ast/nodes/shared/VirtualObjectExpression.js index b7c897a2746..9a0e49cc606 100644 --- a/src/ast/nodes/shared/VirtualObjectExpression.js +++ b/src/ast/nodes/shared/VirtualObjectExpression.js @@ -9,10 +9,6 @@ export default class VirtualObjectExpression extends Node { return path.length > 1; } - hasEffectsWhenMutatedAtPath ( path ) { - return path.length > 0; - } - toString () { return '[[VIRTUAL OBJECT]]'; } diff --git a/src/ast/values.js b/src/ast/values.js index 4279b88bc04..06f962b1c99 100644 --- a/src/ast/values.js +++ b/src/ast/values.js @@ -6,6 +6,5 @@ export const UNKNOWN_ASSIGNMENT = { hasEffectsWhenAccessedAtPath: path => path.length > 0, hasEffectsWhenAssignedAtPath: () => true, hasEffectsWhenCalledAtPath: () => true, - hasEffectsWhenMutatedAtPath: () => true, toString: () => '[[UNKNOWN]]' }; diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index 023c5a411b6..c00b647f252 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -45,8 +45,7 @@ export default class LocalVariable extends Variable { return true; } return this.assignedExpressions.someAtPath( path, ( relativePath, node ) => - relativePath.length > 0 - && !options.hasNodeBeenAccessedAtPath( relativePath, node ) + !options.hasNodeBeenAccessedAtPath( relativePath, node ) && node.hasEffectsWhenAccessedAtPath( relativePath, options.addAccessedNodeAtPath( relativePath, node ) ) ); } @@ -74,20 +73,6 @@ export default class LocalVariable extends Variable { } ); } - hasEffectsWhenMutatedAtPath ( path, options ) { - if ( path.length > MAX_PATH_LENGTH ) { - return true; - } - return this.included - || this.assignedExpressions.someAtPath( path, ( relativePath, node ) => { - if ( relativePath.length === 0 ) { - return !options.hasNodeBeenMutated( node ) - && node.hasEffectsWhenMutatedAtPath( [], options.addMutatedNode( node ) ); - } - return node.hasEffectsWhenMutatedAtPath( relativePath, options ); - } ); - } - includeVariable () { const hasBeenIncluded = super.includeVariable(); if ( hasBeenIncluded ) { diff --git a/src/ast/variables/ThisVariable.js b/src/ast/variables/ThisVariable.js index f42308aab42..890b1df1072 100644 --- a/src/ast/variables/ThisVariable.js +++ b/src/ast/variables/ThisVariable.js @@ -23,11 +23,6 @@ export default class ThisVariable extends LocalVariable { return super.hasEffectsWhenCalledAtPath( path, callOptions, options ); } - hasEffectsWhenMutatedAtPath ( path, options ) { - this._updateInit( options ); - return super.hasEffectsWhenMutatedAtPath( path, options ); - } - _updateInit ( options ) { this.assignedExpressions.setInit( options.getReplacedThisInit( this ) || UNKNOWN_ASSIGNMENT ); } diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index b5554e0b8de..e7fb6811c43 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -57,15 +57,6 @@ export default class Variable { return true; } - /** - * @param {String[]} path - * @param {ExecutionPathOptions} options - * @return {boolean} - */ - hasEffectsWhenMutatedAtPath ( path, options ) { - return true; - } - /** * Marks this variable as being part of the bundle, which is usually the case when one of * its identifiers becomes part of the bundle. Returns true if it has not been included From 7aa94bccaa712ab9291682eaf158fd480d6381f0 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Fri, 13 Oct 2017 20:54:51 +0200 Subject: [PATCH 41/76] Split bind into bindNode and bindChildren to avoid the need to call super.bind() and split the concerns. --- src/ast/Node.js | 17 ++++++++++++++++- src/ast/nodes/AssignmentExpression.js | 3 +-- src/ast/nodes/AssignmentPattern.js | 3 +-- src/ast/nodes/BlockStatement.js | 4 ---- src/ast/nodes/CallExpression.js | 4 +--- src/ast/nodes/ExportDefaultDeclaration.js | 3 +-- src/ast/nodes/ExportNamedDeclaration.js | 2 +- src/ast/nodes/ForOfStatement.js | 3 +-- src/ast/nodes/Identifier.js | 2 +- src/ast/nodes/ImportDeclaration.js | 5 +---- src/ast/nodes/TaggedTemplateExpression.js | 4 +--- src/ast/nodes/ThisExpression.js | 2 +- src/ast/nodes/UnaryExpression.js | 3 +-- src/ast/nodes/UpdateExpression.js | 3 +-- src/ast/nodes/shared/FunctionNode.js | 3 +-- 15 files changed, 29 insertions(+), 32 deletions(-) diff --git a/src/ast/Node.js b/src/ast/Node.js index 6623680cbda..b56f7f75c99 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -12,9 +12,24 @@ export default class Node { /** * Called once all nodes have been initialised and the scopes have been populated. - * Use this to bind assignments to variables. + * Usually one should not override this function but override bindNode and/or + * bindChildren instead. */ bind () { + this.bindChildren(); + 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() ); } diff --git a/src/ast/nodes/AssignmentExpression.js b/src/ast/nodes/AssignmentExpression.js index 6a1f495a5d5..108afcfd582 100644 --- a/src/ast/nodes/AssignmentExpression.js +++ b/src/ast/nodes/AssignmentExpression.js @@ -2,8 +2,7 @@ import Node from '../Node.js'; import disallowIllegalReassignment from './shared/disallowIllegalReassignment.js'; export default class AssignmentExpression extends Node { - bind () { - super.bind(); + bindNode () { disallowIllegalReassignment( this.scope, this.left ); this.left.bindAssignmentAtPath( [], this.right ); } diff --git a/src/ast/nodes/AssignmentPattern.js b/src/ast/nodes/AssignmentPattern.js index 73ba49730cd..df02e252140 100644 --- a/src/ast/nodes/AssignmentPattern.js +++ b/src/ast/nodes/AssignmentPattern.js @@ -1,8 +1,7 @@ import Node from '../Node.js'; export default class AssignmentPattern extends Node { - bind () { - super.bind(); + bindNode () { this.left.bindAssignmentAtPath( [], this.right ); } diff --git a/src/ast/nodes/BlockStatement.js b/src/ast/nodes/BlockStatement.js index de261c5aa00..8b8468ee0c8 100644 --- a/src/ast/nodes/BlockStatement.js +++ b/src/ast/nodes/BlockStatement.js @@ -2,10 +2,6 @@ import Statement from './shared/Statement.js'; import BlockScope from '../scopes/BlockScope'; export default class BlockStatement extends Statement { - bind () { - this.body.forEach( node => node.bind() ); - } - hasEffects ( options ) { // Empty block statements do not have effects even though they may be included as e.g. function body return this.body.some( child => child.hasEffects( options ) ); diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index ab9f8111779..cfe30f1ebbc 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -1,7 +1,7 @@ import Node from '../Node.js'; export default class CallExpression extends Node { - bind () { + bindNode () { if ( this.callee.type === 'Identifier' ) { const variable = this.scope.findVariable( this.callee.name ); @@ -20,8 +20,6 @@ export default class CallExpression extends Node { }, this.start ); } } - - super.bind(); } hasEffects ( options ) { diff --git a/src/ast/nodes/ExportDefaultDeclaration.js b/src/ast/nodes/ExportDefaultDeclaration.js index 39ff2af7bcf..6b006d1451d 100644 --- a/src/ast/nodes/ExportDefaultDeclaration.js +++ b/src/ast/nodes/ExportDefaultDeclaration.js @@ -4,11 +4,10 @@ import ExecutionPathOptions from '../ExecutionPathOptions'; const functionOrClassDeclaration = /^(?:Function|Class)Declaration/; export default class ExportDefaultDeclaration extends Node { - bind () { + bindNode () { if ( this._declarationName ) { this.variable.setOriginalVariable( this.scope.findVariable( this._declarationName ) ); } - this.declaration.bind(); } hasEffectsWhenCalledAtPath ( path, callOptions, options ) { diff --git a/src/ast/nodes/ExportNamedDeclaration.js b/src/ast/nodes/ExportNamedDeclaration.js index 92d15cdc135..6ce0d121038 100644 --- a/src/ast/nodes/ExportNamedDeclaration.js +++ b/src/ast/nodes/ExportNamedDeclaration.js @@ -1,7 +1,7 @@ import Node from '../Node.js'; export default class ExportNamedDeclaration extends Node { - bind () { + bindChildren () { // Do not bind specifiers if ( this.declaration ) this.declaration.bind(); } diff --git a/src/ast/nodes/ForOfStatement.js b/src/ast/nodes/ForOfStatement.js index 8f68cc6d396..ba08ddb7092 100644 --- a/src/ast/nodes/ForOfStatement.js +++ b/src/ast/nodes/ForOfStatement.js @@ -3,8 +3,7 @@ import BlockScope from '../scopes/BlockScope'; import { UNKNOWN_ASSIGNMENT } from '../values'; export default class ForOfStatement extends Statement { - bind () { - super.bind(); + bindNode () { this.left.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT ); } diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index 5d91442a53c..48ef09ec25d 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -3,7 +3,7 @@ import isReference from 'is-reference'; import { UNKNOWN_ASSIGNMENT } from '../values'; export default class Identifier extends Node { - bind () { + bindNode () { this._bindVariableIfMissing(); } diff --git a/src/ast/nodes/ImportDeclaration.js b/src/ast/nodes/ImportDeclaration.js index 261aeed2d12..7ed083b5e7d 100644 --- a/src/ast/nodes/ImportDeclaration.js +++ b/src/ast/nodes/ImportDeclaration.js @@ -1,10 +1,7 @@ import Node from '../Node.js'; export default class ImportDeclaration extends Node { - bind () { - // noop - // TODO do the inter-module binding setup here? - } + bindChildren () {} initialiseNode () { this.isImportDeclaration = true; diff --git a/src/ast/nodes/TaggedTemplateExpression.js b/src/ast/nodes/TaggedTemplateExpression.js index 83f3ecba8de..d8aaea58f34 100644 --- a/src/ast/nodes/TaggedTemplateExpression.js +++ b/src/ast/nodes/TaggedTemplateExpression.js @@ -1,7 +1,7 @@ import Node from '../Node.js'; export default class TaggedTemplateExpression extends Node { - bind () { + bindNode () { if ( this.tag.type === 'Identifier' ) { const variable = this.scope.findVariable( this.tag.name ); @@ -20,8 +20,6 @@ export default class TaggedTemplateExpression extends Node { }, this.start ); } } - - super.bind(); } hasEffects ( options ) { diff --git a/src/ast/nodes/ThisExpression.js b/src/ast/nodes/ThisExpression.js index 2a03d9ae32a..3b4a4427ad4 100644 --- a/src/ast/nodes/ThisExpression.js +++ b/src/ast/nodes/ThisExpression.js @@ -16,7 +16,7 @@ export default class ThisExpression extends Node { } } - bind () { + bindNode () { this.variable = this.scope.findVariable( 'this' ); } diff --git a/src/ast/nodes/UnaryExpression.js b/src/ast/nodes/UnaryExpression.js index 48c5ba9c0e4..6090416e282 100644 --- a/src/ast/nodes/UnaryExpression.js +++ b/src/ast/nodes/UnaryExpression.js @@ -13,8 +13,7 @@ const operators = { }; export default class UnaryExpression extends Node { - bind () { - if ( this.value === UNKNOWN_VALUE ) super.bind(); + bindNode () { if ( this.operator === 'delete' ) { this.argument.bindAssignmentAtPath( [], new UndefinedIdentifier() ); } diff --git a/src/ast/nodes/UpdateExpression.js b/src/ast/nodes/UpdateExpression.js index 248c10dde0e..e69b248cfdf 100644 --- a/src/ast/nodes/UpdateExpression.js +++ b/src/ast/nodes/UpdateExpression.js @@ -3,8 +3,7 @@ import disallowIllegalReassignment from './shared/disallowIllegalReassignment.js import VirtualNumberLiteral from './shared/VirtualNumberLiteral'; export default class UpdateExpression extends Node { - bind () { - super.bind(); + bindNode () { disallowIllegalReassignment( this.scope, this.argument ); this.argument.bindAssignmentAtPath( [], new VirtualNumberLiteral() ); if ( this.argument.type === 'Identifier' ) { diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index 3da07d33b97..be00c6dd27b 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -5,8 +5,7 @@ import UndefinedIdentifier from './UndefinedIdentifier'; import { UNKNOWN_ASSIGNMENT } from '../../values'; export default class FunctionNode extends Node { - bind () { - super.bind(); + bindNode () { this.thisVariable = this.scope.findVariable( 'this' ); } From bd911aa34cb72ca89e2901168f6edb5f9d7d2f18 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Fri, 13 Oct 2017 21:11:45 +0200 Subject: [PATCH 42/76] Move adding of implicit return values to the block statement and tree-shake arrow functions with block statement bodies. --- src/ast/nodes/ArrowFunctionExpression.js | 17 ++++++++++------- src/ast/nodes/BlockStatement.js | 8 ++++++++ src/ast/nodes/shared/FunctionNode.js | 6 +----- .../arrow-function-return-values/_config.js | 2 +- .../_expected/amd.js | 9 +++++++-- .../_expected/cjs.js | 9 +++++++-- .../_expected/es.js | 9 +++++++-- .../_expected/iife.js | 9 +++++++-- .../_expected/umd.js | 9 +++++++-- .../arrow-function-return-values/main.js | 17 +++++++++++++---- 10 files changed, 68 insertions(+), 27 deletions(-) diff --git a/src/ast/nodes/ArrowFunctionExpression.js b/src/ast/nodes/ArrowFunctionExpression.js index b9555be498f..01fd4b00214 100644 --- a/src/ast/nodes/ArrowFunctionExpression.js +++ b/src/ast/nodes/ArrowFunctionExpression.js @@ -1,8 +1,14 @@ import Node from '../Node'; -import Scope from '../scopes/Scope.js'; -import { UNKNOWN_ASSIGNMENT } from '../values'; +import Scope from '../scopes/Scope'; +import ReturnValueScope from '../scopes/ReturnValueScope'; export default class ArrowFunctionExpression extends Node { + bindNode () { + this.body.bindImplicitReturnExpressionToScope + ? this.body.bindImplicitReturnExpressionToScope() + : this.scope.addReturnExpression( this.body ); + } + hasEffects () { return this.included; } @@ -33,13 +39,10 @@ export default class ArrowFunctionExpression extends Node { } initialiseScope ( parentScope ) { - this.scope = new Scope( { parent: parentScope } ); + this.scope = new ReturnValueScope( { parent: parentScope } ); } someReturnExpressionAtPath ( path, callOptions, predicateFunction ) { - if ( this.body.type !== 'BlockStatement' ) { - return predicateFunction( path, this.body ); - } - return predicateFunction( path, UNKNOWN_ASSIGNMENT ); + return this.scope.someReturnExpressionAtPath( path, callOptions, predicateFunction ); } } diff --git a/src/ast/nodes/BlockStatement.js b/src/ast/nodes/BlockStatement.js index 8b8468ee0c8..7913d66ac06 100644 --- a/src/ast/nodes/BlockStatement.js +++ b/src/ast/nodes/BlockStatement.js @@ -1,7 +1,15 @@ import Statement from './shared/Statement.js'; import BlockScope from '../scopes/BlockScope'; +import UndefinedIdentifier from './shared/UndefinedIdentifier'; export default class BlockStatement extends Statement { + bindImplicitReturnExpressionToScope () { + const lastStatement = this.body[ this.body.length - 1 ]; + if ( !lastStatement || lastStatement.type !== 'ReturnStatement' ) { + this.scope.addReturnExpression( new UndefinedIdentifier() ); + } + } + hasEffects ( options ) { // Empty block statements do not have effects even though they may be included as e.g. function body return this.body.some( child => child.hasEffects( options ) ); diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index be00c6dd27b..53aadb286a3 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -1,12 +1,12 @@ import Node from '../../Node.js'; import FunctionScope from '../../scopes/FunctionScope'; import VirtualObjectExpression from './VirtualObjectExpression'; -import UndefinedIdentifier from './UndefinedIdentifier'; import { UNKNOWN_ASSIGNMENT } from '../../values'; export default class FunctionNode extends Node { bindNode () { this.thisVariable = this.scope.findVariable( 'this' ); + this.body.bindImplicitReturnExpressionToScope(); } hasEffects ( options ) { @@ -45,10 +45,6 @@ export default class FunctionNode extends Node { initialiseNode () { this.prototypeObject = new VirtualObjectExpression(); - const lastBodyStatement = this.body.body[ this.body.body.length - 1 ]; - if ( !lastBodyStatement || lastBodyStatement.type !== 'ReturnStatement' ) { - this.scope.addReturnExpression( new UndefinedIdentifier() ); - } } initialiseScope ( parentScope ) { diff --git a/test/form/samples/arrow-function-return-values/_config.js b/test/form/samples/arrow-function-return-values/_config.js index 164c6b26455..ec78acb1bad 100644 --- a/test/form/samples/arrow-function-return-values/_config.js +++ b/test/form/samples/arrow-function-return-values/_config.js @@ -1,3 +1,3 @@ module.exports = { - description: 'forwards return values of body-less arrow functions' + description: 'forwards return values of arrow functions' }; diff --git a/test/form/samples/arrow-function-return-values/_expected/amd.js b/test/form/samples/arrow-function-return-values/_expected/amd.js index 1745d81b7ab..e00550668fd 100644 --- a/test/form/samples/arrow-function-return-values/_expected/amd.js +++ b/test/form/samples/arrow-function-return-values/_expected/amd.js @@ -4,7 +4,12 @@ define(function () { 'use strict'; (() => () => () => console.log( 'effect' ))()()(); - const bar = () => () => console.log('effect'); - bar()(); + const retained1 = () => () => console.log( 'effect' ); + retained1()(); + + const retained2 = () => { + return () => console.log( 'effect' ); + }; + retained2()(); }); diff --git a/test/form/samples/arrow-function-return-values/_expected/cjs.js b/test/form/samples/arrow-function-return-values/_expected/cjs.js index 61f8287a0f2..eed7b3effc8 100644 --- a/test/form/samples/arrow-function-return-values/_expected/cjs.js +++ b/test/form/samples/arrow-function-return-values/_expected/cjs.js @@ -4,5 +4,10 @@ (() => () => () => console.log( 'effect' ))()()(); -const bar = () => () => console.log('effect'); -bar()(); +const retained1 = () => () => console.log( 'effect' ); +retained1()(); + +const retained2 = () => { + return () => console.log( 'effect' ); +}; +retained2()(); diff --git a/test/form/samples/arrow-function-return-values/_expected/es.js b/test/form/samples/arrow-function-return-values/_expected/es.js index b56ce30916a..d73672fac85 100644 --- a/test/form/samples/arrow-function-return-values/_expected/es.js +++ b/test/form/samples/arrow-function-return-values/_expected/es.js @@ -2,5 +2,10 @@ (() => () => () => console.log( 'effect' ))()()(); -const bar = () => () => console.log('effect'); -bar()(); +const retained1 = () => () => console.log( 'effect' ); +retained1()(); + +const retained2 = () => { + return () => console.log( 'effect' ); +}; +retained2()(); diff --git a/test/form/samples/arrow-function-return-values/_expected/iife.js b/test/form/samples/arrow-function-return-values/_expected/iife.js index 1bbd4b26cc5..77272363bf4 100644 --- a/test/form/samples/arrow-function-return-values/_expected/iife.js +++ b/test/form/samples/arrow-function-return-values/_expected/iife.js @@ -5,7 +5,12 @@ (() => () => () => console.log( 'effect' ))()()(); - const bar = () => () => console.log('effect'); - bar()(); + const retained1 = () => () => console.log( 'effect' ); + retained1()(); + + const retained2 = () => { + return () => console.log( 'effect' ); + }; + retained2()(); }()); diff --git a/test/form/samples/arrow-function-return-values/_expected/umd.js b/test/form/samples/arrow-function-return-values/_expected/umd.js index 7a697563c11..f9f18b9b41e 100644 --- a/test/form/samples/arrow-function-return-values/_expected/umd.js +++ b/test/form/samples/arrow-function-return-values/_expected/umd.js @@ -8,7 +8,12 @@ (() => () => () => console.log( 'effect' ))()()(); - const bar = () => () => console.log('effect'); - bar()(); + const retained1 = () => () => console.log( 'effect' ); + retained1()(); + + const retained2 = () => { + return () => console.log( 'effect' ); + }; + retained2()(); }))); diff --git a/test/form/samples/arrow-function-return-values/main.js b/test/form/samples/arrow-function-return-values/main.js index 8135004c12a..aafad865ec4 100644 --- a/test/form/samples/arrow-function-return-values/main.js +++ b/test/form/samples/arrow-function-return-values/main.js @@ -4,7 +4,16 @@ (() => () => () => {})()()(); (() => () => () => console.log( 'effect' ))()()(); -const foo = () => () => {}; -foo()(); -const bar = () => () => console.log('effect'); -bar()(); +const removed1 = () => () => {}; +removed1()(); +const retained1 = () => () => console.log( 'effect' ); +retained1()(); + +const removed2 = () => { + return () => {}; +}; +removed2()(); +const retained2 = () => { + return () => console.log( 'effect' ); +}; +retained2()(); From 40be898c06d9c39324b1a97aa6d9247ccdb3599e Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Fri, 13 Oct 2017 23:17:44 +0200 Subject: [PATCH 43/76] * Remove "path" parameter from callback of someReturnExpressionAtPath as it always needs to be [] * Rename someReturnExpressionAtPath -> someReturnExpressionWhenCalledAtPath * Remove the path from the same function on the ReturnValueScope --- src/ast/Node.js | 7 +++---- src/ast/nodes/ArrowFunctionExpression.js | 4 ++-- src/ast/nodes/CallExpression.js | 18 +++++++++--------- src/ast/nodes/Identifier.js | 6 +++--- src/ast/nodes/MemberExpression.js | 8 ++++++++ src/ast/nodes/ObjectExpression.js | 10 ++++++++++ src/ast/nodes/Property.js | 7 +++++++ src/ast/nodes/shared/FunctionNode.js | 4 ++-- src/ast/scopes/ReturnValueScope.js | 5 ++--- src/ast/values.js | 1 + src/ast/variables/LocalVariable.js | 4 ++-- src/ast/variables/Variable.js | 4 ++-- .../_expected/amd.js | 9 ++++++--- .../_expected/cjs.js | 9 ++++++--- .../_expected/es.js | 9 ++++++--- .../_expected/iife.js | 9 ++++++--- .../_expected/umd.js | 9 ++++++--- .../arrow-function-return-values/main.js | 17 +++++++++++------ 18 files changed, 92 insertions(+), 48 deletions(-) diff --git a/src/ast/Node.js b/src/ast/Node.js index b56f7f75c99..0f7c304b876 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -228,15 +228,14 @@ export default class Node { /** * Returns true if some possible return expression when called at the given - * path returns true. predicateFunction receives a `relativePath` and an `expression` - * which is called at this relative path as parameters. + * path returns true. predicateFunction receives a `node` as parameter. * @param {String[]} path * @param {Object} callOptions * @param {Function} predicateFunction * @returns {boolean} */ - someReturnExpressionAtPath ( path, callOptions, predicateFunction ) { - return predicateFunction( path, UNKNOWN_ASSIGNMENT ); + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { + return predicateFunction( UNKNOWN_ASSIGNMENT ); } toString () { diff --git a/src/ast/nodes/ArrowFunctionExpression.js b/src/ast/nodes/ArrowFunctionExpression.js index 01fd4b00214..5b29828b31c 100644 --- a/src/ast/nodes/ArrowFunctionExpression.js +++ b/src/ast/nodes/ArrowFunctionExpression.js @@ -42,7 +42,7 @@ export default class ArrowFunctionExpression extends Node { this.scope = new ReturnValueScope( { parent: parentScope } ); } - someReturnExpressionAtPath ( path, callOptions, predicateFunction ) { - return this.scope.someReturnExpressionAtPath( path, callOptions, predicateFunction ); + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { + return this.scope.someReturnExpressionWhenCalled( callOptions, predicateFunction ); } } diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index cfe30f1ebbc..5d257d260b0 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -37,26 +37,26 @@ export default class CallExpression extends Node { return false; } const innerOptions = options.addAccessedNodeAtPath( path, this ); - return this.callee.someReturnExpressionAtPath( path, this._callOptions, ( relativePath, node ) => - node.hasEffectsWhenAccessedAtPath( relativePath, innerOptions ) ); + return this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, node => + node.hasEffectsWhenAccessedAtPath( path, innerOptions ) ); } hasEffectsWhenAssignedAtPath ( path, options ) { - return this.callee.someReturnExpressionAtPath( path, this._callOptions, ( relativePath, node ) => - node.hasEffectsWhenAssignedAtPath( relativePath, options ) ); + return this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, node => + node.hasEffectsWhenAssignedAtPath( path, options ) ); } hasEffectsWhenCalledAtPath ( path, callOptions, options ) { - return this.callee.someReturnExpressionAtPath( path, this._callOptions, ( relativePath, node ) => - node.hasEffectsWhenCalledAtPath( relativePath, callOptions, options ) ); + return this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, node => + node.hasEffectsWhenCalledAtPath( path, callOptions, options ) ); } initialiseNode () { this._callOptions = { withNew: false }; } - someReturnExpressionAtPath ( path, callOptions, predicateFunction ) { - return this.callee.someReturnExpressionAtPath( path, this._callOptions, ( relativePath, node ) => - node.someReturnExpressionAtPath( relativePath, callOptions, predicateFunction ) ); + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { + return this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, node => + node.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction ) ); } } diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index 48ef09ec25d..b6b45bc305c 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -77,10 +77,10 @@ export default class Identifier extends Node { } } - someReturnExpressionAtPath ( path, callOptions, predicateFunction ) { + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { if ( this.variable ) { - return this.variable.someReturnExpressionAtPath( path, callOptions, predicateFunction ); + return this.variable.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction ); } - return predicateFunction( path, UNKNOWN_ASSIGNMENT ); + return predicateFunction( UNKNOWN_ASSIGNMENT ); } } diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index 5095ad553d1..15b108e33a8 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -137,4 +137,12 @@ export default class MemberExpression extends Node { super.render( code, es ); } + + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { + if ( this.variable ) { + return this.variable.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction ); + } + return this.computed + || this.object.someReturnExpressionWhenCalledAtPath( [ this.property.name, ...path ], callOptions, predicateFunction ); + } } diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index f959479f9cf..99fc8f25354 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -64,4 +64,14 @@ export default class ObjectExpression extends Node { return !hasCertainHit || properties.some( property => property.hasEffectsWhenCalledAtPath( path.slice( 1 ), callOptions, options ) ); } + + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { + 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 ) ); + } } diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index 14f578fca85..cccd79633ca 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -54,4 +54,11 @@ export default class Property extends Node { } this.value.render( code, es ); } + + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { + if ( this.kind === 'get' ) { + return true; + } + return this.value.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction ); + } } diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index 53aadb286a3..f86b0e39f24 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -51,7 +51,7 @@ export default class FunctionNode extends Node { this.scope = new FunctionScope( { parent: parentScope } ); } - someReturnExpressionAtPath ( path, callOptions, predicateFunction ) { - return this.scope.someReturnExpressionAtPath( path, callOptions, predicateFunction ); + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { + return this.scope.someReturnExpressionWhenCalled( callOptions, predicateFunction ); } } diff --git a/src/ast/scopes/ReturnValueScope.js b/src/ast/scopes/ReturnValueScope.js index ee074166958..1821bf4b1a6 100644 --- a/src/ast/scopes/ReturnValueScope.js +++ b/src/ast/scopes/ReturnValueScope.js @@ -10,8 +10,7 @@ export default class ReturnValueScope extends Scope { this._returnExpressions.add( expression ); } - someReturnExpressionAtPath ( path, callOptions, predicateFunction ) { - return Array.from( this._returnExpressions ).some( returnExpression => - predicateFunction( path, returnExpression ) ); + someReturnExpressionWhenCalled ( callOptions, predicateFunction ) { + return Array.from( this._returnExpressions ).some( predicateFunction ); } } diff --git a/src/ast/values.js b/src/ast/values.js index 06f962b1c99..131b1a4dbb4 100644 --- a/src/ast/values.js +++ b/src/ast/values.js @@ -6,5 +6,6 @@ export const UNKNOWN_ASSIGNMENT = { hasEffectsWhenAccessedAtPath: path => path.length > 0, hasEffectsWhenAssignedAtPath: () => true, hasEffectsWhenCalledAtPath: () => true, + someReturnExpressionWhenCalledAtPath: () => true, toString: () => '[[UNKNOWN]]' }; diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index c00b647f252..eaf363387ea 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -81,12 +81,12 @@ export default class LocalVariable extends Variable { return hasBeenIncluded; } - someReturnExpressionAtPath ( path, callOptions, predicateFunction ) { + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { if ( path.length > MAX_PATH_LENGTH ) { return true; } return this.assignedExpressions.someAtPath( path, ( relativePath, node ) => - node.someReturnExpressionAtPath( relativePath, callOptions, predicateFunction ) ); + node.someReturnExpressionWhenCalledAtPath( relativePath, callOptions, predicateFunction ) ); } toString () { diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index e7fb6811c43..6dfcaa0ef4c 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -78,7 +78,7 @@ export default class Variable { * @param {Function} predicateFunction * @returns {boolean} */ - someReturnExpressionAtPath ( path, callOptions, predicateFunction ) { - return predicateFunction( path, UNKNOWN_ASSIGNMENT ); + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { + return predicateFunction( UNKNOWN_ASSIGNMENT ); } } diff --git a/test/form/samples/arrow-function-return-values/_expected/amd.js b/test/form/samples/arrow-function-return-values/_expected/amd.js index e00550668fd..ef42cbfa136 100644 --- a/test/form/samples/arrow-function-return-values/_expected/amd.js +++ b/test/form/samples/arrow-function-return-values/_expected/amd.js @@ -7,9 +7,12 @@ define(function () { 'use strict'; const retained1 = () => () => console.log( 'effect' ); retained1()(); - const retained2 = () => { + (() => { return () => console.log( 'effect' ); - }; - retained2()(); + })()(); + + (() => ({ foo: () => console.log( 'effect' ) }))().foo(); + + (() => ({ foo: () => ({ bar: () => console.log( 'effect' ) }) }))().foo().bar(); }); diff --git a/test/form/samples/arrow-function-return-values/_expected/cjs.js b/test/form/samples/arrow-function-return-values/_expected/cjs.js index eed7b3effc8..15e8f61a415 100644 --- a/test/form/samples/arrow-function-return-values/_expected/cjs.js +++ b/test/form/samples/arrow-function-return-values/_expected/cjs.js @@ -7,7 +7,10 @@ const retained1 = () => () => console.log( 'effect' ); retained1()(); -const retained2 = () => { +(() => { return () => console.log( 'effect' ); -}; -retained2()(); +})()(); + +(() => ({ foo: () => console.log( 'effect' ) }))().foo(); + +(() => ({ foo: () => ({ bar: () => console.log( 'effect' ) }) }))().foo().bar(); diff --git a/test/form/samples/arrow-function-return-values/_expected/es.js b/test/form/samples/arrow-function-return-values/_expected/es.js index d73672fac85..e53f8a40dab 100644 --- a/test/form/samples/arrow-function-return-values/_expected/es.js +++ b/test/form/samples/arrow-function-return-values/_expected/es.js @@ -5,7 +5,10 @@ const retained1 = () => () => console.log( 'effect' ); retained1()(); -const retained2 = () => { +(() => { return () => console.log( 'effect' ); -}; -retained2()(); +})()(); + +(() => ({ foo: () => console.log( 'effect' ) }))().foo(); + +(() => ({ foo: () => ({ bar: () => console.log( 'effect' ) }) }))().foo().bar(); diff --git a/test/form/samples/arrow-function-return-values/_expected/iife.js b/test/form/samples/arrow-function-return-values/_expected/iife.js index 77272363bf4..be640aade72 100644 --- a/test/form/samples/arrow-function-return-values/_expected/iife.js +++ b/test/form/samples/arrow-function-return-values/_expected/iife.js @@ -8,9 +8,12 @@ const retained1 = () => () => console.log( 'effect' ); retained1()(); - const retained2 = () => { + (() => { return () => console.log( 'effect' ); - }; - retained2()(); + })()(); + + (() => ({ foo: () => console.log( 'effect' ) }))().foo(); + + (() => ({ foo: () => ({ bar: () => console.log( 'effect' ) }) }))().foo().bar(); }()); diff --git a/test/form/samples/arrow-function-return-values/_expected/umd.js b/test/form/samples/arrow-function-return-values/_expected/umd.js index f9f18b9b41e..f137d9f8d8d 100644 --- a/test/form/samples/arrow-function-return-values/_expected/umd.js +++ b/test/form/samples/arrow-function-return-values/_expected/umd.js @@ -11,9 +11,12 @@ const retained1 = () => () => console.log( 'effect' ); retained1()(); - const retained2 = () => { + (() => { return () => console.log( 'effect' ); - }; - retained2()(); + })()(); + + (() => ({ foo: () => console.log( 'effect' ) }))().foo(); + + (() => ({ foo: () => ({ bar: () => console.log( 'effect' ) }) }))().foo().bar(); }))); diff --git a/test/form/samples/arrow-function-return-values/main.js b/test/form/samples/arrow-function-return-values/main.js index aafad865ec4..4f1d7a80712 100644 --- a/test/form/samples/arrow-function-return-values/main.js +++ b/test/form/samples/arrow-function-return-values/main.js @@ -9,11 +9,16 @@ removed1()(); const retained1 = () => () => console.log( 'effect' ); retained1()(); -const removed2 = () => { +(() => { return () => {}; -}; -removed2()(); -const retained2 = () => { +})()(); + +(() => { return () => console.log( 'effect' ); -}; -retained2()(); +})()(); + +(() => ({ foo: () => {} }))().foo(); +(() => ({ foo: () => console.log( 'effect' ) }))().foo(); + +(() => ({ foo: () => ({ bar: () => ({ baz: () => {} }) }) }))().foo().bar().baz(); +(() => ({ foo: () => ({ bar: () => console.log( 'effect' ) }) }))().foo().bar(); From 3c746e621d9599bb4aa6f26e8b87f090b3fef65f Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Sat, 14 Oct 2017 18:09:44 +0200 Subject: [PATCH 44/76] * Fix and test various kinds of infinite recursions including an older one when calling object paths * Recursion prevention for call expressions that has been made necessary due to the return value handling must not use the same flags as LocalVariable as otherwise effects can be ignored when checked via LocalVariable --- src/ast/CallOptions.js | 26 +++ src/ast/ExecutionPathOptions.js | 180 +++++++++++------- src/ast/Node.js | 4 +- src/ast/nodes/CallExpression.js | 28 ++- src/ast/nodes/NewExpression.js | 6 +- src/ast/nodes/Property.js | 9 +- src/ast/nodes/TaggedTemplateExpression.js | 6 +- src/ast/nodes/shared/FunctionNode.js | 4 +- src/ast/variables/LocalVariable.js | 52 ++--- src/ast/variables/Variable.js | 4 +- test/form/samples/cyclic-assignments/main.js | 15 +- .../_expected/amd.js | 2 +- .../_expected/cjs.js | 2 +- .../_expected/es.js | 2 +- .../_expected/iife.js | 2 +- .../_expected/umd.js | 2 +- .../function-body-return-values/main.js | 2 +- .../recursive-assignments/_expected/amd.js | 5 +- .../recursive-assignments/_expected/cjs.js | 5 +- .../recursive-assignments/_expected/es.js | 5 +- .../recursive-assignments/_expected/iife.js | 5 +- .../recursive-assignments/_expected/umd.js | 5 +- .../samples/recursive-assignments/main.js | 5 +- .../samples/recursive-calls/_expected/amd.js | 10 + .../samples/recursive-calls/_expected/cjs.js | 11 ++ .../samples/recursive-calls/_expected/es.js | 10 + .../samples/recursive-calls/_expected/iife.js | 10 + .../samples/recursive-calls/_expected/umd.js | 10 + test/form/samples/recursive-calls/main.js | 22 ++- 29 files changed, 284 insertions(+), 165 deletions(-) create mode 100644 src/ast/CallOptions.js diff --git a/src/ast/CallOptions.js b/src/ast/CallOptions.js new file mode 100644 index 00000000000..a3119102daf --- /dev/null +++ b/src/ast/CallOptions.js @@ -0,0 +1,26 @@ +import Immutable from 'immutable'; + +const RESULT_KEY = {}; + +export default class CallOptions { + static create ( callOptions ) { + return new this( callOptions, Immutable.Map() ); + } + + constructor ( { withNew = false } = {}, nodesCalledAtPath ) { + this.withNew = withNew; + this._nodesCalledAtPath = nodesCalledAtPath; + } + + addCalledNodeAtPath ( path, node ) { + return new this.constructor( this, this._nodesCalledAtPath.setIn( [ node, ...path, RESULT_KEY ], true ) ); + } + + equals ( callOptions ) { + return this.withNew === callOptions.withNew; + } + + hasNodeBeenCalledAtPath ( path, node ) { + return this._nodesCalledAtPath.getIn( [ node, ...path, RESULT_KEY ] ); + } +} diff --git a/src/ast/ExecutionPathOptions.js b/src/ast/ExecutionPathOptions.js index 9873300bcf4..ababc966da2 100644 --- a/src/ast/ExecutionPathOptions.js +++ b/src/ast/ExecutionPathOptions.js @@ -4,15 +4,15 @@ const OPTION_IGNORE_BREAK_STATEMENTS = 'IGNORE_BREAK_STATEMENTS'; const OPTION_IGNORE_RETURN_AWAIT_YIELD = 'IGNORE_RETURN_AWAIT_YIELD'; const OPTION_ACCESSED_NODES = 'ACCESSED_NODES'; const OPTION_ASSIGNED_NODES = 'ASSIGNED_NODES'; -const OPTION_NODES_CALLED_WITH_OPTIONS = 'OPTION_NODES_CALLED_WITH_OPTIONS'; -const OPTION_MUTATED_NODES = 'MUTATED_NODES'; +const OPTION_NODES_CALLED_AT_PATH_WITH_OPTIONS = 'NODES_CALLED_AT_PATH_WITH_OPTIONS'; +const OPTION_RETURN_EXPRESSIONS_ACCESSED_AT_PATH = 'RETURN_EXPRESSIONS_ACCESSED_AT_PATH'; +const OPTION_RETURN_EXPRESSIONS_ASSIGNED_AT_PATH = 'RETURN_EXPRESSIONS_ASSIGNED_AT_PATH'; +const OPTION_RETURN_EXPRESSIONS_CALLED_AT_PATH = 'RETURN_EXPRESSIONS_CALLED_AT_PATH'; const OPTION_VALID_THIS_VARIABLES = 'VALID_THIS_VARIABLES'; const IGNORED_LABELS = 'IGNORED_LABELS'; const RESULT_KEY = {}; -const areCallOptionsEqual = ( options, otherOptions ) => options.withNew === otherOptions.withNew; - /** Wrapper to ensure immutability */ export default class ExecutionPathOptions { /** @@ -26,6 +26,14 @@ export default class ExecutionPathOptions { this._optionValues = optionValues; } + /** + * @param {string} option - The name of an option + * @returns {*} Its value + */ + get ( option ) { + return this._optionValues.get( option ); + } + /** * Returns a new ExecutionPathOptions instance with the given option set to a new value. * Does not mutate the current instance. Also works in sub-classes. @@ -42,73 +50,85 @@ export default class ExecutionPathOptions { } /** - * @param {string} option - The name of an option - * @returns {*} Its value + * @param {String[]} path + * @param {Node} node + * @return {ExecutionPathOptions} */ - get ( option ) { - return this._optionValues.get( option ); + addAccessedNodeAtPath ( path, node ) { + return this.setIn( [ OPTION_ACCESSED_NODES, node, ...path, RESULT_KEY ], true ); } /** - * @return {boolean} + * @param {String[]} path + * @param {CallExpression} callExpression + * @return {ExecutionPathOptions} */ - ignoreBreakStatements () { - return this.get( OPTION_IGNORE_BREAK_STATEMENTS ); + addAccessedReturnExpressionAtPath ( path, callExpression ) { + return this.setIn( [ OPTION_RETURN_EXPRESSIONS_ACCESSED_AT_PATH, callExpression, ...path, RESULT_KEY ], true ); } /** - * @param {boolean} [value=true] + * @param {String[]} path + * @param {Node} node * @return {ExecutionPathOptions} */ - setIgnoreBreakStatements ( value = true ) { - return this.set( OPTION_IGNORE_BREAK_STATEMENTS, value ); + addAssignedNodeAtPath ( path, node ) { + return this.setIn( [ OPTION_ASSIGNED_NODES, node, ...path, RESULT_KEY ], true ); } /** - * @param {string} labelName - * @return {boolean} + * @param {String[]} path + * @param {CallExpression} callExpression + * @return {ExecutionPathOptions} */ - ignoreLabel ( labelName ) { - return this._optionValues.getIn( [ IGNORED_LABELS, labelName ] ); + addAssignedReturnExpressionAtPath ( path, callExpression ) { + return this.setIn( [ OPTION_RETURN_EXPRESSIONS_ASSIGNED_AT_PATH, callExpression, ...path, RESULT_KEY ], true ); } /** - * @param {string} labelName + * @param {String[]} path + * @param {Node} node + * @param {CallOptions} callOptions * @return {ExecutionPathOptions} */ - setIgnoreLabel ( labelName ) { - return this.setIn( [ IGNORED_LABELS, labelName ], true ); + addCalledNodeAtPathWithOptions ( path, node, callOptions ) { + return this.setIn( [ OPTION_NODES_CALLED_AT_PATH_WITH_OPTIONS, node, ...path, RESULT_KEY, callOptions ], true ); } /** + * @param {String[]} path + * @param {CallExpression} callExpression * @return {ExecutionPathOptions} */ - setIgnoreNoLabels () { - return this.set( IGNORED_LABELS, null ); + addCalledReturnExpressionAtPath ( path, callExpression ) { + return this.setIn( [ OPTION_RETURN_EXPRESSIONS_CALLED_AT_PATH, callExpression, ...path, RESULT_KEY ], true ); } /** - * @return {boolean} + * @return {ExecutionPathOptions} */ - ignoreReturnAwaitYield () { - return this.get( OPTION_IGNORE_RETURN_AWAIT_YIELD ); + getHasEffectsWhenCalledOptions () { + return this + .setIgnoreReturnAwaitYield() + .setIgnoreBreakStatements( false ) + .setIgnoreNoLabels(); } /** - * @param {boolean} [value=true] - * @return {ExecutionPathOptions} + * @param {ThisVariable} thisVariable + * @returns {Node} */ - setIgnoreReturnAwaitYield ( value = true ) { - return this.set( OPTION_IGNORE_RETURN_AWAIT_YIELD, value ); + getReplacedThisInit ( thisVariable ) { + return this._optionValues.getIn( [ OPTION_VALID_THIS_VARIABLES, thisVariable ] ); } /** * @param {String[]} path * @param {Node} node - * @return {ExecutionPathOptions} + * @return {boolean} */ - addAccessedNodeAtPath ( path, node ) { - return this.setIn( [ OPTION_ACCESSED_NODES, node, ...path, RESULT_KEY ], true ); + hasNodeBeenAccessedAtPath ( path, node ) { + return this._optionValues.getIn( [ OPTION_ACCESSED_NODES, node, ...path, RESULT_KEY ] ); } /** @@ -116,75 +136,68 @@ export default class ExecutionPathOptions { * @param {Node} node * @return {boolean} */ - hasNodeBeenAccessedAtPath ( path, node ) { - return this._optionValues.getIn( [ OPTION_ACCESSED_NODES, node, ...path, RESULT_KEY ] ); + hasNodeBeenAssignedAtPath ( path, node ) { + return this._optionValues.getIn( [ OPTION_ASSIGNED_NODES, node, ...path, RESULT_KEY ] ); } /** * @param {String[]} path * @param {Node} node - * @return {ExecutionPathOptions} + * @param {CallOptions} callOptions + * @return {boolean} */ - addAssignedNodeAtPath ( path, node ) { - return this.setIn( [ OPTION_ASSIGNED_NODES, node, ...path, RESULT_KEY ], true ); + hasNodeBeenCalledAtPathWithOptions ( path, node, callOptions ) { + const previousCallOptions = this._optionValues.getIn( [ OPTION_NODES_CALLED_AT_PATH_WITH_OPTIONS, node, ...path, RESULT_KEY ] ); + return previousCallOptions && previousCallOptions.find( ( _, otherCallOptions ) => otherCallOptions.equals( callOptions ) ); } /** * @param {String[]} path - * @param {Node} node + * @param {CallExpression} callExpression * @return {boolean} */ - hasNodeBeenAssignedAtPath ( path, node ) { - return this._optionValues.getIn( [ OPTION_ASSIGNED_NODES, node, ...path, RESULT_KEY ] ); + hasReturnExpressionBeenAccessedAtPath ( path, callExpression ) { + return this._optionValues.getIn( [ OPTION_RETURN_EXPRESSIONS_ACCESSED_AT_PATH, callExpression, ...path, RESULT_KEY ] ); } /** - * @param {Node} node - * @return {ExecutionPathOptions} + * @param {String[]} path + * @param {CallExpression} callExpression + * @return {boolean} */ - addMutatedNode ( node ) { - return this.setIn( [ OPTION_MUTATED_NODES, node ], true ); + hasReturnExpressionBeenAssignedAtPath ( path, callExpression ) { + return this._optionValues.getIn( [ OPTION_RETURN_EXPRESSIONS_ASSIGNED_AT_PATH, callExpression, ...path, RESULT_KEY ] ); } /** - * @param {Node} node + * @param {String[]} path + * @param {CallExpression} callExpression * @return {boolean} */ - hasNodeBeenMutated ( node ) { - return this._optionValues.getIn( [ OPTION_MUTATED_NODES, node ] ); + hasReturnExpressionBeenCalledAtPath ( path, callExpression ) { + return this._optionValues.getIn( [ OPTION_RETURN_EXPRESSIONS_CALLED_AT_PATH, callExpression, ...path, RESULT_KEY ] ); } /** - * @param {Node} node - * @param {Object} callOptions - * @return {ExecutionPathOptions} + * @return {boolean} */ - addNodeCalledWithOptions ( node, callOptions ) { - return this.setIn( [ OPTION_NODES_CALLED_WITH_OPTIONS, node, callOptions ], true ); + ignoreBreakStatements () { + return this.get( OPTION_IGNORE_BREAK_STATEMENTS ); } /** - * @param {Node} node - * @param {Object} callOptions + * @param {string} labelName * @return {boolean} */ - hasNodeBeenCalledWithOptions ( node, callOptions ) { - return this._optionValues.hasIn( [ OPTION_NODES_CALLED_WITH_OPTIONS, node ] ) - && this._optionValues.getIn( [ OPTION_NODES_CALLED_WITH_OPTIONS, node ] ) - .find( ( _, options ) => areCallOptionsEqual( options, callOptions ) ); + ignoreLabel ( labelName ) { + return this._optionValues.getIn( [ IGNORED_LABELS, labelName ] ); } /** - * @param {Node} node - * @param {Object} callOptions - * @return {ExecutionPathOptions} + * @return {boolean} */ - getHasEffectsWhenCalledOptions ( node, callOptions ) { - return this - .setIgnoreReturnAwaitYield() - .setIgnoreBreakStatements( false ) - .setIgnoreNoLabels() - .addNodeCalledWithOptions( node, callOptions ); + ignoreReturnAwaitYield () { + return this.get( OPTION_IGNORE_RETURN_AWAIT_YIELD ); } /** @@ -197,10 +210,33 @@ export default class ExecutionPathOptions { } /** - * @param {ThisVariable} thisVariable - * @returns {Node} + * @param {boolean} [value=true] + * @return {ExecutionPathOptions} */ - getReplacedThisInit ( thisVariable ) { - return this._optionValues.getIn( [ OPTION_VALID_THIS_VARIABLES, thisVariable ] ); + setIgnoreBreakStatements ( value = true ) { + return this.set( OPTION_IGNORE_BREAK_STATEMENTS, value ); + } + + /** + * @param {string} labelName + * @return {ExecutionPathOptions} + */ + setIgnoreLabel ( labelName ) { + return this.setIn( [ IGNORED_LABELS, labelName ], true ); + } + + /** + * @return {ExecutionPathOptions} + */ + setIgnoreNoLabels () { + return this.set( IGNORED_LABELS, null ); + } + + /** + * @param {boolean} [value=true] + * @return {ExecutionPathOptions} + */ + setIgnoreReturnAwaitYield ( value = true ) { + return this.set( OPTION_IGNORE_RETURN_AWAIT_YIELD, value ); } } diff --git a/src/ast/Node.js b/src/ast/Node.js index 0f7c304b876..096edceffef 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -95,7 +95,7 @@ export default class Node { /** * @param {String[]} path - * @param {Object} callOptions + * @param {CallOptions} callOptions * @param {ExecutionPathOptions} options * @return {boolean} */ @@ -230,7 +230,7 @@ export default class Node { * Returns true if some possible return expression when called at the given * path returns true. predicateFunction receives a `node` as parameter. * @param {String[]} path - * @param {Object} callOptions + * @param {CallOptions} callOptions * @param {Function} predicateFunction * @returns {boolean} */ diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index 5d257d260b0..2c8a676074c 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -1,4 +1,5 @@ import Node from '../Node.js'; +import CallOptions from '../CallOptions'; export default class CallExpression extends Node { bindNode () { @@ -25,34 +26,29 @@ export default class CallExpression extends Node { hasEffects ( options ) { return this.included || this.arguments.some( child => child.hasEffects( options ) ) - || ( - !options.hasNodeBeenCalledWithOptions( this, this._callOptions ) - && this.callee.hasEffectsWhenCalledAtPath( [], this._callOptions, - options.getHasEffectsWhenCalledOptions( this, this._callOptions ) ) - ); + || this.callee.hasEffectsWhenCalledAtPath( [], this._callOptions, options.getHasEffectsWhenCalledOptions() ); } hasEffectsWhenAccessedAtPath ( path, options ) { - if ( options.hasNodeBeenAccessedAtPath( path, this ) ) { - return false; - } - const innerOptions = options.addAccessedNodeAtPath( path, this ); - return this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, node => - node.hasEffectsWhenAccessedAtPath( path, innerOptions ) ); + return !options.hasReturnExpressionBeenAccessedAtPath( path, this ) + && this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, node => + node.hasEffectsWhenAccessedAtPath( path, options.addAccessedReturnExpressionAtPath( path, this ) ) ); } hasEffectsWhenAssignedAtPath ( path, options ) { - return this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, node => - node.hasEffectsWhenAssignedAtPath( path, options ) ); + return !options.hasReturnExpressionBeenAssignedAtPath( path, this ) + && this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, node => + node.hasEffectsWhenAssignedAtPath( path, options.addAssignedReturnExpressionAtPath( path, this ) ) ); } hasEffectsWhenCalledAtPath ( path, callOptions, options ) { - return this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, node => - node.hasEffectsWhenCalledAtPath( path, callOptions, options ) ); + return !options.hasReturnExpressionBeenCalledAtPath( path, this ) + && this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, node => + node.hasEffectsWhenCalledAtPath( path, callOptions, options.addCalledReturnExpressionAtPath( path, this ) ) ); } initialiseNode () { - this._callOptions = { withNew: false }; + this._callOptions = CallOptions.create( { withNew: false } ); } someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { diff --git a/src/ast/nodes/NewExpression.js b/src/ast/nodes/NewExpression.js index df791075e22..d7644803949 100644 --- a/src/ast/nodes/NewExpression.js +++ b/src/ast/nodes/NewExpression.js @@ -1,11 +1,11 @@ import Node from '../Node.js'; +import CallOptions from '../CallOptions'; export default class NewExpression extends Node { hasEffects ( options ) { return this.included || this.arguments.some( child => child.hasEffects( options ) ) - || this.callee.hasEffectsWhenCalledAtPath( [], this._callOptions, - options.getHasEffectsWhenCalledOptions( this, this._callOptions ) ); + || this.callee.hasEffectsWhenCalledAtPath( [], this._callOptions, options.getHasEffectsWhenCalledOptions() ); } hasEffectsWhenAccessedAtPath ( path ) { @@ -13,6 +13,6 @@ export default class NewExpression extends Node { } initialiseNode () { - this._callOptions = { withNew: true }; + this._callOptions = CallOptions.create( { withNew: true } ); } } diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index cccd79633ca..8f65b07708d 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -1,4 +1,5 @@ import Node from '../Node.js'; +import CallOptions from '../CallOptions'; import { UNKNOWN_ASSIGNMENT } from '../values'; export default class Property extends Node { @@ -15,8 +16,7 @@ export default class Property extends Node { hasEffectsWhenAccessedAtPath ( path, options ) { if ( this.kind === 'get' ) { return path.length > 0 - || this.value.hasEffectsWhenCalledAtPath( [], this._callOptions, - options.getHasEffectsWhenCalledOptions( this, this._callOptions ) ); + || this.value.hasEffectsWhenCalledAtPath( [], this._callOptions, options.getHasEffectsWhenCalledOptions() ); } return this.value.hasEffectsWhenAccessedAtPath( path, options ); } @@ -24,8 +24,7 @@ export default class Property extends Node { hasEffectsWhenAssignedAtPath ( path, options ) { if ( this.kind === 'set' ) { return path.length > 0 - || this.value.hasEffectsWhenCalledAtPath( [], this._callOptions, - options.getHasEffectsWhenCalledOptions( this, this._callOptions ) ); + || this.value.hasEffectsWhenCalledAtPath( [], this._callOptions, options.getHasEffectsWhenCalledOptions() ); } return this.value.hasEffectsWhenAssignedAtPath( path, options ); } @@ -45,7 +44,7 @@ export default class Property extends Node { } initialiseNode () { - this._callOptions = { withNew: false }; + this._callOptions = CallOptions.create( { withNew: false } ); } render ( code, es ) { diff --git a/src/ast/nodes/TaggedTemplateExpression.js b/src/ast/nodes/TaggedTemplateExpression.js index d8aaea58f34..5fd4b6a6a91 100644 --- a/src/ast/nodes/TaggedTemplateExpression.js +++ b/src/ast/nodes/TaggedTemplateExpression.js @@ -1,4 +1,5 @@ import Node from '../Node.js'; +import CallOptions from '../CallOptions'; export default class TaggedTemplateExpression extends Node { bindNode () { @@ -24,11 +25,10 @@ export default class TaggedTemplateExpression extends Node { hasEffects ( options ) { return super.hasEffects( options ) - || this.tag.hasEffectsWhenCalledAtPath( [], this._callOptions, - options.getHasEffectsWhenCalledOptions( this, this._callOptions ) ); + || this.tag.hasEffectsWhenCalledAtPath( [], this._callOptions, options.getHasEffectsWhenCalledOptions() ); } initialiseNode () { - this._callOptions = { withNew: false }; + this._callOptions = CallOptions.create( { withNew: false } ); } } diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index f86b0e39f24..58395ee8672 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -33,12 +33,12 @@ export default class FunctionNode extends Node { return true; } - hasEffectsWhenCalledAtPath ( path, { withNew }, options ) { + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { if ( path.length > 0 ) { return true; } const innerOptions = options.replaceThisInit( this.thisVariable, - withNew ? new VirtualObjectExpression() : UNKNOWN_ASSIGNMENT ); + callOptions.withNew ? new VirtualObjectExpression() : UNKNOWN_ASSIGNMENT ); return this.params.some( param => param.hasEffects( innerOptions ) ) || this.body.hasEffects( innerOptions ); } diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index eaf363387ea..e49b1574607 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -19,16 +19,12 @@ export default class LocalVariable extends Variable { } assignExpressionAtPath ( path, expression ) { - if ( path.length > MAX_PATH_LENGTH ) { - return; - } - if ( this.assignedExpressions.hasAtPath( path, expression ) ) return; + if ( path.length > MAX_PATH_LENGTH || this.assignedExpressions.hasAtPath( path, expression ) ) return; this.assignedExpressions.addAtPath( path, expression ); if ( path.length > 0 ) { this.assignedExpressions.forEachAtPath( path.slice( 0, -1 ), ( relativePath, node ) => node.bindAssignmentAtPath( [ ...relativePath, ...path.slice( -1 ) ], expression ) ); - } - if ( path.length === 0 ) { + } else { this.isReassigned = true; } } @@ -41,36 +37,30 @@ export default class LocalVariable extends Variable { } hasEffectsWhenAccessedAtPath ( path, options ) { - if ( path.length > MAX_PATH_LENGTH ) { - return true; - } - return this.assignedExpressions.someAtPath( path, ( relativePath, node ) => - !options.hasNodeBeenAccessedAtPath( relativePath, node ) - && node.hasEffectsWhenAccessedAtPath( relativePath, options.addAccessedNodeAtPath( relativePath, node ) ) ); + return path.length > MAX_PATH_LENGTH + || this.assignedExpressions.someAtPath( path, ( relativePath, node ) => + !options.hasNodeBeenAccessedAtPath( relativePath, node ) + && node.hasEffectsWhenAccessedAtPath( relativePath, options + .addAccessedNodeAtPath( relativePath, node ) ) ); } hasEffectsWhenAssignedAtPath ( path, options ) { - if ( path.length > MAX_PATH_LENGTH ) { - return true; - } return this.included + || path.length > MAX_PATH_LENGTH || this.assignedExpressions.someAtPath( path, ( relativePath, node ) => relativePath.length > 0 && !options.hasNodeBeenAssignedAtPath( relativePath, node ) - && node.hasEffectsWhenAssignedAtPath( relativePath, options.addAssignedNodeAtPath( relativePath, node ) ) ); + && node.hasEffectsWhenAssignedAtPath( relativePath, options + .addAssignedNodeAtPath( relativePath, node ) ) ); } hasEffectsWhenCalledAtPath ( path, callOptions, options ) { - if ( path.length > MAX_PATH_LENGTH ) { - return true; - } - return this.assignedExpressions.someAtPath( path, ( relativePath, node ) => { - if ( relativePath.length === 0 ) { - return !options.hasNodeBeenCalledWithOptions( node, callOptions ) - && node.hasEffectsWhenCalledAtPath( [], callOptions, options.addNodeCalledWithOptions( node, callOptions ) ); - } - return node.hasEffectsWhenCalledAtPath( relativePath, callOptions, options ); - } ); + return path.length > MAX_PATH_LENGTH + || this.assignedExpressions.someAtPath( path, ( relativePath, node ) => + !options.hasNodeBeenCalledAtPathWithOptions( relativePath, node, callOptions ) + && node.hasEffectsWhenCalledAtPath( relativePath, callOptions, options + .addCalledNodeAtPathWithOptions( relativePath, node, callOptions ) ) + ); } includeVariable () { @@ -82,11 +72,11 @@ export default class LocalVariable extends Variable { } someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { - if ( path.length > MAX_PATH_LENGTH ) { - return true; - } - return this.assignedExpressions.someAtPath( path, ( relativePath, node ) => - node.someReturnExpressionWhenCalledAtPath( relativePath, callOptions, predicateFunction ) ); + return path.length > MAX_PATH_LENGTH + || this.assignedExpressions.someAtPath( path, ( relativePath, node ) => + !callOptions.hasNodeBeenCalledAtPath( relativePath, node ) + && node.someReturnExpressionWhenCalledAtPath( relativePath, callOptions + .addCalledNodeAtPath( relativePath, node ), predicateFunction ) ); } toString () { diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index 6dfcaa0ef4c..2c71172e6df 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -50,7 +50,7 @@ export default class Variable { /** * @param {String[]} path * @param {ExecutionPathOptions} options - * @param {Object} callOptions + * @param {CallOptions} callOptions * @return {boolean} */ hasEffectsWhenCalledAtPath ( path, callOptions, options ) { @@ -74,7 +74,7 @@ export default class Variable { /** * @param {String[]} path - * @param {Object} callOptions + * @param {CallOptions} callOptions * @param {Function} predicateFunction * @returns {boolean} */ diff --git a/test/form/samples/cyclic-assignments/main.js b/test/form/samples/cyclic-assignments/main.js index 0405142963c..943b5a6f9ab 100644 --- a/test/form/samples/cyclic-assignments/main.js +++ b/test/form/samples/cyclic-assignments/main.js @@ -1,13 +1,10 @@ -let a = {}; +let a = { foo: () => {}, bar: () => () => {} }; let b = a; a = b; -a.foo = 1; +a.foo = () => {}; +a.foo(); + b = b; -b.foo = 1; +b.bar = () => () => {}; -let c = () => {}; -let d = c; -c = d; -c(); -d = d; -d(); +b.bar()(); diff --git a/test/form/samples/function-body-return-values/_expected/amd.js b/test/form/samples/function-body-return-values/_expected/amd.js index c9cae50e93a..ec039c4ce51 100644 --- a/test/form/samples/function-body-return-values/_expected/amd.js +++ b/test/form/samples/function-body-return-values/_expected/amd.js @@ -34,7 +34,7 @@ define(function () { 'use strict'; retained5().x.y = 1; function retained6 () { - return { x: () => console.log('effect') }; + return { x: () => console.log( 'effect' ) }; } retained6().x(); diff --git a/test/form/samples/function-body-return-values/_expected/cjs.js b/test/form/samples/function-body-return-values/_expected/cjs.js index a8bf384102c..bb29db746fb 100644 --- a/test/form/samples/function-body-return-values/_expected/cjs.js +++ b/test/form/samples/function-body-return-values/_expected/cjs.js @@ -34,7 +34,7 @@ function retained5 () { retained5().x.y = 1; function retained6 () { - return { x: () => console.log('effect') }; + return { x: () => console.log( 'effect' ) }; } retained6().x(); diff --git a/test/form/samples/function-body-return-values/_expected/es.js b/test/form/samples/function-body-return-values/_expected/es.js index e1646edce5d..43220dda943 100644 --- a/test/form/samples/function-body-return-values/_expected/es.js +++ b/test/form/samples/function-body-return-values/_expected/es.js @@ -32,7 +32,7 @@ function retained5 () { retained5().x.y = 1; function retained6 () { - return { x: () => console.log('effect') }; + return { x: () => console.log( 'effect' ) }; } retained6().x(); diff --git a/test/form/samples/function-body-return-values/_expected/iife.js b/test/form/samples/function-body-return-values/_expected/iife.js index bd70f35e821..db28c2aa400 100644 --- a/test/form/samples/function-body-return-values/_expected/iife.js +++ b/test/form/samples/function-body-return-values/_expected/iife.js @@ -35,7 +35,7 @@ retained5().x.y = 1; function retained6 () { - return { x: () => console.log('effect') }; + return { x: () => console.log( 'effect' ) }; } retained6().x(); diff --git a/test/form/samples/function-body-return-values/_expected/umd.js b/test/form/samples/function-body-return-values/_expected/umd.js index 30bd6862c9f..6a97eda5558 100644 --- a/test/form/samples/function-body-return-values/_expected/umd.js +++ b/test/form/samples/function-body-return-values/_expected/umd.js @@ -38,7 +38,7 @@ retained5().x.y = 1; function retained6 () { - return { x: () => console.log('effect') }; + return { x: () => console.log( 'effect' ) }; } retained6().x(); diff --git a/test/form/samples/function-body-return-values/main.js b/test/form/samples/function-body-return-values/main.js index 24e2a8a0611..197b10a41fd 100644 --- a/test/form/samples/function-body-return-values/main.js +++ b/test/form/samples/function-body-return-values/main.js @@ -50,7 +50,7 @@ function retained5 () { retained5().x.y = 1; function retained6 () { - return { x: () => console.log('effect') }; + return { x: () => console.log( 'effect' ) }; } retained6().x(); diff --git a/test/form/samples/recursive-assignments/_expected/amd.js b/test/form/samples/recursive-assignments/_expected/amd.js index 262f3db04e3..9dd7aaebe68 100644 --- a/test/form/samples/recursive-assignments/_expected/amd.js +++ b/test/form/samples/recursive-assignments/_expected/amd.js @@ -1,6 +1,6 @@ define(function () { 'use strict'; - let foo = () => {}; + let foo = () => function () {}; foo.value = foo; while ( foo.value ) { @@ -8,7 +8,8 @@ define(function () { 'use strict'; } foo(); + foo()(); + new (foo())(); foo.bar = 1; - foo['baz'] = 1; }); diff --git a/test/form/samples/recursive-assignments/_expected/cjs.js b/test/form/samples/recursive-assignments/_expected/cjs.js index 4ace26df3e2..5cbf3b4ab1d 100644 --- a/test/form/samples/recursive-assignments/_expected/cjs.js +++ b/test/form/samples/recursive-assignments/_expected/cjs.js @@ -1,6 +1,6 @@ 'use strict'; -let foo = () => {}; +let foo = () => function () {}; foo.value = foo; while ( foo.value ) { @@ -8,5 +8,6 @@ while ( foo.value ) { } foo(); +foo()(); +new (foo())(); foo.bar = 1; -foo['baz'] = 1; diff --git a/test/form/samples/recursive-assignments/_expected/es.js b/test/form/samples/recursive-assignments/_expected/es.js index 42f9044790f..3535a634b90 100644 --- a/test/form/samples/recursive-assignments/_expected/es.js +++ b/test/form/samples/recursive-assignments/_expected/es.js @@ -1,4 +1,4 @@ -let foo = () => {}; +let foo = () => function () {}; foo.value = foo; while ( foo.value ) { @@ -6,5 +6,6 @@ while ( foo.value ) { } foo(); +foo()(); +new (foo())(); foo.bar = 1; -foo['baz'] = 1; diff --git a/test/form/samples/recursive-assignments/_expected/iife.js b/test/form/samples/recursive-assignments/_expected/iife.js index 611bd2db1c0..b8be3815254 100644 --- a/test/form/samples/recursive-assignments/_expected/iife.js +++ b/test/form/samples/recursive-assignments/_expected/iife.js @@ -1,7 +1,7 @@ (function () { 'use strict'; - let foo = () => {}; + let foo = () => function () {}; foo.value = foo; while ( foo.value ) { @@ -9,7 +9,8 @@ } foo(); + foo()(); + new (foo())(); foo.bar = 1; - foo['baz'] = 1; }()); diff --git a/test/form/samples/recursive-assignments/_expected/umd.js b/test/form/samples/recursive-assignments/_expected/umd.js index 074e4c950f1..c94ff4f0700 100644 --- a/test/form/samples/recursive-assignments/_expected/umd.js +++ b/test/form/samples/recursive-assignments/_expected/umd.js @@ -4,7 +4,7 @@ (factory()); }(this, (function () { 'use strict'; - let foo = () => {}; + let foo = () => function () {}; foo.value = foo; while ( foo.value ) { @@ -12,7 +12,8 @@ } foo(); + foo()(); + new (foo())(); foo.bar = 1; - foo['baz'] = 1; }))); diff --git a/test/form/samples/recursive-assignments/main.js b/test/form/samples/recursive-assignments/main.js index 42f9044790f..211933a3142 100644 --- a/test/form/samples/recursive-assignments/main.js +++ b/test/form/samples/recursive-assignments/main.js @@ -1,4 +1,4 @@ -let foo = () => {}; +let foo = () => function () {}; foo.value = foo; while ( foo.value ) { @@ -6,5 +6,6 @@ while ( foo.value ) { } foo(); +foo()(); +new (foo())() foo.bar = 1; -foo['baz'] = 1; diff --git a/test/form/samples/recursive-calls/_expected/amd.js b/test/form/samples/recursive-calls/_expected/amd.js index f9f8229aa40..f61ff9bbb61 100644 --- a/test/form/samples/recursive-calls/_expected/amd.js +++ b/test/form/samples/recursive-calls/_expected/amd.js @@ -1,5 +1,15 @@ define(function () { 'use strict'; + const retained1 = () => globalVar ? retained1() : console.log( 'effect' ); + retained1(); + const retained2 = () => () => globalVar ? retained2()() : console.log( 'effect' ); + retained2()(); + + const retained3 = () => globalVar ? retained3() : {}; + retained3().x.y = 3; + + const retained4 = () => globalVar ? retained4() : { x: () => console.log( 'effect' ) }; + retained4().x(); }); diff --git a/test/form/samples/recursive-calls/_expected/cjs.js b/test/form/samples/recursive-calls/_expected/cjs.js index eb109abbed0..9308043bfb3 100644 --- a/test/form/samples/recursive-calls/_expected/cjs.js +++ b/test/form/samples/recursive-calls/_expected/cjs.js @@ -1,2 +1,13 @@ 'use strict'; +const retained1 = () => globalVar ? retained1() : console.log( 'effect' ); +retained1(); + +const retained2 = () => () => globalVar ? retained2()() : console.log( 'effect' ); +retained2()(); + +const retained3 = () => globalVar ? retained3() : {}; +retained3().x.y = 3; + +const retained4 = () => globalVar ? retained4() : { x: () => console.log( 'effect' ) }; +retained4().x(); diff --git a/test/form/samples/recursive-calls/_expected/es.js b/test/form/samples/recursive-calls/_expected/es.js index 8b137891791..0dec965e40d 100644 --- a/test/form/samples/recursive-calls/_expected/es.js +++ b/test/form/samples/recursive-calls/_expected/es.js @@ -1 +1,11 @@ +const retained1 = () => globalVar ? retained1() : console.log( 'effect' ); +retained1(); +const retained2 = () => () => globalVar ? retained2()() : console.log( 'effect' ); +retained2()(); + +const retained3 = () => globalVar ? retained3() : {}; +retained3().x.y = 3; + +const retained4 = () => globalVar ? retained4() : { x: () => console.log( 'effect' ) }; +retained4().x(); diff --git a/test/form/samples/recursive-calls/_expected/iife.js b/test/form/samples/recursive-calls/_expected/iife.js index 43ef5426880..1d6607cdc0a 100644 --- a/test/form/samples/recursive-calls/_expected/iife.js +++ b/test/form/samples/recursive-calls/_expected/iife.js @@ -1,6 +1,16 @@ (function () { 'use strict'; + const retained1 = () => globalVar ? retained1() : console.log( 'effect' ); + retained1(); + const retained2 = () => () => globalVar ? retained2()() : console.log( 'effect' ); + retained2()(); + + const retained3 = () => globalVar ? retained3() : {}; + retained3().x.y = 3; + + const retained4 = () => globalVar ? retained4() : { x: () => console.log( 'effect' ) }; + retained4().x(); }()); diff --git a/test/form/samples/recursive-calls/_expected/umd.js b/test/form/samples/recursive-calls/_expected/umd.js index 07ce27e42f1..d6cdd4f137f 100644 --- a/test/form/samples/recursive-calls/_expected/umd.js +++ b/test/form/samples/recursive-calls/_expected/umd.js @@ -4,6 +4,16 @@ (factory()); }(this, (function () { 'use strict'; + const retained1 = () => globalVar ? retained1() : console.log( 'effect' ); + retained1(); + const retained2 = () => () => globalVar ? retained2()() : console.log( 'effect' ); + retained2()(); + + const retained3 = () => globalVar ? retained3() : {}; + retained3().x.y = 3; + + const retained4 = () => globalVar ? retained4() : { x: () => console.log( 'effect' ) }; + retained4().x(); }))); diff --git a/test/form/samples/recursive-calls/main.js b/test/form/samples/recursive-calls/main.js index ba0d2a89876..9cce984c38a 100644 --- a/test/form/samples/recursive-calls/main.js +++ b/test/form/samples/recursive-calls/main.js @@ -1,5 +1,23 @@ -const removed1 = () => removed1(); +const removed1 = () => globalVar || removed1(); removed1(); -const removed2 = () => () => removed2()(); +const removed2 = () => () => globalVar || removed2()(); removed2()(); + +const removed3 = () => globalVar ? removed3() : {}; +removed3().x = 3; + +const removed4 = () => globalVar ? removed4() : { x: () => {} }; +removed4().x(); + +const retained1 = () => globalVar ? retained1() : console.log( 'effect' ); +retained1(); + +const retained2 = () => () => globalVar ? retained2()() : console.log( 'effect' ); +retained2()(); + +const retained3 = () => globalVar ? retained3() : {}; +retained3().x.y = 3; + +const retained4 = () => globalVar ? retained4() : { x: () => console.log( 'effect' ) }; +retained4().x(); From 55ec590ba6ad387426264d47988c237396900ec3 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Sat, 14 Oct 2017 20:10:45 +0200 Subject: [PATCH 45/76] Properly detect side-effects when handling getter return values. --- src/ast/ExecutionPathOptions.js | 12 ++-- src/ast/Node.js | 3 +- src/ast/nodes/ArrowFunctionExpression.js | 4 +- src/ast/nodes/CallExpression.js | 10 ++-- src/ast/nodes/Identifier.js | 4 +- src/ast/nodes/MemberExpression.js | 6 +- src/ast/nodes/ObjectExpression.js | 7 ++- src/ast/nodes/Property.js | 23 ++++--- src/ast/variables/LocalVariable.js | 4 +- src/ast/variables/Variable.js | 5 +- .../samples/getter-return-values/_config.js | 3 + .../getter-return-values/_expected/amd.js | 39 ++++++++++++ .../getter-return-values/_expected/cjs.js | 37 ++++++++++++ .../getter-return-values/_expected/es.js | 35 +++++++++++ .../getter-return-values/_expected/iife.js | 40 +++++++++++++ .../getter-return-values/_expected/umd.js | 43 +++++++++++++ .../form/samples/getter-return-values/main.js | 50 ++++++++++++++++ .../samples/recursive-calls/_expected/amd.js | 30 +++++++++- .../samples/recursive-calls/_expected/cjs.js | 30 +++++++++- .../samples/recursive-calls/_expected/es.js | 30 +++++++++- .../samples/recursive-calls/_expected/iife.js | 30 +++++++++- .../samples/recursive-calls/_expected/umd.js | 30 +++++++++- test/form/samples/recursive-calls/main.js | 60 ++++++++++++++++++- 23 files changed, 494 insertions(+), 41 deletions(-) create mode 100644 test/form/samples/getter-return-values/_config.js create mode 100644 test/form/samples/getter-return-values/_expected/amd.js create mode 100644 test/form/samples/getter-return-values/_expected/cjs.js create mode 100644 test/form/samples/getter-return-values/_expected/es.js create mode 100644 test/form/samples/getter-return-values/_expected/iife.js create mode 100644 test/form/samples/getter-return-values/_expected/umd.js create mode 100644 test/form/samples/getter-return-values/main.js diff --git a/src/ast/ExecutionPathOptions.js b/src/ast/ExecutionPathOptions.js index ababc966da2..3a2c911af55 100644 --- a/src/ast/ExecutionPathOptions.js +++ b/src/ast/ExecutionPathOptions.js @@ -60,7 +60,7 @@ export default class ExecutionPathOptions { /** * @param {String[]} path - * @param {CallExpression} callExpression + * @param {CallExpression|Property} callExpression * @return {ExecutionPathOptions} */ addAccessedReturnExpressionAtPath ( path, callExpression ) { @@ -78,7 +78,7 @@ export default class ExecutionPathOptions { /** * @param {String[]} path - * @param {CallExpression} callExpression + * @param {CallExpression|Property} callExpression * @return {ExecutionPathOptions} */ addAssignedReturnExpressionAtPath ( path, callExpression ) { @@ -97,7 +97,7 @@ export default class ExecutionPathOptions { /** * @param {String[]} path - * @param {CallExpression} callExpression + * @param {CallExpression|Property} callExpression * @return {ExecutionPathOptions} */ addCalledReturnExpressionAtPath ( path, callExpression ) { @@ -153,7 +153,7 @@ export default class ExecutionPathOptions { /** * @param {String[]} path - * @param {CallExpression} callExpression + * @param {CallExpression|Property} callExpression * @return {boolean} */ hasReturnExpressionBeenAccessedAtPath ( path, callExpression ) { @@ -162,7 +162,7 @@ export default class ExecutionPathOptions { /** * @param {String[]} path - * @param {CallExpression} callExpression + * @param {CallExpression|Property} callExpression * @return {boolean} */ hasReturnExpressionBeenAssignedAtPath ( path, callExpression ) { @@ -171,7 +171,7 @@ export default class ExecutionPathOptions { /** * @param {String[]} path - * @param {CallExpression} callExpression + * @param {CallExpression|Property} callExpression * @return {boolean} */ hasReturnExpressionBeenCalledAtPath ( path, callExpression ) { diff --git a/src/ast/Node.js b/src/ast/Node.js index 096edceffef..679cdeeaea3 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -232,9 +232,10 @@ export default class Node { * @param {String[]} path * @param {CallOptions} callOptions * @param {Function} predicateFunction + * @param {ExecutionPathOptions} options * @returns {boolean} */ - someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { return predicateFunction( UNKNOWN_ASSIGNMENT ); } diff --git a/src/ast/nodes/ArrowFunctionExpression.js b/src/ast/nodes/ArrowFunctionExpression.js index 5b29828b31c..bd12a26a57d 100644 --- a/src/ast/nodes/ArrowFunctionExpression.js +++ b/src/ast/nodes/ArrowFunctionExpression.js @@ -42,7 +42,7 @@ export default class ArrowFunctionExpression extends Node { this.scope = new ReturnValueScope( { parent: parentScope } ); } - someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { - return this.scope.someReturnExpressionWhenCalled( callOptions, predicateFunction ); + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { + return this.scope.someReturnExpressionWhenCalled( callOptions, predicateFunction, options ); } } diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index 2c8a676074c..c96f2a25556 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -32,27 +32,27 @@ export default class CallExpression extends Node { hasEffectsWhenAccessedAtPath ( path, options ) { return !options.hasReturnExpressionBeenAccessedAtPath( path, this ) && this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, node => - node.hasEffectsWhenAccessedAtPath( path, options.addAccessedReturnExpressionAtPath( path, this ) ) ); + node.hasEffectsWhenAccessedAtPath( path, options.addAccessedReturnExpressionAtPath( path, this ) ), options ); } hasEffectsWhenAssignedAtPath ( path, options ) { return !options.hasReturnExpressionBeenAssignedAtPath( path, this ) && this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, node => - node.hasEffectsWhenAssignedAtPath( path, options.addAssignedReturnExpressionAtPath( path, this ) ) ); + node.hasEffectsWhenAssignedAtPath( path, options.addAssignedReturnExpressionAtPath( path, this ) ), options ); } hasEffectsWhenCalledAtPath ( path, callOptions, options ) { return !options.hasReturnExpressionBeenCalledAtPath( path, this ) && this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, node => - node.hasEffectsWhenCalledAtPath( path, callOptions, options.addCalledReturnExpressionAtPath( path, this ) ) ); + node.hasEffectsWhenCalledAtPath( path, callOptions, options.addCalledReturnExpressionAtPath( path, this ) ), options ); } initialiseNode () { this._callOptions = CallOptions.create( { withNew: false } ); } - someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { return this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, node => - node.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction ) ); + node.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ), options ); } } diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index b6b45bc305c..bc094d9bca1 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -77,9 +77,9 @@ export default class Identifier extends Node { } } - someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { if ( this.variable ) { - return this.variable.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction ); + return this.variable.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ); } return predicateFunction( UNKNOWN_ASSIGNMENT ); } diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index 15b108e33a8..c2791bc7fed 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -138,11 +138,11 @@ export default class MemberExpression extends Node { super.render( code, es ); } - someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { if ( this.variable ) { - return this.variable.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction ); + return this.variable.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ); } return this.computed - || this.object.someReturnExpressionWhenCalledAtPath( [ this.property.name, ...path ], callOptions, predicateFunction ); + || this.object.someReturnExpressionWhenCalledAtPath( [ this.property.name, ...path ], callOptions, predicateFunction, options ); } } diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index 99fc8f25354..d0e184f1c69 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -48,7 +48,8 @@ export default class ObjectExpression extends Node { if ( path.length === 0 ) { return false; } - const { properties, hasCertainHit } = this._getPossiblePropertiesWithName( path[ 0 ], PROPERTY_KINDS_WRITE ); + 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') @@ -65,13 +66,13 @@ export default class ObjectExpression extends Node { property.hasEffectsWhenCalledAtPath( path.slice( 1 ), callOptions, options ) ); } - someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { 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 ) ); + property.someReturnExpressionWhenCalledAtPath( path.slice( 1 ), callOptions, predicateFunction, options ) ); } } diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index 8f65b07708d..26cd47df355 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -15,8 +15,10 @@ export default class Property extends Node { hasEffectsWhenAccessedAtPath ( path, options ) { if ( this.kind === 'get' ) { - return path.length > 0 - || this.value.hasEffectsWhenCalledAtPath( [], this._callOptions, options.getHasEffectsWhenCalledOptions() ); + return this.value.hasEffectsWhenCalledAtPath( [], this._getterCallOptions, options.getHasEffectsWhenCalledOptions() ) + || (!options.hasReturnExpressionBeenAccessedAtPath( path, this ) + && this.value.someReturnExpressionWhenCalledAtPath( [], this._getterCallOptions, node => + node.hasEffectsWhenAccessedAtPath( path, options.addAccessedReturnExpressionAtPath( path, this ) ), options )); } return this.value.hasEffectsWhenAccessedAtPath( path, options ); } @@ -24,14 +26,17 @@ export default class Property extends Node { hasEffectsWhenAssignedAtPath ( path, options ) { if ( this.kind === 'set' ) { return path.length > 0 - || this.value.hasEffectsWhenCalledAtPath( [], this._callOptions, options.getHasEffectsWhenCalledOptions() ); + || this.value.hasEffectsWhenCalledAtPath( [], this._getterCallOptions, options.getHasEffectsWhenCalledOptions() ); } return this.value.hasEffectsWhenAssignedAtPath( path, options ); } hasEffectsWhenCalledAtPath ( path, callOptions, options ) { if ( this.kind === 'get' ) { - return true; + return this.value.hasEffectsWhenCalledAtPath( [], this._getterCallOptions, options.getHasEffectsWhenCalledOptions() ) + || (!options.hasReturnExpressionBeenCalledAtPath( path, this ) + && this.value.someReturnExpressionWhenCalledAtPath( [], this._getterCallOptions, node => + node.hasEffectsWhenCalledAtPath( path, callOptions, options.addCalledReturnExpressionAtPath( path, this ) ), options )); } return this.value.hasEffectsWhenCalledAtPath( path, callOptions, options ); } @@ -44,7 +49,7 @@ export default class Property extends Node { } initialiseNode () { - this._callOptions = CallOptions.create( { withNew: false } ); + this._getterCallOptions = CallOptions.create( { withNew: false } ); } render ( code, es ) { @@ -54,10 +59,12 @@ export default class Property extends Node { this.value.render( code, es ); } - someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { if ( this.kind === 'get' ) { - return true; + return this.value.hasEffectsWhenCalledAtPath( [], this._getterCallOptions, options.getHasEffectsWhenCalledOptions() ) + || this.value.someReturnExpressionWhenCalledAtPath( [], this._getterCallOptions, node => + node.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ), options ); } - return this.value.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction ); + return this.value.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ); } } diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index e49b1574607..1aa9061e117 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -71,12 +71,12 @@ export default class LocalVariable extends Variable { return hasBeenIncluded; } - someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { return path.length > MAX_PATH_LENGTH || this.assignedExpressions.someAtPath( path, ( relativePath, node ) => !callOptions.hasNodeBeenCalledAtPath( relativePath, node ) && node.someReturnExpressionWhenCalledAtPath( relativePath, callOptions - .addCalledNodeAtPath( relativePath, node ), predicateFunction ) ); + .addCalledNodeAtPath( relativePath, node ), predicateFunction, options ) ); } toString () { diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index 2c71172e6df..982ab51f9a4 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -49,8 +49,8 @@ export default class Variable { /** * @param {String[]} path - * @param {ExecutionPathOptions} options * @param {CallOptions} callOptions + * @param {ExecutionPathOptions} options * @return {boolean} */ hasEffectsWhenCalledAtPath ( path, callOptions, options ) { @@ -76,9 +76,10 @@ export default class Variable { * @param {String[]} path * @param {CallOptions} callOptions * @param {Function} predicateFunction + * @param {ExecutionPathOptions} options * @returns {boolean} */ - someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { return predicateFunction( UNKNOWN_ASSIGNMENT ); } } diff --git a/test/form/samples/getter-return-values/_config.js b/test/form/samples/getter-return-values/_config.js new file mode 100644 index 00000000000..0c6bc6a1647 --- /dev/null +++ b/test/form/samples/getter-return-values/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'forwards return values of getters' +}; diff --git a/test/form/samples/getter-return-values/_expected/amd.js b/test/form/samples/getter-return-values/_expected/amd.js new file mode 100644 index 00000000000..419645f275e --- /dev/null +++ b/test/form/samples/getter-return-values/_expected/amd.js @@ -0,0 +1,39 @@ +define(function () { 'use strict'; + + ({ + get foo () { + console.log( 'effect' ); + return {}; + } + }).foo.bar; + ({ + get foo () { + return {}; + } + }).foo.bar.baz; + + ({ + get foo () { + console.log( 'effect' ); + return () => {}; + } + }).foo(); + ({ + get foo () { + return () => console.log( 'effect' ); + } + }).foo(); + + ({ + get foo () { + console.log( 'effect' ); + return () => () => {}; + } + }).foo()(); + ({ + get foo () { + return () => () => console.log( 'effect' ); + } + }).foo()(); + +}); diff --git a/test/form/samples/getter-return-values/_expected/cjs.js b/test/form/samples/getter-return-values/_expected/cjs.js new file mode 100644 index 00000000000..75f11467dc7 --- /dev/null +++ b/test/form/samples/getter-return-values/_expected/cjs.js @@ -0,0 +1,37 @@ +'use strict'; + +({ + get foo () { + console.log( 'effect' ); + return {}; + } +}).foo.bar; +({ + get foo () { + return {}; + } +}).foo.bar.baz; + +({ + get foo () { + console.log( 'effect' ); + return () => {}; + } +}).foo(); +({ + get foo () { + return () => console.log( 'effect' ); + } +}).foo(); + +({ + get foo () { + console.log( 'effect' ); + return () => () => {}; + } +}).foo()(); +({ + get foo () { + return () => () => console.log( 'effect' ); + } +}).foo()(); diff --git a/test/form/samples/getter-return-values/_expected/es.js b/test/form/samples/getter-return-values/_expected/es.js new file mode 100644 index 00000000000..bccc4f1da3c --- /dev/null +++ b/test/form/samples/getter-return-values/_expected/es.js @@ -0,0 +1,35 @@ +({ + get foo () { + console.log( 'effect' ); + return {}; + } +}).foo.bar; +({ + get foo () { + return {}; + } +}).foo.bar.baz; + +({ + get foo () { + console.log( 'effect' ); + return () => {}; + } +}).foo(); +({ + get foo () { + return () => console.log( 'effect' ); + } +}).foo(); + +({ + get foo () { + console.log( 'effect' ); + return () => () => {}; + } +}).foo()(); +({ + get foo () { + return () => () => console.log( 'effect' ); + } +}).foo()(); diff --git a/test/form/samples/getter-return-values/_expected/iife.js b/test/form/samples/getter-return-values/_expected/iife.js new file mode 100644 index 00000000000..71304bee381 --- /dev/null +++ b/test/form/samples/getter-return-values/_expected/iife.js @@ -0,0 +1,40 @@ +(function () { + 'use strict'; + + ({ + get foo () { + console.log( 'effect' ); + return {}; + } + }).foo.bar; + ({ + get foo () { + return {}; + } + }).foo.bar.baz; + + ({ + get foo () { + console.log( 'effect' ); + return () => {}; + } + }).foo(); + ({ + get foo () { + return () => console.log( 'effect' ); + } + }).foo(); + + ({ + get foo () { + console.log( 'effect' ); + return () => () => {}; + } + }).foo()(); + ({ + get foo () { + return () => () => console.log( 'effect' ); + } + }).foo()(); + +}()); diff --git a/test/form/samples/getter-return-values/_expected/umd.js b/test/form/samples/getter-return-values/_expected/umd.js new file mode 100644 index 00000000000..aaefd8664cb --- /dev/null +++ b/test/form/samples/getter-return-values/_expected/umd.js @@ -0,0 +1,43 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + ({ + get foo () { + console.log( 'effect' ); + return {}; + } + }).foo.bar; + ({ + get foo () { + return {}; + } + }).foo.bar.baz; + + ({ + get foo () { + console.log( 'effect' ); + return () => {}; + } + }).foo(); + ({ + get foo () { + return () => console.log( 'effect' ); + } + }).foo(); + + ({ + get foo () { + console.log( 'effect' ); + return () => () => {}; + } + }).foo()(); + ({ + get foo () { + return () => () => console.log( 'effect' ); + } + }).foo()(); + +}))); diff --git a/test/form/samples/getter-return-values/main.js b/test/form/samples/getter-return-values/main.js new file mode 100644 index 00000000000..5aa55cb085c --- /dev/null +++ b/test/form/samples/getter-return-values/main.js @@ -0,0 +1,50 @@ +({ + get foo () { + return {}; + } +}).foo.bar; +({ + get foo () { + console.log( 'effect' ); + return {}; + } +}).foo.bar; +({ + get foo () { + return {}; + } +}).foo.bar.baz; + +({ + get foo () { + return () => {}; + } +}).foo(); +({ + get foo () { + console.log( 'effect' ); + return () => {}; + } +}).foo(); +({ + get foo () { + return () => console.log( 'effect' ); + } +}).foo(); + +({ + get foo () { + return () => () => {}; + } +}).foo()(); +({ + get foo () { + console.log( 'effect' ); + return () => () => {}; + } +}).foo()(); +({ + get foo () { + return () => () => console.log( 'effect' ); + } +}).foo()(); diff --git a/test/form/samples/recursive-calls/_expected/amd.js b/test/form/samples/recursive-calls/_expected/amd.js index f61ff9bbb61..b0c545e2a2a 100644 --- a/test/form/samples/recursive-calls/_expected/amd.js +++ b/test/form/samples/recursive-calls/_expected/amd.js @@ -3,7 +3,7 @@ define(function () { 'use strict'; const retained1 = () => globalVar ? retained1() : console.log( 'effect' ); retained1(); - const retained2 = () => () => globalVar ? retained2()() : console.log( 'effect' ); + const retained2 = () => globalVar ? () => retained2()() : () => console.log( 'effect' ); retained2()(); const retained3 = () => globalVar ? retained3() : {}; @@ -12,4 +12,32 @@ define(function () { 'use strict'; const retained4 = () => globalVar ? retained4() : { x: () => console.log( 'effect' ) }; retained4().x(); + const retained5 = { + get x () { + return globalVar ? retained5.x : console.log( 'effect' ); + } + }; + retained5.x; + + const retained6 = { + get x () { + return globalVar ? retained6.x : () => console.log( 'effect' ); + } + }; + retained6.x(); + + const retained7 = { + get x () { + return globalVar ? retained7.x : {}; + } + }; + retained7.x.y.z = 7; + + const retained8 = { + get x () { + return globalVar ? retained8.x : { y: () => console.log( 'effect' ) }; + } + }; + retained8.x.y(); + }); diff --git a/test/form/samples/recursive-calls/_expected/cjs.js b/test/form/samples/recursive-calls/_expected/cjs.js index 9308043bfb3..928df1ebb04 100644 --- a/test/form/samples/recursive-calls/_expected/cjs.js +++ b/test/form/samples/recursive-calls/_expected/cjs.js @@ -3,7 +3,7 @@ const retained1 = () => globalVar ? retained1() : console.log( 'effect' ); retained1(); -const retained2 = () => () => globalVar ? retained2()() : console.log( 'effect' ); +const retained2 = () => globalVar ? () => retained2()() : () => console.log( 'effect' ); retained2()(); const retained3 = () => globalVar ? retained3() : {}; @@ -11,3 +11,31 @@ retained3().x.y = 3; const retained4 = () => globalVar ? retained4() : { x: () => console.log( 'effect' ) }; retained4().x(); + +const retained5 = { + get x () { + return globalVar ? retained5.x : console.log( 'effect' ); + } +}; +retained5.x; + +const retained6 = { + get x () { + return globalVar ? retained6.x : () => console.log( 'effect' ); + } +}; +retained6.x(); + +const retained7 = { + get x () { + return globalVar ? retained7.x : {}; + } +}; +retained7.x.y.z = 7; + +const retained8 = { + get x () { + return globalVar ? retained8.x : { y: () => console.log( 'effect' ) }; + } +}; +retained8.x.y(); diff --git a/test/form/samples/recursive-calls/_expected/es.js b/test/form/samples/recursive-calls/_expected/es.js index 0dec965e40d..55529c36ed6 100644 --- a/test/form/samples/recursive-calls/_expected/es.js +++ b/test/form/samples/recursive-calls/_expected/es.js @@ -1,7 +1,7 @@ const retained1 = () => globalVar ? retained1() : console.log( 'effect' ); retained1(); -const retained2 = () => () => globalVar ? retained2()() : console.log( 'effect' ); +const retained2 = () => globalVar ? () => retained2()() : () => console.log( 'effect' ); retained2()(); const retained3 = () => globalVar ? retained3() : {}; @@ -9,3 +9,31 @@ retained3().x.y = 3; const retained4 = () => globalVar ? retained4() : { x: () => console.log( 'effect' ) }; retained4().x(); + +const retained5 = { + get x () { + return globalVar ? retained5.x : console.log( 'effect' ); + } +}; +retained5.x; + +const retained6 = { + get x () { + return globalVar ? retained6.x : () => console.log( 'effect' ); + } +}; +retained6.x(); + +const retained7 = { + get x () { + return globalVar ? retained7.x : {}; + } +}; +retained7.x.y.z = 7; + +const retained8 = { + get x () { + return globalVar ? retained8.x : { y: () => console.log( 'effect' ) }; + } +}; +retained8.x.y(); diff --git a/test/form/samples/recursive-calls/_expected/iife.js b/test/form/samples/recursive-calls/_expected/iife.js index 1d6607cdc0a..84fd6d69a78 100644 --- a/test/form/samples/recursive-calls/_expected/iife.js +++ b/test/form/samples/recursive-calls/_expected/iife.js @@ -4,7 +4,7 @@ const retained1 = () => globalVar ? retained1() : console.log( 'effect' ); retained1(); - const retained2 = () => () => globalVar ? retained2()() : console.log( 'effect' ); + const retained2 = () => globalVar ? () => retained2()() : () => console.log( 'effect' ); retained2()(); const retained3 = () => globalVar ? retained3() : {}; @@ -13,4 +13,32 @@ const retained4 = () => globalVar ? retained4() : { x: () => console.log( 'effect' ) }; retained4().x(); + const retained5 = { + get x () { + return globalVar ? retained5.x : console.log( 'effect' ); + } + }; + retained5.x; + + const retained6 = { + get x () { + return globalVar ? retained6.x : () => console.log( 'effect' ); + } + }; + retained6.x(); + + const retained7 = { + get x () { + return globalVar ? retained7.x : {}; + } + }; + retained7.x.y.z = 7; + + const retained8 = { + get x () { + return globalVar ? retained8.x : { y: () => console.log( 'effect' ) }; + } + }; + retained8.x.y(); + }()); diff --git a/test/form/samples/recursive-calls/_expected/umd.js b/test/form/samples/recursive-calls/_expected/umd.js index d6cdd4f137f..e00ee3161cc 100644 --- a/test/form/samples/recursive-calls/_expected/umd.js +++ b/test/form/samples/recursive-calls/_expected/umd.js @@ -7,7 +7,7 @@ const retained1 = () => globalVar ? retained1() : console.log( 'effect' ); retained1(); - const retained2 = () => () => globalVar ? retained2()() : console.log( 'effect' ); + const retained2 = () => globalVar ? () => retained2()() : () => console.log( 'effect' ); retained2()(); const retained3 = () => globalVar ? retained3() : {}; @@ -16,4 +16,32 @@ const retained4 = () => globalVar ? retained4() : { x: () => console.log( 'effect' ) }; retained4().x(); + const retained5 = { + get x () { + return globalVar ? retained5.x : console.log( 'effect' ); + } + }; + retained5.x; + + const retained6 = { + get x () { + return globalVar ? retained6.x : () => console.log( 'effect' ); + } + }; + retained6.x(); + + const retained7 = { + get x () { + return globalVar ? retained7.x : {}; + } + }; + retained7.x.y.z = 7; + + const retained8 = { + get x () { + return globalVar ? retained8.x : { y: () => console.log( 'effect' ) }; + } + }; + retained8.x.y(); + }))); diff --git a/test/form/samples/recursive-calls/main.js b/test/form/samples/recursive-calls/main.js index 9cce984c38a..2fba7ddac73 100644 --- a/test/form/samples/recursive-calls/main.js +++ b/test/form/samples/recursive-calls/main.js @@ -1,7 +1,7 @@ const removed1 = () => globalVar || removed1(); removed1(); -const removed2 = () => () => globalVar || removed2()(); +const removed2 = () => globalVar ? () => removed2()() : () => {}; removed2()(); const removed3 = () => globalVar ? removed3() : {}; @@ -10,10 +10,38 @@ removed3().x = 3; const removed4 = () => globalVar ? removed4() : { x: () => {} }; removed4().x(); +const removed5 = { + get x () { + return globalVar || removed5.x; + } +}; +removed5.x; + +const removed6 = { + get x () { + return globalVar ? removed6.x : () => {}; + } +}; +removed6.x(); + +const removed7 = { + get x () { + return globalVar ? removed7.x : {}; + } +}; +removed7.x.y = 7; + +const removed8 = { + get x () { + return globalVar ? removed8.x : { y: () => {} }; + } +}; +removed8.x.y(); + const retained1 = () => globalVar ? retained1() : console.log( 'effect' ); retained1(); -const retained2 = () => () => globalVar ? retained2()() : console.log( 'effect' ); +const retained2 = () => globalVar ? () => retained2()() : () => console.log( 'effect' ); retained2()(); const retained3 = () => globalVar ? retained3() : {}; @@ -21,3 +49,31 @@ retained3().x.y = 3; const retained4 = () => globalVar ? retained4() : { x: () => console.log( 'effect' ) }; retained4().x(); + +const retained5 = { + get x () { + return globalVar ? retained5.x : console.log( 'effect' ); + } +}; +retained5.x; + +const retained6 = { + get x () { + return globalVar ? retained6.x : () => console.log( 'effect' ); + } +}; +retained6.x(); + +const retained7 = { + get x () { + return globalVar ? retained7.x : {}; + } +}; +retained7.x.y.z = 7; + +const retained8 = { + get x () { + return globalVar ? retained8.x : { y: () => console.log( 'effect' ) }; + } +}; +retained8.x.y(); From 8979d5487f82c0598273024201fd28e63bb6380d Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Sat, 14 Oct 2017 21:27:25 +0200 Subject: [PATCH 46/76] Simplify conditional expression and add effect detection for call return expressions. --- src/ast/nodes/ConditionalExpression.js | 52 +++++++------------ .../conditional-expression/_expected/amd.js | 14 ++--- .../conditional-expression/_expected/cjs.js | 14 ++--- .../conditional-expression/_expected/es.js | 14 ++--- .../conditional-expression/_expected/iife.js | 14 ++--- .../conditional-expression/_expected/umd.js | 14 ++--- .../samples/conditional-expression/main.js | 29 ++++++----- 7 files changed, 70 insertions(+), 81 deletions(-) diff --git a/src/ast/nodes/ConditionalExpression.js b/src/ast/nodes/ConditionalExpression.js index 3272a894e25..83dcb27afbc 100644 --- a/src/ast/nodes/ConditionalExpression.js +++ b/src/ast/nodes/ConditionalExpression.js @@ -24,48 +24,23 @@ export default class ConditionalExpression extends Node { return ( this.included || this.test.hasEffects( options ) - || (this.testValue === UNKNOWN_VALUE && (this.consequent.hasEffects( options ) || this.alternate.hasEffects( options ))) - || (this.testValue ? this.consequent.hasEffects( options ) : this.alternate.hasEffects( options )) + || this._someRelevantBranch( node => node.hasEffects( options ) ) ); } hasEffectsWhenAccessedAtPath ( path, options ) { - return ( - this.testValue === UNKNOWN_VALUE && ( - this.consequent.hasEffectsWhenAccessedAtPath( path, options ) - || this.alternate.hasEffectsWhenAccessedAtPath( path, options ) - ) - ) || ( - this.testValue - ? this.consequent.hasEffectsWhenAccessedAtPath( path, options ) - : this.alternate.hasEffectsWhenAccessedAtPath( path, options ) - ); + return this._someRelevantBranch( node => + node.hasEffectsWhenAccessedAtPath( path, options ) ); } hasEffectsWhenAssignedAtPath ( path, options ) { - return ( - this.testValue === UNKNOWN_VALUE && ( - this.consequent.hasEffectsWhenAssignedAtPath( path, options ) - || this.alternate.hasEffectsWhenAssignedAtPath( path, options ) - ) - ) || ( - this.testValue - ? this.consequent.hasEffectsWhenAssignedAtPath( path, options ) - : this.alternate.hasEffectsWhenAssignedAtPath( path, options ) - ); + return this._someRelevantBranch( node => + node.hasEffectsWhenAssignedAtPath( path, options ) ); } hasEffectsWhenCalledAtPath ( path, callOptions, options ) { - return ( - this.testValue === UNKNOWN_VALUE && ( - this.consequent.hasEffectsWhenCalledAtPath( path, callOptions, options ) - || this.alternate.hasEffectsWhenCalledAtPath( path, callOptions, options ) - ) - ) || ( - this.testValue - ? this.consequent.hasEffectsWhenCalledAtPath( path, callOptions, options ) - : this.alternate.hasEffectsWhenCalledAtPath( path, callOptions, options ) - ); + return this._someRelevantBranch( node => + node.hasEffectsWhenCalledAtPath( path, callOptions, options ) ); } initialiseChildren ( parentScope ) { @@ -109,4 +84,17 @@ export default class ConditionalExpression extends Node { } } } + + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { + return this._someRelevantBranch( node => + node.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ) ); + } + + _someRelevantBranch ( predicateFunction ) { + return this.testValue === UNKNOWN_VALUE + ? predicateFunction( this.consequent ) || predicateFunction( this.alternate ) + : this.testValue + ? predicateFunction( this.consequent ) + : predicateFunction( this.alternate ); + } } diff --git a/test/form/samples/conditional-expression/_expected/amd.js b/test/form/samples/conditional-expression/_expected/amd.js index 2bfcb484f92..68d948bc2f0 100644 --- a/test/form/samples/conditional-expression/_expected/amd.js +++ b/test/form/samples/conditional-expression/_expected/amd.js @@ -8,14 +8,14 @@ define(function () { 'use strict'; // unknown branch with side-effect var c = unknownValue ? foo() : 2; var d = unknownValue ? 1 : foo(); - var d1 = function () {}; - var d2 = function () {this.x = 1;}; - (unknownValue ? d1 : d2)(); + (unknownValue ? function () {} : function () {this.x = 1;})(); // known side-effect - var h = foo(); - var h1 = (function () {this.x = 1;})(); - var i = foo(); - var i1 = (function () {this.x = 1;})(); + var h1 = foo(); + var h2 = (function () {this.x = 1;})(); + var h3 = (() => () => console.log( 'effect' ))()(); + var i1 = foo(); + var i2 = (function () {this.x = 1;})(); + var i3 = (() => () => console.log( 'effect' ))()(); }); diff --git a/test/form/samples/conditional-expression/_expected/cjs.js b/test/form/samples/conditional-expression/_expected/cjs.js index 8c7e1e41588..c61c76dde61 100644 --- a/test/form/samples/conditional-expression/_expected/cjs.js +++ b/test/form/samples/conditional-expression/_expected/cjs.js @@ -8,12 +8,12 @@ var unknownValue = bar(); // unknown branch with side-effect var c = unknownValue ? foo() : 2; var d = unknownValue ? 1 : foo(); -var d1 = function () {}; -var d2 = function () {this.x = 1;}; -(unknownValue ? d1 : d2)(); +(unknownValue ? function () {} : function () {this.x = 1;})(); // known side-effect -var h = foo(); -var h1 = (function () {this.x = 1;})(); -var i = foo(); -var i1 = (function () {this.x = 1;})(); +var h1 = foo(); +var h2 = (function () {this.x = 1;})(); +var h3 = (() => () => console.log( 'effect' ))()(); +var i1 = foo(); +var i2 = (function () {this.x = 1;})(); +var i3 = (() => () => console.log( 'effect' ))()(); diff --git a/test/form/samples/conditional-expression/_expected/es.js b/test/form/samples/conditional-expression/_expected/es.js index fa9664fcf1b..b502efe6566 100644 --- a/test/form/samples/conditional-expression/_expected/es.js +++ b/test/form/samples/conditional-expression/_expected/es.js @@ -6,12 +6,12 @@ var unknownValue = bar(); // unknown branch with side-effect var c = unknownValue ? foo() : 2; var d = unknownValue ? 1 : foo(); -var d1 = function () {}; -var d2 = function () {this.x = 1;}; -(unknownValue ? d1 : d2)(); +(unknownValue ? function () {} : function () {this.x = 1;})(); // known side-effect -var h = foo(); -var h1 = (function () {this.x = 1;})(); -var i = foo(); -var i1 = (function () {this.x = 1;})(); +var h1 = foo(); +var h2 = (function () {this.x = 1;})(); +var h3 = (() => () => console.log( 'effect' ))()(); +var i1 = foo(); +var i2 = (function () {this.x = 1;})(); +var i3 = (() => () => console.log( 'effect' ))()(); diff --git a/test/form/samples/conditional-expression/_expected/iife.js b/test/form/samples/conditional-expression/_expected/iife.js index 680881f7d21..ad32053c8c8 100644 --- a/test/form/samples/conditional-expression/_expected/iife.js +++ b/test/form/samples/conditional-expression/_expected/iife.js @@ -9,14 +9,14 @@ // unknown branch with side-effect var c = unknownValue ? foo() : 2; var d = unknownValue ? 1 : foo(); - var d1 = function () {}; - var d2 = function () {this.x = 1;}; - (unknownValue ? d1 : d2)(); + (unknownValue ? function () {} : function () {this.x = 1;})(); // known side-effect - var h = foo(); - var h1 = (function () {this.x = 1;})(); - var i = foo(); - var i1 = (function () {this.x = 1;})(); + var h1 = foo(); + var h2 = (function () {this.x = 1;})(); + var h3 = (() => () => console.log( 'effect' ))()(); + var i1 = foo(); + var i2 = (function () {this.x = 1;})(); + var i3 = (() => () => console.log( 'effect' ))()(); }()); diff --git a/test/form/samples/conditional-expression/_expected/umd.js b/test/form/samples/conditional-expression/_expected/umd.js index 1d972649d27..47ebb10a071 100644 --- a/test/form/samples/conditional-expression/_expected/umd.js +++ b/test/form/samples/conditional-expression/_expected/umd.js @@ -12,14 +12,14 @@ // unknown branch with side-effect var c = unknownValue ? foo() : 2; var d = unknownValue ? 1 : foo(); - var d1 = function () {}; - var d2 = function () {this.x = 1;}; - (unknownValue ? d1 : d2)(); + (unknownValue ? function () {} : function () {this.x = 1;})(); // known side-effect - var h = foo(); - var h1 = (function () {this.x = 1;})(); - var i = foo(); - var i1 = (function () {this.x = 1;})(); + var h1 = foo(); + var h2 = (function () {this.x = 1;})(); + var h3 = (() => () => console.log( 'effect' ))()(); + var i1 = foo(); + var i2 = (function () {this.x = 1;})(); + var i3 = (() => () => console.log( 'effect' ))()(); }))); diff --git a/test/form/samples/conditional-expression/main.js b/test/form/samples/conditional-expression/main.js index de0d2ad5bf7..59a7f88e96b 100644 --- a/test/form/samples/conditional-expression/main.js +++ b/test/form/samples/conditional-expression/main.js @@ -5,26 +5,27 @@ var unknownValue = bar(); // unknown branch without side-effects var b = unknownValue ? 1 : 2; -var b1 = function () {}; -var b2 = function () {this.x = 1;}; -new (unknownValue ? b1 : b2)(); +new (unknownValue ? function () {} : function () {this.x = 1;})(); +(unknownValue ? () => () => {} : () => () => {})()(); // unknown branch with side-effect var c = unknownValue ? foo() : 2; var d = unknownValue ? 1 : foo(); -var d1 = function () {}; -var d2 = function () {this.x = 1;}; -(unknownValue ? d1 : d2)(); +(unknownValue ? function () {} : function () {this.x = 1;})(); // no side-effects -var e = true ? 1 : foo(); -var e1 = (true ? function () {} : function () {this.x = 1;})(); -var f = false ? foo() : 2; -var f1 = (false ? function () {this.x = 1;} : function () {})(); +var e1 = true ? 1 : foo(); +var e2 = (true ? function () {} : function () {this.x = 1;})(); +var e3 = (true ? () => () => {} : () => () => console.log( 'effect' ))()(); +var f1 = false ? foo() : 2; +var f2 = (false ? function () {this.x = 1;} : function () {})(); +var f3 = (false ? () => () => console.log( 'effect' ) : () => () => {})()(); var g = true ? 1 : 2; // known side-effect -var h = true ? foo() : 2; -var h1 = (true ? function () {this.x = 1;} : function () {})(); -var i = false ? 1 : foo(); -var i1 = (false ? function () {} : function () {this.x = 1;})(); +var h1 = true ? foo() : 2; +var h2 = (true ? function () {this.x = 1;} : function () {})(); +var h3 = (true ? () => () => console.log( 'effect' ) : () => () => {})()(); +var i1 = false ? 1 : foo(); +var i2 = (false ? function () {} : function () {this.x = 1;})(); +var i3 = (false ? () => () => {} : () => () => console.log( 'effect' ))()(); From d53dcb15c604d1bdb80d9907ef7c94c7acfcd0c7 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Sat, 14 Oct 2017 22:37:33 +0200 Subject: [PATCH 47/76] Rework logical expressions to provide partial evaluation where possible and understand all relevant effects. --- src/ast/nodes/LogicalExpression.js | 56 +++++++++++------- .../_config.js | 3 + .../_expected/amd.js | 36 ++++++++++++ .../_expected/cjs.js | 34 +++++++++++ .../_expected/es.js | 32 +++++++++++ .../_expected/iife.js | 37 ++++++++++++ .../_expected/umd.js | 40 +++++++++++++ .../side-effects-logical-expressions/main.js | 57 +++++++++++++++++++ 8 files changed, 273 insertions(+), 22 deletions(-) create mode 100644 test/form/samples/side-effects-logical-expressions/_config.js create mode 100644 test/form/samples/side-effects-logical-expressions/_expected/amd.js create mode 100644 test/form/samples/side-effects-logical-expressions/_expected/cjs.js create mode 100644 test/form/samples/side-effects-logical-expressions/_expected/es.js create mode 100644 test/form/samples/side-effects-logical-expressions/_expected/iife.js create mode 100644 test/form/samples/side-effects-logical-expressions/_expected/umd.js create mode 100644 test/form/samples/side-effects-logical-expressions/main.js diff --git a/src/ast/nodes/LogicalExpression.js b/src/ast/nodes/LogicalExpression.js index dcd7e2e33f8..4a34bff84f9 100644 --- a/src/ast/nodes/LogicalExpression.js +++ b/src/ast/nodes/LogicalExpression.js @@ -1,43 +1,55 @@ import Node from '../Node.js'; import { UNKNOWN_VALUE } from '../values.js'; -const operators = { - '&&': ( left, right ) => left && right, - '||': ( left, right ) => left || right -}; - export default class LogicalExpression extends Node { getValue () { const leftValue = this.left.getValue(); if ( leftValue === UNKNOWN_VALUE ) return UNKNOWN_VALUE; + if ( (leftValue && this.operator === '||') || (!leftValue && this.operator === '&&') ) { + return leftValue; + } + return this.right.getValue(); + } - const rightValue = this.right.getValue(); - if ( rightValue === UNKNOWN_VALUE ) return UNKNOWN_VALUE; - - return operators[ this.operator ]( leftValue, rightValue ); + hasEffects ( options ) { + const leftValue = this.left.getValue(); + return this.left.hasEffects( options ) + || ( + ( leftValue === UNKNOWN_VALUE + || (!leftValue && this.operator === '||') + || (leftValue && this.operator === '&&') ) + && this.right.hasEffects( options ) + ); } hasEffectsWhenAccessedAtPath ( path, options ) { - const leftValue = this.left.getValue(); - if ( leftValue === UNKNOWN_VALUE ) { - return this.left.hasEffectsWhenAccessedAtPath( path, options ) - || this.right.hasEffectsWhenAccessedAtPath( path, options ); - } - if ( (leftValue && this.operator === '||') || (!leftValue && this.operator === '&&') ) { - return this.left.hasEffectsWhenAccessedAtPath( path, options ); - } - return this.right.hasEffectsWhenAccessedAtPath( path, options ); + return this._someRelevantBranch( node => + node.hasEffectsWhenAccessedAtPath( path, options ) ); } hasEffectsWhenAssignedAtPath ( path, options ) { + return this._someRelevantBranch( node => + node.hasEffectsWhenAssignedAtPath( path, options ) ); + } + + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { + return this._someRelevantBranch( node => + node.hasEffectsWhenCalledAtPath( path, callOptions, options ) ); + } + + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { + return this._someRelevantBranch( node => + node.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ) ); + } + + _someRelevantBranch ( predicateFunction ) { const leftValue = this.left.getValue(); if ( leftValue === UNKNOWN_VALUE ) { - return this.left.hasEffectsWhenAssignedAtPath( path, options ) - || this.right.hasEffectsWhenAssignedAtPath( path, options ); + return predicateFunction( this.left ) || predicateFunction( this.right ); } if ( (leftValue && this.operator === '||') || (!leftValue && this.operator === '&&') ) { - return this.left.hasEffectsWhenAssignedAtPath( path, options ); + return predicateFunction( this.left ); } - return this.right.hasEffectsWhenAssignedAtPath( path, options ); + return predicateFunction( this.right ); } } diff --git a/test/form/samples/side-effects-logical-expressions/_config.js b/test/form/samples/side-effects-logical-expressions/_config.js new file mode 100644 index 00000000000..c836fb6ccb7 --- /dev/null +++ b/test/form/samples/side-effects-logical-expressions/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'only evaluate the second part of a logical expression if necessary' +}; diff --git a/test/form/samples/side-effects-logical-expressions/_expected/amd.js b/test/form/samples/side-effects-logical-expressions/_expected/amd.js new file mode 100644 index 00000000000..604234b58ff --- /dev/null +++ b/test/form/samples/side-effects-logical-expressions/_expected/amd.js @@ -0,0 +1,36 @@ +define(function () { 'use strict'; + + // effect + false || console.log( 'effect' ); + true && console.log( 'effect' ); + console.log( 'effect' ) || {}; + console.log( 'effect' ) && {}; + + const foo = { + get effect () { + console.log( 'effect' ); + }, + get noEffect () {} + }; + + // effect + (false || foo).effect; + (true && foo).effect; + + // effect + (false || null).foo = 1; + (true && null).foo = 1; + + // effect + (true || (() => {}))(); + (false && (() => {}))(); + (false || (() => console.log( 'effect' )))(); + (true && (() => console.log( 'effect' )))(); + + // effect + (true || (() => () => {}))()(); + (false && (() => () => {}))()(); + (false || (() => () => console.log( 'effect' )))()(); + (true && (() => () => console.log( 'effect' )))()(); + +}); diff --git a/test/form/samples/side-effects-logical-expressions/_expected/cjs.js b/test/form/samples/side-effects-logical-expressions/_expected/cjs.js new file mode 100644 index 00000000000..67ed94614b8 --- /dev/null +++ b/test/form/samples/side-effects-logical-expressions/_expected/cjs.js @@ -0,0 +1,34 @@ +'use strict'; + +// effect +false || console.log( 'effect' ); +true && console.log( 'effect' ); +console.log( 'effect' ) || {}; +console.log( 'effect' ) && {}; + +const foo = { + get effect () { + console.log( 'effect' ); + }, + get noEffect () {} +}; + +// effect +(false || foo).effect; +(true && foo).effect; + +// effect +(false || null).foo = 1; +(true && null).foo = 1; + +// effect +(true || (() => {}))(); +(false && (() => {}))(); +(false || (() => console.log( 'effect' )))(); +(true && (() => console.log( 'effect' )))(); + +// effect +(true || (() => () => {}))()(); +(false && (() => () => {}))()(); +(false || (() => () => console.log( 'effect' )))()(); +(true && (() => () => console.log( 'effect' )))()(); diff --git a/test/form/samples/side-effects-logical-expressions/_expected/es.js b/test/form/samples/side-effects-logical-expressions/_expected/es.js new file mode 100644 index 00000000000..c23f979e80c --- /dev/null +++ b/test/form/samples/side-effects-logical-expressions/_expected/es.js @@ -0,0 +1,32 @@ +// effect +false || console.log( 'effect' ); +true && console.log( 'effect' ); +console.log( 'effect' ) || {}; +console.log( 'effect' ) && {}; + +const foo = { + get effect () { + console.log( 'effect' ); + }, + get noEffect () {} +}; + +// effect +(false || foo).effect; +(true && foo).effect; + +// effect +(false || null).foo = 1; +(true && null).foo = 1; + +// effect +(true || (() => {}))(); +(false && (() => {}))(); +(false || (() => console.log( 'effect' )))(); +(true && (() => console.log( 'effect' )))(); + +// effect +(true || (() => () => {}))()(); +(false && (() => () => {}))()(); +(false || (() => () => console.log( 'effect' )))()(); +(true && (() => () => console.log( 'effect' )))()(); diff --git a/test/form/samples/side-effects-logical-expressions/_expected/iife.js b/test/form/samples/side-effects-logical-expressions/_expected/iife.js new file mode 100644 index 00000000000..8803442d309 --- /dev/null +++ b/test/form/samples/side-effects-logical-expressions/_expected/iife.js @@ -0,0 +1,37 @@ +(function () { + 'use strict'; + + // effect + false || console.log( 'effect' ); + true && console.log( 'effect' ); + console.log( 'effect' ) || {}; + console.log( 'effect' ) && {}; + + const foo = { + get effect () { + console.log( 'effect' ); + }, + get noEffect () {} + }; + + // effect + (false || foo).effect; + (true && foo).effect; + + // effect + (false || null).foo = 1; + (true && null).foo = 1; + + // effect + (true || (() => {}))(); + (false && (() => {}))(); + (false || (() => console.log( 'effect' )))(); + (true && (() => console.log( 'effect' )))(); + + // effect + (true || (() => () => {}))()(); + (false && (() => () => {}))()(); + (false || (() => () => console.log( 'effect' )))()(); + (true && (() => () => console.log( 'effect' )))()(); + +}()); diff --git a/test/form/samples/side-effects-logical-expressions/_expected/umd.js b/test/form/samples/side-effects-logical-expressions/_expected/umd.js new file mode 100644 index 00000000000..3f8f7a8de7b --- /dev/null +++ b/test/form/samples/side-effects-logical-expressions/_expected/umd.js @@ -0,0 +1,40 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + // effect + false || console.log( 'effect' ); + true && console.log( 'effect' ); + console.log( 'effect' ) || {}; + console.log( 'effect' ) && {}; + + const foo = { + get effect () { + console.log( 'effect' ); + }, + get noEffect () {} + }; + + // effect + (false || foo).effect; + (true && foo).effect; + + // effect + (false || null).foo = 1; + (true && null).foo = 1; + + // effect + (true || (() => {}))(); + (false && (() => {}))(); + (false || (() => console.log( 'effect' )))(); + (true && (() => console.log( 'effect' )))(); + + // effect + (true || (() => () => {}))()(); + (false && (() => () => {}))()(); + (false || (() => () => console.log( 'effect' )))()(); + (true && (() => () => console.log( 'effect' )))()(); + +}))); diff --git a/test/form/samples/side-effects-logical-expressions/main.js b/test/form/samples/side-effects-logical-expressions/main.js new file mode 100644 index 00000000000..3b229271da1 --- /dev/null +++ b/test/form/samples/side-effects-logical-expressions/main.js @@ -0,0 +1,57 @@ +false || {}; +true && {}; +true || console.log( 'effect' ); +false && console.log( 'effect' ); + +// effect +false || console.log( 'effect' ); +true && console.log( 'effect' ); +console.log( 'effect' ) || {}; +console.log( 'effect' ) && {}; + +const foo = { + get effect () { + console.log( 'effect' ); + }, + get noEffect () {} +}; + +// accessed - no effect +(false || foo).noEffect; +(true && foo).noEffect; +(true || foo).effect; +(false && foo).effect; + +// effect +(false || foo).effect; +(true && foo).effect; + +// assigned - no effect +(false || {}).foo = 1; +(true && {}).foo = 1; +(true || null).foo = 1; +(false && null).foo = 1; + +// effect +(false || null).foo = 1; +(true && null).foo = 1; + +// called - no effect +(false || (() => {}))(); +(true && (() => {}))(); + +// effect +(true || (() => {}))(); +(false && (() => {}))(); +(false || (() => console.log( 'effect' )))(); +(true && (() => console.log( 'effect' )))(); + +// call return value - no effect +(false || (() => () => {}))()(); +(true && (() => () => {}))()(); + +// effect +(true || (() => () => {}))()(); +(false && (() => () => {}))()(); +(false || (() => () => console.log( 'effect' )))()(); +(true && (() => () => console.log( 'effect' )))()(); From 58b735a817e09a8aff0bf48587b11d79efb04f0c Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Sun, 15 Oct 2017 09:46:13 +0200 Subject: [PATCH 48/76] Implement basic call parameter handling for arrow functions. --- src/ast/CallOptions.js | 7 +++-- src/ast/ExecutionPathOptions.js | 14 ++++----- src/ast/nodes/ArrowFunctionExpression.js | 9 +++--- src/ast/nodes/CallExpression.js | 2 +- src/ast/nodes/shared/FunctionNode.js | 2 +- src/ast/scopes/FunctionScope.js | 5 +-- src/ast/scopes/ParameterScope.js | 31 +++++++++++++++++++ src/ast/scopes/ReturnValueScope.js | 4 +-- src/ast/scopes/Scope.js | 2 +- src/ast/variables/ParameterVariable.js | 25 +++++++++++++-- src/ast/variables/ThisVariable.js | 2 +- test/form/samples/call-parameters/_config.js | 3 ++ .../samples/call-parameters/_expected/amd.js | 6 ++++ .../samples/call-parameters/_expected/cjs.js | 4 +++ .../samples/call-parameters/_expected/es.js | 2 ++ .../samples/call-parameters/_expected/iife.js | 7 +++++ .../samples/call-parameters/_expected/umd.js | 10 ++++++ test/form/samples/call-parameters/main.js | 5 +++ 18 files changed, 117 insertions(+), 23 deletions(-) create mode 100644 src/ast/scopes/ParameterScope.js create mode 100644 test/form/samples/call-parameters/_config.js create mode 100644 test/form/samples/call-parameters/_expected/amd.js create mode 100644 test/form/samples/call-parameters/_expected/cjs.js create mode 100644 test/form/samples/call-parameters/_expected/es.js create mode 100644 test/form/samples/call-parameters/_expected/iife.js create mode 100644 test/form/samples/call-parameters/_expected/umd.js create mode 100644 test/form/samples/call-parameters/main.js diff --git a/src/ast/CallOptions.js b/src/ast/CallOptions.js index a3119102daf..1ec94660513 100644 --- a/src/ast/CallOptions.js +++ b/src/ast/CallOptions.js @@ -7,8 +7,9 @@ export default class CallOptions { return new this( callOptions, Immutable.Map() ); } - constructor ( { withNew = false } = {}, nodesCalledAtPath ) { + constructor ( { withNew = false, parameters = [] } = {}, nodesCalledAtPath ) { this.withNew = withNew; + this.parameters = parameters; this._nodesCalledAtPath = nodesCalledAtPath; } @@ -17,7 +18,9 @@ export default class CallOptions { } equals ( callOptions ) { - return this.withNew === callOptions.withNew; + return this.withNew === callOptions.withNew + && this.parameters.length === callOptions.parameters.length + && this.parameters.every( ( parameter, index ) => parameter === callOptions.parameters[ index ] ); } hasNodeBeenCalledAtPath ( path, node ) { diff --git a/src/ast/ExecutionPathOptions.js b/src/ast/ExecutionPathOptions.js index 3a2c911af55..ff5d69be955 100644 --- a/src/ast/ExecutionPathOptions.js +++ b/src/ast/ExecutionPathOptions.js @@ -8,7 +8,7 @@ const OPTION_NODES_CALLED_AT_PATH_WITH_OPTIONS = 'NODES_CALLED_AT_PATH_WITH_OPTI const OPTION_RETURN_EXPRESSIONS_ACCESSED_AT_PATH = 'RETURN_EXPRESSIONS_ACCESSED_AT_PATH'; const OPTION_RETURN_EXPRESSIONS_ASSIGNED_AT_PATH = 'RETURN_EXPRESSIONS_ASSIGNED_AT_PATH'; const OPTION_RETURN_EXPRESSIONS_CALLED_AT_PATH = 'RETURN_EXPRESSIONS_CALLED_AT_PATH'; -const OPTION_VALID_THIS_VARIABLES = 'VALID_THIS_VARIABLES'; +const OPTION_REPLACED_VARIABLE_INITS = 'REPLACED_VARIABLE_INITS'; const IGNORED_LABELS = 'IGNORED_LABELS'; const RESULT_KEY = {}; @@ -115,11 +115,11 @@ export default class ExecutionPathOptions { } /** - * @param {ThisVariable} thisVariable + * @param {ThisVariable|ParameterVariable} variable * @returns {Node} */ - getReplacedThisInit ( thisVariable ) { - return this._optionValues.getIn( [ OPTION_VALID_THIS_VARIABLES, thisVariable ] ); + getReplacedVariableInit ( variable ) { + return this._optionValues.getIn( [ OPTION_REPLACED_VARIABLE_INITS, variable ] ); } /** @@ -201,12 +201,12 @@ export default class ExecutionPathOptions { } /** - * @param {ThisVariable} thisVariable + * @param {ThisVariable|ParameterVariable} variable * @param {Node} init * @return {ExecutionPathOptions} */ - replaceThisInit ( thisVariable, init ) { - return this.setIn( [ OPTION_VALID_THIS_VARIABLES, thisVariable ], init ); + replaceVariableInit ( variable, init ) { + return this.setIn( [ OPTION_REPLACED_VARIABLE_INITS, variable ], init ); } /** diff --git a/src/ast/nodes/ArrowFunctionExpression.js b/src/ast/nodes/ArrowFunctionExpression.js index bd12a26a57d..97e9c57387a 100644 --- a/src/ast/nodes/ArrowFunctionExpression.js +++ b/src/ast/nodes/ArrowFunctionExpression.js @@ -25,8 +25,9 @@ export default class ArrowFunctionExpression extends Node { if ( path.length > 0 ) { return true; } - return this.params.some( param => param.hasEffects( options ) ) - || this.body.hasEffects( options ); + const innerOptions = this.scope.getOptionsWithReplacedParameters( callOptions.parameters, options ); + return this.params.some( param => param.hasEffects( innerOptions ) ) + || this.body.hasEffects( innerOptions ); } initialiseChildren () { @@ -42,7 +43,7 @@ export default class ArrowFunctionExpression extends Node { this.scope = new ReturnValueScope( { parent: parentScope } ); } - someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { - return this.scope.someReturnExpressionWhenCalled( callOptions, predicateFunction, options ); + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { + return this.scope.someReturnExpressionWhenCalled( callOptions, predicateFunction ); } } diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index c96f2a25556..de06325cb1e 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -48,7 +48,7 @@ export default class CallExpression extends Node { } initialiseNode () { - this._callOptions = CallOptions.create( { withNew: false } ); + this._callOptions = CallOptions.create( { withNew: false, parameters: this.arguments } ); } someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index 58395ee8672..619623cf0bc 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -37,7 +37,7 @@ export default class FunctionNode extends Node { if ( path.length > 0 ) { return true; } - const innerOptions = options.replaceThisInit( this.thisVariable, + const innerOptions = options.replaceVariableInit( this.thisVariable, callOptions.withNew ? new VirtualObjectExpression() : UNKNOWN_ASSIGNMENT ); return this.params.some( param => param.hasEffects( innerOptions ) ) || this.body.hasEffects( innerOptions ); diff --git a/src/ast/scopes/FunctionScope.js b/src/ast/scopes/FunctionScope.js index b26678adb8e..0e88ce89e0e 100644 --- a/src/ast/scopes/FunctionScope.js +++ b/src/ast/scopes/FunctionScope.js @@ -1,11 +1,12 @@ import ReturnValueScope from './ReturnValueScope'; -import ParameterVariable from '../variables/ParameterVariable'; import ThisVariable from '../variables/ThisVariable'; +import LocalVariable from '../variables/LocalVariable'; +import { UNKNOWN_ASSIGNMENT } from '../values'; export default class FunctionScope extends ReturnValueScope { constructor ( options = {} ) { super( options ); - this.variables.arguments = new ParameterVariable( 'arguments' ); + this.variables.arguments = new LocalVariable( 'arguments', null, UNKNOWN_ASSIGNMENT ); this.variables.this = new ThisVariable(); } diff --git a/src/ast/scopes/ParameterScope.js b/src/ast/scopes/ParameterScope.js new file mode 100644 index 00000000000..31b4714678e --- /dev/null +++ b/src/ast/scopes/ParameterScope.js @@ -0,0 +1,31 @@ +import Scope from './Scope'; +import ParameterVariable from '../variables/ParameterVariable'; +import { UNKNOWN_ASSIGNMENT } from '../values'; + +export default class ParameterScope extends Scope { + constructor ( options = {} ) { + super( options ); + this._parameters = []; + } + + /** + * Adds a parameter to this scope. Parameters must be added in the correct + * order, e.g. from left to right. + * @param {Identifier} identifier + * @returns {Variable} + */ + addParameterDeclaration ( identifier ) { + const variable = new ParameterVariable( identifier ); + this.variables[ identifier.name ] = variable; + this._parameters.push( variable ); + return variable; + } + + getOptionsWithReplacedParameters ( parameterReplacements, options ) { + let newOptions = options; + this._parameters.forEach( ( parameter, index ) => + newOptions = newOptions.replaceVariableInit( parameter, parameterReplacements[ index ] || UNKNOWN_ASSIGNMENT ) + ); + return newOptions; + } +} diff --git a/src/ast/scopes/ReturnValueScope.js b/src/ast/scopes/ReturnValueScope.js index 1821bf4b1a6..b18fa8aab1f 100644 --- a/src/ast/scopes/ReturnValueScope.js +++ b/src/ast/scopes/ReturnValueScope.js @@ -1,6 +1,6 @@ -import Scope from './Scope'; +import ParameterScope from './ParameterScope'; -export default class ReturnValueScope extends Scope { +export default class ReturnValueScope extends ParameterScope { constructor ( options = {} ) { super( options ); this._returnExpressions = new Set(); diff --git a/src/ast/scopes/Scope.js b/src/ast/scopes/Scope.js index 7e958734e3a..ce5f62644c3 100644 --- a/src/ast/scopes/Scope.js +++ b/src/ast/scopes/Scope.js @@ -41,7 +41,7 @@ export default class Scope { addParameterDeclaration ( identifier ) { const name = identifier.name; - this.variables[ name ] = new ParameterVariable( name, identifier ); + this.variables[ name ] = new ParameterVariable( identifier ); return this.variables[ name ]; } diff --git a/src/ast/variables/ParameterVariable.js b/src/ast/variables/ParameterVariable.js index a0064250371..15fd7b43f7f 100644 --- a/src/ast/variables/ParameterVariable.js +++ b/src/ast/variables/ParameterVariable.js @@ -1,12 +1,33 @@ import LocalVariable from './LocalVariable'; +import ReplaceableInitStructuredAssignmentTracker from './ReplaceableInitStructuredAssignmentTracker'; import { UNKNOWN_ASSIGNMENT } from '../values'; export default class ParameterVariable extends LocalVariable { - constructor ( name, declarator ) { - super( name, declarator, UNKNOWN_ASSIGNMENT ); + constructor ( identifier ) { + super( identifier.name, identifier, null ); + this.assignedExpressions = new ReplaceableInitStructuredAssignmentTracker( UNKNOWN_ASSIGNMENT ); } getName () { return this.name; } + + hasEffectsWhenAccessedAtPath ( path, options ) { + this._updateInit( options ); + return super.hasEffectsWhenAccessedAtPath( path, options ); + } + + hasEffectsWhenAssignedAtPath ( path, options ) { + this._updateInit( options ); + return super.hasEffectsWhenAssignedAtPath( path, options ); + } + + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { + this._updateInit( options ); + return super.hasEffectsWhenCalledAtPath( path, callOptions, options ); + } + + _updateInit ( options ) { + this.assignedExpressions.setInit( options.getReplacedVariableInit( this ) || UNKNOWN_ASSIGNMENT ); + } } diff --git a/src/ast/variables/ThisVariable.js b/src/ast/variables/ThisVariable.js index 890b1df1072..1db2c7bcc8d 100644 --- a/src/ast/variables/ThisVariable.js +++ b/src/ast/variables/ThisVariable.js @@ -24,6 +24,6 @@ export default class ThisVariable extends LocalVariable { } _updateInit ( options ) { - this.assignedExpressions.setInit( options.getReplacedThisInit( this ) || UNKNOWN_ASSIGNMENT ); + this.assignedExpressions.setInit( options.getReplacedVariableInit( this ) || UNKNOWN_ASSIGNMENT ); } } diff --git a/test/form/samples/call-parameters/_config.js b/test/form/samples/call-parameters/_config.js new file mode 100644 index 00000000000..259e50e7c66 --- /dev/null +++ b/test/form/samples/call-parameters/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'take actual parameters into account when determining side-effects of calls' +}; diff --git a/test/form/samples/call-parameters/_expected/amd.js b/test/form/samples/call-parameters/_expected/amd.js new file mode 100644 index 00000000000..3000adb64e3 --- /dev/null +++ b/test/form/samples/call-parameters/_expected/amd.js @@ -0,0 +1,6 @@ +define(function () { 'use strict'; + + const callArg2 = arg => arg(); + callArg2( () => console.log( 'effect' ) ); + +}); diff --git a/test/form/samples/call-parameters/_expected/cjs.js b/test/form/samples/call-parameters/_expected/cjs.js new file mode 100644 index 00000000000..3d19a896326 --- /dev/null +++ b/test/form/samples/call-parameters/_expected/cjs.js @@ -0,0 +1,4 @@ +'use strict'; + +const callArg2 = arg => arg(); +callArg2( () => console.log( 'effect' ) ); diff --git a/test/form/samples/call-parameters/_expected/es.js b/test/form/samples/call-parameters/_expected/es.js new file mode 100644 index 00000000000..5d3045a485f --- /dev/null +++ b/test/form/samples/call-parameters/_expected/es.js @@ -0,0 +1,2 @@ +const callArg2 = arg => arg(); +callArg2( () => console.log( 'effect' ) ); diff --git a/test/form/samples/call-parameters/_expected/iife.js b/test/form/samples/call-parameters/_expected/iife.js new file mode 100644 index 00000000000..c265638747a --- /dev/null +++ b/test/form/samples/call-parameters/_expected/iife.js @@ -0,0 +1,7 @@ +(function () { + 'use strict'; + + const callArg2 = arg => arg(); + callArg2( () => console.log( 'effect' ) ); + +}()); diff --git a/test/form/samples/call-parameters/_expected/umd.js b/test/form/samples/call-parameters/_expected/umd.js new file mode 100644 index 00000000000..a3e1bb897e4 --- /dev/null +++ b/test/form/samples/call-parameters/_expected/umd.js @@ -0,0 +1,10 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + const callArg2 = arg => arg(); + callArg2( () => console.log( 'effect' ) ); + +}))); diff --git a/test/form/samples/call-parameters/main.js b/test/form/samples/call-parameters/main.js new file mode 100644 index 00000000000..b800962edb3 --- /dev/null +++ b/test/form/samples/call-parameters/main.js @@ -0,0 +1,5 @@ +const callArg1 = arg => arg(); +callArg1( () => {} ); + +const callArg2 = arg => arg(); +callArg2( () => console.log( 'effect' ) ); From d1f62df1e11375778fa8e285542ec1f72a8f5ca4 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Sun, 15 Oct 2017 10:10:35 +0200 Subject: [PATCH 49/76] Let the predicateFunction in someReturnExpressionWhenCalledAtPath reuse the outer options to be able to pass in parameter values. --- src/ast/Node.js | 2 +- src/ast/nodes/ArrowFunctionExpression.js | 5 +++-- src/ast/nodes/CallExpression.js | 16 ++++++++-------- src/ast/nodes/Identifier.js | 2 +- src/ast/nodes/Property.js | 12 ++++++------ src/ast/nodes/shared/FunctionNode.js | 5 +++-- src/ast/scopes/ReturnValueScope.js | 5 +++-- src/ast/variables/Variable.js | 2 +- .../samples/call-parameters/_expected/amd.js | 3 +++ .../samples/call-parameters/_expected/cjs.js | 3 +++ .../form/samples/call-parameters/_expected/es.js | 3 +++ .../samples/call-parameters/_expected/iife.js | 3 +++ .../samples/call-parameters/_expected/umd.js | 3 +++ test/form/samples/call-parameters/main.js | 6 ++++++ 14 files changed, 47 insertions(+), 23 deletions(-) diff --git a/src/ast/Node.js b/src/ast/Node.js index 679cdeeaea3..248eddef0e4 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -236,7 +236,7 @@ export default class Node { * @returns {boolean} */ someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { - return predicateFunction( UNKNOWN_ASSIGNMENT ); + return predicateFunction( options )( UNKNOWN_ASSIGNMENT ); } toString () { diff --git a/src/ast/nodes/ArrowFunctionExpression.js b/src/ast/nodes/ArrowFunctionExpression.js index 97e9c57387a..4d09bb43623 100644 --- a/src/ast/nodes/ArrowFunctionExpression.js +++ b/src/ast/nodes/ArrowFunctionExpression.js @@ -43,7 +43,8 @@ export default class ArrowFunctionExpression extends Node { this.scope = new ReturnValueScope( { parent: parentScope } ); } - someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { - return this.scope.someReturnExpressionWhenCalled( callOptions, predicateFunction ); + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { + return path.length > 0 + || this.scope.someReturnExpressionWhenCalled( callOptions, predicateFunction, options ); } } diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index de06325cb1e..dc8564c072d 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -31,20 +31,20 @@ export default class CallExpression extends Node { hasEffectsWhenAccessedAtPath ( path, options ) { return !options.hasReturnExpressionBeenAccessedAtPath( path, this ) - && this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, node => - node.hasEffectsWhenAccessedAtPath( path, options.addAccessedReturnExpressionAtPath( path, this ) ), options ); + && this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, innerOptions => node => + node.hasEffectsWhenAccessedAtPath( path, innerOptions.addAccessedReturnExpressionAtPath( path, this ) ), options ); } hasEffectsWhenAssignedAtPath ( path, options ) { return !options.hasReturnExpressionBeenAssignedAtPath( path, this ) - && this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, node => - node.hasEffectsWhenAssignedAtPath( path, options.addAssignedReturnExpressionAtPath( path, this ) ), options ); + && this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, innerOptions => node => + node.hasEffectsWhenAssignedAtPath( path, innerOptions.addAssignedReturnExpressionAtPath( path, this ) ), options ); } hasEffectsWhenCalledAtPath ( path, callOptions, options ) { return !options.hasReturnExpressionBeenCalledAtPath( path, this ) - && this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, node => - node.hasEffectsWhenCalledAtPath( path, callOptions, options.addCalledReturnExpressionAtPath( path, this ) ), options ); + && this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, innerOptions => node => + node.hasEffectsWhenCalledAtPath( path, callOptions, innerOptions.addCalledReturnExpressionAtPath( path, this ) ), options ); } initialiseNode () { @@ -52,7 +52,7 @@ export default class CallExpression extends Node { } someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { - return this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, node => - node.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ), options ); + return this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, innerOptions => node => + node.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, innerOptions ), options ); } } diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index bc094d9bca1..fb8d13fa49f 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -81,6 +81,6 @@ export default class Identifier extends Node { if ( this.variable ) { return this.variable.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ); } - return predicateFunction( UNKNOWN_ASSIGNMENT ); + return predicateFunction( options )( UNKNOWN_ASSIGNMENT ); } } diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index 26cd47df355..f059a383f8b 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -17,8 +17,8 @@ export default class Property extends Node { if ( this.kind === 'get' ) { return this.value.hasEffectsWhenCalledAtPath( [], this._getterCallOptions, options.getHasEffectsWhenCalledOptions() ) || (!options.hasReturnExpressionBeenAccessedAtPath( path, this ) - && this.value.someReturnExpressionWhenCalledAtPath( [], this._getterCallOptions, node => - node.hasEffectsWhenAccessedAtPath( path, options.addAccessedReturnExpressionAtPath( path, this ) ), options )); + && this.value.someReturnExpressionWhenCalledAtPath( [], this._getterCallOptions, innerOptions => node => + node.hasEffectsWhenAccessedAtPath( path, innerOptions.addAccessedReturnExpressionAtPath( path, this ) ), options )); } return this.value.hasEffectsWhenAccessedAtPath( path, options ); } @@ -35,8 +35,8 @@ export default class Property extends Node { if ( this.kind === 'get' ) { return this.value.hasEffectsWhenCalledAtPath( [], this._getterCallOptions, options.getHasEffectsWhenCalledOptions() ) || (!options.hasReturnExpressionBeenCalledAtPath( path, this ) - && this.value.someReturnExpressionWhenCalledAtPath( [], this._getterCallOptions, node => - node.hasEffectsWhenCalledAtPath( path, callOptions, options.addCalledReturnExpressionAtPath( path, this ) ), options )); + && this.value.someReturnExpressionWhenCalledAtPath( [], this._getterCallOptions, innerOptions => node => + node.hasEffectsWhenCalledAtPath( path, callOptions, innerOptions.addCalledReturnExpressionAtPath( path, this ) ), options )); } return this.value.hasEffectsWhenCalledAtPath( path, callOptions, options ); } @@ -62,8 +62,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, node => - node.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ), options ); + || this.value.someReturnExpressionWhenCalledAtPath( [], this._getterCallOptions, innerOptions => node => + node.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, innerOptions ), options ); } return this.value.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ); } diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index 619623cf0bc..6af21af08c6 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -51,7 +51,8 @@ export default class FunctionNode extends Node { this.scope = new FunctionScope( { parent: parentScope } ); } - someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction ) { - return this.scope.someReturnExpressionWhenCalled( callOptions, predicateFunction ); + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { + return path.length > 0 + || this.scope.someReturnExpressionWhenCalled( callOptions, predicateFunction, options ); } } diff --git a/src/ast/scopes/ReturnValueScope.js b/src/ast/scopes/ReturnValueScope.js index b18fa8aab1f..f32084706ef 100644 --- a/src/ast/scopes/ReturnValueScope.js +++ b/src/ast/scopes/ReturnValueScope.js @@ -10,7 +10,8 @@ export default class ReturnValueScope extends ParameterScope { this._returnExpressions.add( expression ); } - someReturnExpressionWhenCalled ( callOptions, predicateFunction ) { - return Array.from( this._returnExpressions ).some( predicateFunction ); + someReturnExpressionWhenCalled ( callOptions, predicateFunction, options ) { + const innerOptions = this.getOptionsWithReplacedParameters( callOptions.parameters, options ); + return Array.from( this._returnExpressions ).some( predicateFunction( innerOptions ) ); } } diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index 982ab51f9a4..b4a67fde3b6 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -80,6 +80,6 @@ export default class Variable { * @returns {boolean} */ someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { - return predicateFunction( UNKNOWN_ASSIGNMENT ); + return predicateFunction( options )( UNKNOWN_ASSIGNMENT ); } } diff --git a/test/form/samples/call-parameters/_expected/amd.js b/test/form/samples/call-parameters/_expected/amd.js index 3000adb64e3..067db41748f 100644 --- a/test/form/samples/call-parameters/_expected/amd.js +++ b/test/form/samples/call-parameters/_expected/amd.js @@ -3,4 +3,7 @@ define(function () { 'use strict'; const callArg2 = arg => arg(); callArg2( () => console.log( 'effect' ) ); + const returnArg2 = arg => arg; + returnArg2( () => console.log( 'effect' ) )(); + }); diff --git a/test/form/samples/call-parameters/_expected/cjs.js b/test/form/samples/call-parameters/_expected/cjs.js index 3d19a896326..20c66b2560f 100644 --- a/test/form/samples/call-parameters/_expected/cjs.js +++ b/test/form/samples/call-parameters/_expected/cjs.js @@ -2,3 +2,6 @@ const callArg2 = arg => arg(); callArg2( () => console.log( 'effect' ) ); + +const returnArg2 = arg => arg; +returnArg2( () => console.log( 'effect' ) )(); diff --git a/test/form/samples/call-parameters/_expected/es.js b/test/form/samples/call-parameters/_expected/es.js index 5d3045a485f..216ac013d11 100644 --- a/test/form/samples/call-parameters/_expected/es.js +++ b/test/form/samples/call-parameters/_expected/es.js @@ -1,2 +1,5 @@ const callArg2 = arg => arg(); callArg2( () => console.log( 'effect' ) ); + +const returnArg2 = arg => arg; +returnArg2( () => console.log( 'effect' ) )(); diff --git a/test/form/samples/call-parameters/_expected/iife.js b/test/form/samples/call-parameters/_expected/iife.js index c265638747a..078c1e2aaf6 100644 --- a/test/form/samples/call-parameters/_expected/iife.js +++ b/test/form/samples/call-parameters/_expected/iife.js @@ -4,4 +4,7 @@ const callArg2 = arg => arg(); callArg2( () => console.log( 'effect' ) ); + const returnArg2 = arg => arg; + returnArg2( () => console.log( 'effect' ) )(); + }()); diff --git a/test/form/samples/call-parameters/_expected/umd.js b/test/form/samples/call-parameters/_expected/umd.js index a3e1bb897e4..fa96dd07988 100644 --- a/test/form/samples/call-parameters/_expected/umd.js +++ b/test/form/samples/call-parameters/_expected/umd.js @@ -7,4 +7,7 @@ const callArg2 = arg => arg(); callArg2( () => console.log( 'effect' ) ); + const returnArg2 = arg => arg; + returnArg2( () => console.log( 'effect' ) )(); + }))); diff --git a/test/form/samples/call-parameters/main.js b/test/form/samples/call-parameters/main.js index b800962edb3..79544fe32ca 100644 --- a/test/form/samples/call-parameters/main.js +++ b/test/form/samples/call-parameters/main.js @@ -3,3 +3,9 @@ callArg1( () => {} ); const callArg2 = arg => arg(); callArg2( () => console.log( 'effect' ) ); + +const returnArg1 = arg => arg; +returnArg1( () => {} )(); + +const returnArg2 = arg => arg; +returnArg2( () => console.log( 'effect' ) )(); From c65545962868ae22247018be3c9ea5acb0cda6c2 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Sun, 15 Oct 2017 11:02:14 +0200 Subject: [PATCH 50/76] Make sure parameters are used correctly in call return values. --- src/ast/variables/ParameterVariable.js | 5 ++ .../samples/call-parameters/_expected/amd.js | 24 ++++++++++ .../samples/call-parameters/_expected/cjs.js | 24 ++++++++++ .../samples/call-parameters/_expected/es.js | 24 ++++++++++ .../samples/call-parameters/_expected/iife.js | 24 ++++++++++ .../samples/call-parameters/_expected/umd.js | 24 ++++++++++ test/form/samples/call-parameters/main.js | 48 +++++++++++++++++++ 7 files changed, 173 insertions(+) diff --git a/src/ast/variables/ParameterVariable.js b/src/ast/variables/ParameterVariable.js index 15fd7b43f7f..7477677bc43 100644 --- a/src/ast/variables/ParameterVariable.js +++ b/src/ast/variables/ParameterVariable.js @@ -27,6 +27,11 @@ export default class ParameterVariable extends LocalVariable { return super.hasEffectsWhenCalledAtPath( path, callOptions, options ); } + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { + this._updateInit( options ); + return super.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ); + } + _updateInit ( options ) { this.assignedExpressions.setInit( options.getReplacedVariableInit( this ) || UNKNOWN_ASSIGNMENT ); } diff --git a/test/form/samples/call-parameters/_expected/amd.js b/test/form/samples/call-parameters/_expected/amd.js index 067db41748f..3e609d33362 100644 --- a/test/form/samples/call-parameters/_expected/amd.js +++ b/test/form/samples/call-parameters/_expected/amd.js @@ -3,7 +3,31 @@ define(function () { 'use strict'; const callArg2 = arg => arg(); callArg2( () => console.log( 'effect' ) ); + const assignArg2 = arg => arg.foo.bar = 1; + assignArg2( {} ); + const returnArg2 = arg => arg; returnArg2( () => console.log( 'effect' ) )(); + const returnArg4 = arg => arg; + returnArg4( {} ).foo.bar = 1; + + const returnArg6 = arg => arg; + returnArg6( () => () => console.log( 'effect' ) )()(); + + const returnArgReturn2 = arg => arg(); + returnArgReturn2( () => () => console.log( 'effect' ) )(); + + const returnArgReturn4 = arg => arg(); + returnArgReturn4( () => ({}) ).foo.bar = 1; + + const returnArgReturn6 = arg => arg(); + returnArgReturn6( () => () => () => console.log( 'effect' ) )()(); + + const multiArgument2 = ( func, obj ) => func( obj ); + multiArgument2( obj => obj(), () => console.log( 'effect' ) ); + + const multiArgument4 = ( func, obj ) => func( obj ); + multiArgument4( obj => obj.foo.bar = 1, {} ); + }); diff --git a/test/form/samples/call-parameters/_expected/cjs.js b/test/form/samples/call-parameters/_expected/cjs.js index 20c66b2560f..02a5835d466 100644 --- a/test/form/samples/call-parameters/_expected/cjs.js +++ b/test/form/samples/call-parameters/_expected/cjs.js @@ -3,5 +3,29 @@ const callArg2 = arg => arg(); callArg2( () => console.log( 'effect' ) ); +const assignArg2 = arg => arg.foo.bar = 1; +assignArg2( {} ); + const returnArg2 = arg => arg; returnArg2( () => console.log( 'effect' ) )(); + +const returnArg4 = arg => arg; +returnArg4( {} ).foo.bar = 1; + +const returnArg6 = arg => arg; +returnArg6( () => () => console.log( 'effect' ) )()(); + +const returnArgReturn2 = arg => arg(); +returnArgReturn2( () => () => console.log( 'effect' ) )(); + +const returnArgReturn4 = arg => arg(); +returnArgReturn4( () => ({}) ).foo.bar = 1; + +const returnArgReturn6 = arg => arg(); +returnArgReturn6( () => () => () => console.log( 'effect' ) )()(); + +const multiArgument2 = ( func, obj ) => func( obj ); +multiArgument2( obj => obj(), () => console.log( 'effect' ) ); + +const multiArgument4 = ( func, obj ) => func( obj ); +multiArgument4( obj => obj.foo.bar = 1, {} ); diff --git a/test/form/samples/call-parameters/_expected/es.js b/test/form/samples/call-parameters/_expected/es.js index 216ac013d11..3f1479c5d0d 100644 --- a/test/form/samples/call-parameters/_expected/es.js +++ b/test/form/samples/call-parameters/_expected/es.js @@ -1,5 +1,29 @@ const callArg2 = arg => arg(); callArg2( () => console.log( 'effect' ) ); +const assignArg2 = arg => arg.foo.bar = 1; +assignArg2( {} ); + const returnArg2 = arg => arg; returnArg2( () => console.log( 'effect' ) )(); + +const returnArg4 = arg => arg; +returnArg4( {} ).foo.bar = 1; + +const returnArg6 = arg => arg; +returnArg6( () => () => console.log( 'effect' ) )()(); + +const returnArgReturn2 = arg => arg(); +returnArgReturn2( () => () => console.log( 'effect' ) )(); + +const returnArgReturn4 = arg => arg(); +returnArgReturn4( () => ({}) ).foo.bar = 1; + +const returnArgReturn6 = arg => arg(); +returnArgReturn6( () => () => () => console.log( 'effect' ) )()(); + +const multiArgument2 = ( func, obj ) => func( obj ); +multiArgument2( obj => obj(), () => console.log( 'effect' ) ); + +const multiArgument4 = ( func, obj ) => func( obj ); +multiArgument4( obj => obj.foo.bar = 1, {} ); diff --git a/test/form/samples/call-parameters/_expected/iife.js b/test/form/samples/call-parameters/_expected/iife.js index 078c1e2aaf6..923053db68a 100644 --- a/test/form/samples/call-parameters/_expected/iife.js +++ b/test/form/samples/call-parameters/_expected/iife.js @@ -4,7 +4,31 @@ const callArg2 = arg => arg(); callArg2( () => console.log( 'effect' ) ); + const assignArg2 = arg => arg.foo.bar = 1; + assignArg2( {} ); + const returnArg2 = arg => arg; returnArg2( () => console.log( 'effect' ) )(); + const returnArg4 = arg => arg; + returnArg4( {} ).foo.bar = 1; + + const returnArg6 = arg => arg; + returnArg6( () => () => console.log( 'effect' ) )()(); + + const returnArgReturn2 = arg => arg(); + returnArgReturn2( () => () => console.log( 'effect' ) )(); + + const returnArgReturn4 = arg => arg(); + returnArgReturn4( () => ({}) ).foo.bar = 1; + + const returnArgReturn6 = arg => arg(); + returnArgReturn6( () => () => () => console.log( 'effect' ) )()(); + + const multiArgument2 = ( func, obj ) => func( obj ); + multiArgument2( obj => obj(), () => console.log( 'effect' ) ); + + const multiArgument4 = ( func, obj ) => func( obj ); + multiArgument4( obj => obj.foo.bar = 1, {} ); + }()); diff --git a/test/form/samples/call-parameters/_expected/umd.js b/test/form/samples/call-parameters/_expected/umd.js index fa96dd07988..5f39801aad8 100644 --- a/test/form/samples/call-parameters/_expected/umd.js +++ b/test/form/samples/call-parameters/_expected/umd.js @@ -7,7 +7,31 @@ const callArg2 = arg => arg(); callArg2( () => console.log( 'effect' ) ); + const assignArg2 = arg => arg.foo.bar = 1; + assignArg2( {} ); + const returnArg2 = arg => arg; returnArg2( () => console.log( 'effect' ) )(); + const returnArg4 = arg => arg; + returnArg4( {} ).foo.bar = 1; + + const returnArg6 = arg => arg; + returnArg6( () => () => console.log( 'effect' ) )()(); + + const returnArgReturn2 = arg => arg(); + returnArgReturn2( () => () => console.log( 'effect' ) )(); + + const returnArgReturn4 = arg => arg(); + returnArgReturn4( () => ({}) ).foo.bar = 1; + + const returnArgReturn6 = arg => arg(); + returnArgReturn6( () => () => () => console.log( 'effect' ) )()(); + + const multiArgument2 = ( func, obj ) => func( obj ); + multiArgument2( obj => obj(), () => console.log( 'effect' ) ); + + const multiArgument4 = ( func, obj ) => func( obj ); + multiArgument4( obj => obj.foo.bar = 1, {} ); + }))); diff --git a/test/form/samples/call-parameters/main.js b/test/form/samples/call-parameters/main.js index 79544fe32ca..4334ee1bcad 100644 --- a/test/form/samples/call-parameters/main.js +++ b/test/form/samples/call-parameters/main.js @@ -4,8 +4,56 @@ callArg1( () => {} ); const callArg2 = arg => arg(); callArg2( () => console.log( 'effect' ) ); +const assignArg1 = arg => arg.foo.bar = 1; +assignArg1( { foo: {} } ); + +const assignArg2 = arg => arg.foo.bar = 1; +assignArg2( {} ); + const returnArg1 = arg => arg; returnArg1( () => {} )(); const returnArg2 = arg => arg; returnArg2( () => console.log( 'effect' ) )(); + +const returnArg3 = arg => arg; +returnArg3( { foo: {} } ).foo.bar = 1; + +const returnArg4 = arg => arg; +returnArg4( {} ).foo.bar = 1; + +const returnArg5 = arg => arg; +returnArg5( () => () => {} )()(); + +const returnArg6 = arg => arg; +returnArg6( () => () => console.log( 'effect' ) )()(); + +const returnArgReturn1 = arg => arg(); +returnArgReturn1( () => () => {} )(); + +const returnArgReturn2 = arg => arg(); +returnArgReturn2( () => () => console.log( 'effect' ) )(); + +const returnArgReturn3 = arg => arg(); +returnArgReturn3( () => ({ foo: {} }) ).foo.bar = 1; + +const returnArgReturn4 = arg => arg(); +returnArgReturn4( () => ({}) ).foo.bar = 1; + +const returnArgReturn5 = arg => arg(); +returnArgReturn5( () => () => () => {} )()(); + +const returnArgReturn6 = arg => arg(); +returnArgReturn6( () => () => () => console.log( 'effect' ) )()(); + +const multiArgument1 = ( func, obj ) => func( obj ); +multiArgument1( obj => obj(), () => {} ); + +const multiArgument2 = ( func, obj ) => func( obj ); +multiArgument2( obj => obj(), () => console.log( 'effect' ) ); + +const multiArgument3 = ( func, obj ) => func( obj ); +multiArgument3( obj => obj.foo.bar = 1, { foo: {} } ); + +const multiArgument4 = ( func, obj ) => func( obj ); +multiArgument4( obj => obj.foo.bar = 1, {} ); From 4a94c53260f2751034cf8c80ecfc28ca00218a58 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Mon, 16 Oct 2017 06:42:33 +0200 Subject: [PATCH 51/76] * Use ParameterScope for catch clauses * Let parameters and "this" share their code --- src/ast/nodes/CatchClause.js | 4 +-- src/ast/scopes/CatchScope.js | 11 ++++++ src/ast/scopes/Scope.js | 6 ---- src/ast/variables/ParameterVariable.js | 33 ++--------------- src/ast/variables/ReplaceableInitVariable.js | 38 ++++++++++++++++++++ src/ast/variables/ThisVariable.js | 28 ++------------- 6 files changed, 57 insertions(+), 63 deletions(-) create mode 100644 src/ast/scopes/CatchScope.js create mode 100644 src/ast/variables/ReplaceableInitVariable.js diff --git a/src/ast/nodes/CatchClause.js b/src/ast/nodes/CatchClause.js index 9fef2f8015c..8752f2fdb8c 100644 --- a/src/ast/nodes/CatchClause.js +++ b/src/ast/nodes/CatchClause.js @@ -1,5 +1,5 @@ import Node from '../Node.js'; -import BlockScope from '../scopes/BlockScope'; +import CatchScope from '../scopes/CatchScope'; export default class CatchClause extends Node { initialiseChildren () { @@ -8,6 +8,6 @@ export default class CatchClause extends Node { } initialiseScope ( parentScope ) { - this.scope = new BlockScope( { parent: parentScope } ); + this.scope = new CatchScope( { parent: parentScope } ); } } diff --git a/src/ast/scopes/CatchScope.js b/src/ast/scopes/CatchScope.js new file mode 100644 index 00000000000..6b0d1d1cc4b --- /dev/null +++ b/src/ast/scopes/CatchScope.js @@ -0,0 +1,11 @@ +import ParameterScope from './ParameterScope'; + +export default class CatchScope extends ParameterScope { + addDeclaration ( identifier, options = {} ) { + if ( options.isHoisted ) { + return this.parent.addDeclaration( identifier, options ); + } else { + return super.addDeclaration( identifier, options ); + } + } +} diff --git a/src/ast/scopes/Scope.js b/src/ast/scopes/Scope.js index ce5f62644c3..93230ad20ed 100644 --- a/src/ast/scopes/Scope.js +++ b/src/ast/scopes/Scope.js @@ -39,12 +39,6 @@ export default class Scope { return this.variables.default; } - addParameterDeclaration ( identifier ) { - const name = identifier.name; - this.variables[ name ] = new ParameterVariable( identifier ); - return this.variables[ name ]; - } - addReturnExpression ( expression ) { this.parent && this.parent.addReturnExpression( expression ); } diff --git a/src/ast/variables/ParameterVariable.js b/src/ast/variables/ParameterVariable.js index 7477677bc43..57db225141f 100644 --- a/src/ast/variables/ParameterVariable.js +++ b/src/ast/variables/ParameterVariable.js @@ -1,38 +1,11 @@ -import LocalVariable from './LocalVariable'; -import ReplaceableInitStructuredAssignmentTracker from './ReplaceableInitStructuredAssignmentTracker'; -import { UNKNOWN_ASSIGNMENT } from '../values'; +import ReplaceableInitVariable from './ReplaceableInitVariable'; -export default class ParameterVariable extends LocalVariable { +export default class ParameterVariable extends ReplaceableInitVariable { constructor ( identifier ) { - super( identifier.name, identifier, null ); - this.assignedExpressions = new ReplaceableInitStructuredAssignmentTracker( UNKNOWN_ASSIGNMENT ); + super( identifier.name, identifier ); } getName () { return this.name; } - - hasEffectsWhenAccessedAtPath ( path, options ) { - this._updateInit( options ); - return super.hasEffectsWhenAccessedAtPath( path, options ); - } - - hasEffectsWhenAssignedAtPath ( path, options ) { - this._updateInit( options ); - return super.hasEffectsWhenAssignedAtPath( path, options ); - } - - hasEffectsWhenCalledAtPath ( path, callOptions, options ) { - this._updateInit( options ); - return super.hasEffectsWhenCalledAtPath( path, callOptions, options ); - } - - someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { - this._updateInit( options ); - return super.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ); - } - - _updateInit ( options ) { - this.assignedExpressions.setInit( options.getReplacedVariableInit( this ) || UNKNOWN_ASSIGNMENT ); - } } diff --git a/src/ast/variables/ReplaceableInitVariable.js b/src/ast/variables/ReplaceableInitVariable.js new file mode 100644 index 00000000000..dcde1e57f7c --- /dev/null +++ b/src/ast/variables/ReplaceableInitVariable.js @@ -0,0 +1,38 @@ +import LocalVariable from './LocalVariable'; +import ReplaceableInitStructuredAssignmentTracker from './ReplaceableInitStructuredAssignmentTracker'; +import { UNKNOWN_ASSIGNMENT } from '../values'; + +export default class ReplaceableInitVariable extends LocalVariable { + constructor ( name, declarator ) { + super( name, declarator, null ); + this.assignedExpressions = new ReplaceableInitStructuredAssignmentTracker( UNKNOWN_ASSIGNMENT ); + } + + getName () { + return this.name; + } + + hasEffectsWhenAccessedAtPath ( path, options ) { + this._updateInit( options ); + return super.hasEffectsWhenAccessedAtPath( path, options ); + } + + hasEffectsWhenAssignedAtPath ( path, options ) { + this._updateInit( options ); + return super.hasEffectsWhenAssignedAtPath( path, options ); + } + + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { + this._updateInit( options ); + return super.hasEffectsWhenCalledAtPath( path, callOptions, options ); + } + + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { + this._updateInit( options ); + return super.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ); + } + + _updateInit ( options ) { + this.assignedExpressions.setInit( options.getReplacedVariableInit( this ) || UNKNOWN_ASSIGNMENT ); + } +} diff --git a/src/ast/variables/ThisVariable.js b/src/ast/variables/ThisVariable.js index 1db2c7bcc8d..6ce262901b1 100644 --- a/src/ast/variables/ThisVariable.js +++ b/src/ast/variables/ThisVariable.js @@ -1,29 +1,7 @@ -import LocalVariable from './LocalVariable'; -import ReplaceableInitStructuredAssignmentTracker from './ReplaceableInitStructuredAssignmentTracker'; -import { UNKNOWN_ASSIGNMENT } from '../values'; +import ReplaceableInitVariable from './ReplaceableInitVariable'; -export default class ThisVariable extends LocalVariable { +export default class ThisVariable extends ReplaceableInitVariable { constructor () { - super( 'this', null, null ); - this.assignedExpressions = new ReplaceableInitStructuredAssignmentTracker( UNKNOWN_ASSIGNMENT ); - } - - hasEffectsWhenAccessedAtPath ( path, options ) { - this._updateInit( options ); - return super.hasEffectsWhenAccessedAtPath( path, options ); - } - - hasEffectsWhenAssignedAtPath ( path, options ) { - this._updateInit( options ); - return super.hasEffectsWhenAssignedAtPath( path, options ); - } - - hasEffectsWhenCalledAtPath ( path, callOptions, options ) { - this._updateInit( options ); - return super.hasEffectsWhenCalledAtPath( path, callOptions, options ); - } - - _updateInit ( options ) { - this.assignedExpressions.setInit( options.getReplacedVariableInit( this ) || UNKNOWN_ASSIGNMENT ); + super( 'this', null ); } } From 8e239f8c7f811e4236be11567ae99912e076323c Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Tue, 17 Oct 2017 20:45:12 +0200 Subject: [PATCH 52/76] Extend parameter logic to functions. TODO: arguments --- src/ast/nodes/shared/FunctionNode.js | 5 +++-- .../arrow-function-call-parameters/_config.js | 3 +++ .../_expected/amd.js | 0 .../_expected/cjs.js | 0 .../_expected/es.js | 0 .../_expected/iife.js | 0 .../_expected/umd.js | 0 .../main.js | 0 .../_config.js | 2 +- .../function-call-parameters/_expected/amd.js | 9 +++++++++ .../function-call-parameters/_expected/cjs.js | 7 +++++++ .../function-call-parameters/_expected/es.js | 5 +++++ .../function-call-parameters/_expected/iife.js | 10 ++++++++++ .../function-call-parameters/_expected/umd.js | 13 +++++++++++++ test/form/samples/function-call-parameters/main.js | 11 +++++++++++ 15 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 test/form/samples/arrow-function-call-parameters/_config.js rename test/form/samples/{call-parameters => arrow-function-call-parameters}/_expected/amd.js (100%) rename test/form/samples/{call-parameters => arrow-function-call-parameters}/_expected/cjs.js (100%) rename test/form/samples/{call-parameters => arrow-function-call-parameters}/_expected/es.js (100%) rename test/form/samples/{call-parameters => arrow-function-call-parameters}/_expected/iife.js (100%) rename test/form/samples/{call-parameters => arrow-function-call-parameters}/_expected/umd.js (100%) rename test/form/samples/{call-parameters => arrow-function-call-parameters}/main.js (100%) rename test/form/samples/{call-parameters => function-call-parameters}/_config.js (68%) create mode 100644 test/form/samples/function-call-parameters/_expected/amd.js create mode 100644 test/form/samples/function-call-parameters/_expected/cjs.js create mode 100644 test/form/samples/function-call-parameters/_expected/es.js create mode 100644 test/form/samples/function-call-parameters/_expected/iife.js create mode 100644 test/form/samples/function-call-parameters/_expected/umd.js create mode 100644 test/form/samples/function-call-parameters/main.js diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index 6af21af08c6..430176430f1 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -37,8 +37,9 @@ export default class FunctionNode extends Node { if ( path.length > 0 ) { return true; } - const innerOptions = options.replaceVariableInit( this.thisVariable, - callOptions.withNew ? new VirtualObjectExpression() : UNKNOWN_ASSIGNMENT ); + const innerOptions = this.scope + .getOptionsWithReplacedParameters( callOptions.parameters, options ) + .replaceVariableInit( this.thisVariable, callOptions.withNew ? new VirtualObjectExpression() : UNKNOWN_ASSIGNMENT ); return this.params.some( param => param.hasEffects( innerOptions ) ) || this.body.hasEffects( innerOptions ); } diff --git a/test/form/samples/arrow-function-call-parameters/_config.js b/test/form/samples/arrow-function-call-parameters/_config.js new file mode 100644 index 00000000000..5af13512a89 --- /dev/null +++ b/test/form/samples/arrow-function-call-parameters/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'take actual parameters into account when determining side-effects of calls to arrow functions' +}; diff --git a/test/form/samples/call-parameters/_expected/amd.js b/test/form/samples/arrow-function-call-parameters/_expected/amd.js similarity index 100% rename from test/form/samples/call-parameters/_expected/amd.js rename to test/form/samples/arrow-function-call-parameters/_expected/amd.js diff --git a/test/form/samples/call-parameters/_expected/cjs.js b/test/form/samples/arrow-function-call-parameters/_expected/cjs.js similarity index 100% rename from test/form/samples/call-parameters/_expected/cjs.js rename to test/form/samples/arrow-function-call-parameters/_expected/cjs.js diff --git a/test/form/samples/call-parameters/_expected/es.js b/test/form/samples/arrow-function-call-parameters/_expected/es.js similarity index 100% rename from test/form/samples/call-parameters/_expected/es.js rename to test/form/samples/arrow-function-call-parameters/_expected/es.js diff --git a/test/form/samples/call-parameters/_expected/iife.js b/test/form/samples/arrow-function-call-parameters/_expected/iife.js similarity index 100% rename from test/form/samples/call-parameters/_expected/iife.js rename to test/form/samples/arrow-function-call-parameters/_expected/iife.js diff --git a/test/form/samples/call-parameters/_expected/umd.js b/test/form/samples/arrow-function-call-parameters/_expected/umd.js similarity index 100% rename from test/form/samples/call-parameters/_expected/umd.js rename to test/form/samples/arrow-function-call-parameters/_expected/umd.js diff --git a/test/form/samples/call-parameters/main.js b/test/form/samples/arrow-function-call-parameters/main.js similarity index 100% rename from test/form/samples/call-parameters/main.js rename to test/form/samples/arrow-function-call-parameters/main.js diff --git a/test/form/samples/call-parameters/_config.js b/test/form/samples/function-call-parameters/_config.js similarity index 68% rename from test/form/samples/call-parameters/_config.js rename to test/form/samples/function-call-parameters/_config.js index 259e50e7c66..f84d207ce37 100644 --- a/test/form/samples/call-parameters/_config.js +++ b/test/form/samples/function-call-parameters/_config.js @@ -1,3 +1,3 @@ module.exports = { - description: 'take actual parameters into account when determining side-effects of calls' + description: 'take actual parameters into account when determining side-effects of calls to functions' }; diff --git a/test/form/samples/function-call-parameters/_expected/amd.js b/test/form/samples/function-call-parameters/_expected/amd.js new file mode 100644 index 00000000000..db174f51f63 --- /dev/null +++ b/test/form/samples/function-call-parameters/_expected/amd.js @@ -0,0 +1,9 @@ +define(function () { 'use strict'; + + const multiArgument2 = function ( func, obj ) { return func( obj ); }; + multiArgument2( obj => obj(), () => () => console.log( 'effect' ) )(); + + const multiArgument4 = function ( func, obj ) { return func( obj ); }; + multiArgument4( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; + +}); diff --git a/test/form/samples/function-call-parameters/_expected/cjs.js b/test/form/samples/function-call-parameters/_expected/cjs.js new file mode 100644 index 00000000000..1dec6b522ff --- /dev/null +++ b/test/form/samples/function-call-parameters/_expected/cjs.js @@ -0,0 +1,7 @@ +'use strict'; + +const multiArgument2 = function ( func, obj ) { return func( obj ); }; +multiArgument2( obj => obj(), () => () => console.log( 'effect' ) )(); + +const multiArgument4 = function ( func, obj ) { return func( obj ); }; +multiArgument4( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; diff --git a/test/form/samples/function-call-parameters/_expected/es.js b/test/form/samples/function-call-parameters/_expected/es.js new file mode 100644 index 00000000000..14ed8cb47c9 --- /dev/null +++ b/test/form/samples/function-call-parameters/_expected/es.js @@ -0,0 +1,5 @@ +const multiArgument2 = function ( func, obj ) { return func( obj ); }; +multiArgument2( obj => obj(), () => () => console.log( 'effect' ) )(); + +const multiArgument4 = function ( func, obj ) { return func( obj ); }; +multiArgument4( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; diff --git a/test/form/samples/function-call-parameters/_expected/iife.js b/test/form/samples/function-call-parameters/_expected/iife.js new file mode 100644 index 00000000000..34f5ca801d9 --- /dev/null +++ b/test/form/samples/function-call-parameters/_expected/iife.js @@ -0,0 +1,10 @@ +(function () { + 'use strict'; + + const multiArgument2 = function ( func, obj ) { return func( obj ); }; + multiArgument2( obj => obj(), () => () => console.log( 'effect' ) )(); + + const multiArgument4 = function ( func, obj ) { return func( obj ); }; + multiArgument4( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; + +}()); diff --git a/test/form/samples/function-call-parameters/_expected/umd.js b/test/form/samples/function-call-parameters/_expected/umd.js new file mode 100644 index 00000000000..1665ec6c015 --- /dev/null +++ b/test/form/samples/function-call-parameters/_expected/umd.js @@ -0,0 +1,13 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + const multiArgument2 = function ( func, obj ) { return func( obj ); }; + multiArgument2( obj => obj(), () => () => console.log( 'effect' ) )(); + + const multiArgument4 = function ( func, obj ) { return func( obj ); }; + multiArgument4( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; + +}))); diff --git a/test/form/samples/function-call-parameters/main.js b/test/form/samples/function-call-parameters/main.js new file mode 100644 index 00000000000..eb49637b605 --- /dev/null +++ b/test/form/samples/function-call-parameters/main.js @@ -0,0 +1,11 @@ +const multiArgument1 = function ( func, obj ) { return func( obj ); }; +multiArgument1( obj => obj(), () => () => {} )(); + +const multiArgument2 = function ( func, obj ) { return func( obj ); }; +multiArgument2( obj => obj(), () => () => console.log( 'effect' ) )(); + +const multiArgument3 = function ( func, obj ) { return func( obj ); }; +multiArgument3( obj => ({ foo: obj }), { bar: {} } ).foo.bar.baz = 1; + +const multiArgument4 = function ( func, obj ) { return func( obj ); }; +multiArgument4( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; From df24641739ce6e1b3aa0c09d4d002b9f931183a5 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Wed, 18 Oct 2017 09:04:35 +0200 Subject: [PATCH 53/76] Associate arguments and parameters in functions --- src/ast/CallOptions.js | 8 +-- src/ast/ExecutionPathOptions.js | 30 ++++++-- src/ast/nodes/ArrowFunctionExpression.js | 2 +- src/ast/nodes/CallExpression.js | 2 +- src/ast/nodes/MemberExpression.js | 26 ++++--- src/ast/nodes/shared/FunctionNode.js | 6 +- src/ast/scopes/FunctionScope.js | 11 ++- src/ast/scopes/ParameterScope.js | 8 ++- src/ast/scopes/ReturnValueScope.js | 2 +- src/ast/variables/ArgumentsVariable.js | 51 ++++++++++++++ .../function-call-parameters/_expected/amd.js | 46 +++++++++++-- .../function-call-parameters/_expected/cjs.js | 46 +++++++++++-- .../function-call-parameters/_expected/es.js | 46 +++++++++++-- .../_expected/iife.js | 46 +++++++++++-- .../function-call-parameters/_expected/umd.js | 46 +++++++++++-- .../samples/function-call-parameters/main.js | 69 ++++++++++++++++--- 16 files changed, 384 insertions(+), 61 deletions(-) create mode 100644 src/ast/variables/ArgumentsVariable.js diff --git a/src/ast/CallOptions.js b/src/ast/CallOptions.js index 1ec94660513..d01381d28f2 100644 --- a/src/ast/CallOptions.js +++ b/src/ast/CallOptions.js @@ -7,9 +7,9 @@ export default class CallOptions { return new this( callOptions, Immutable.Map() ); } - constructor ( { withNew = false, parameters = [] } = {}, nodesCalledAtPath ) { + constructor ( { withNew = false, args = [] } = {}, nodesCalledAtPath ) { this.withNew = withNew; - this.parameters = parameters; + this.args = args; this._nodesCalledAtPath = nodesCalledAtPath; } @@ -19,8 +19,8 @@ export default class CallOptions { equals ( callOptions ) { return this.withNew === callOptions.withNew - && this.parameters.length === callOptions.parameters.length - && this.parameters.every( ( parameter, index ) => parameter === callOptions.parameters[ index ] ); + && this.args.length === callOptions.args.length + && this.args.every( ( parameter, index ) => parameter === callOptions.args[ index ] ); } hasNodeBeenCalledAtPath ( path, node ) { diff --git a/src/ast/ExecutionPathOptions.js b/src/ast/ExecutionPathOptions.js index ff5d69be955..eaaf9a46827 100644 --- a/src/ast/ExecutionPathOptions.js +++ b/src/ast/ExecutionPathOptions.js @@ -1,15 +1,16 @@ import Immutable from 'immutable'; -const OPTION_IGNORE_BREAK_STATEMENTS = 'IGNORE_BREAK_STATEMENTS'; -const OPTION_IGNORE_RETURN_AWAIT_YIELD = 'IGNORE_RETURN_AWAIT_YIELD'; +const OPTION_IGNORED_LABELS = 'IGNORED_LABELS'; const OPTION_ACCESSED_NODES = 'ACCESSED_NODES'; +const OPTION_ARGUMENTS_VARIABLES = 'ARGUMENTS_VARIABLES'; const OPTION_ASSIGNED_NODES = 'ASSIGNED_NODES'; +const OPTION_IGNORE_BREAK_STATEMENTS = 'IGNORE_BREAK_STATEMENTS'; +const OPTION_IGNORE_RETURN_AWAIT_YIELD = 'IGNORE_RETURN_AWAIT_YIELD'; const OPTION_NODES_CALLED_AT_PATH_WITH_OPTIONS = 'NODES_CALLED_AT_PATH_WITH_OPTIONS'; +const OPTION_REPLACED_VARIABLE_INITS = 'REPLACED_VARIABLE_INITS'; const OPTION_RETURN_EXPRESSIONS_ACCESSED_AT_PATH = 'RETURN_EXPRESSIONS_ACCESSED_AT_PATH'; const OPTION_RETURN_EXPRESSIONS_ASSIGNED_AT_PATH = 'RETURN_EXPRESSIONS_ASSIGNED_AT_PATH'; const OPTION_RETURN_EXPRESSIONS_CALLED_AT_PATH = 'RETURN_EXPRESSIONS_CALLED_AT_PATH'; -const OPTION_REPLACED_VARIABLE_INITS = 'REPLACED_VARIABLE_INITS'; -const IGNORED_LABELS = 'IGNORED_LABELS'; const RESULT_KEY = {}; @@ -104,6 +105,13 @@ export default class ExecutionPathOptions { return this.setIn( [ OPTION_RETURN_EXPRESSIONS_CALLED_AT_PATH, callExpression, ...path, RESULT_KEY ], true ); } + /** + * @return {ParameterVariable[]} + */ + getArgumentsVariables () { + return this.get( OPTION_ARGUMENTS_VARIABLES ) || []; + } + /** * @return {ExecutionPathOptions} */ @@ -190,7 +198,7 @@ export default class ExecutionPathOptions { * @return {boolean} */ ignoreLabel ( labelName ) { - return this._optionValues.getIn( [ IGNORED_LABELS, labelName ] ); + return this._optionValues.getIn( [ OPTION_IGNORED_LABELS, labelName ] ); } /** @@ -209,6 +217,14 @@ export default class ExecutionPathOptions { return this.setIn( [ OPTION_REPLACED_VARIABLE_INITS, variable ], init ); } + /** + * @param {ParameterVariable[]} variables + * @return {ExecutionPathOptions} + */ + setArgumentsVariables ( variables ) { + return this.set( OPTION_ARGUMENTS_VARIABLES, variables ); + } + /** * @param {boolean} [value=true] * @return {ExecutionPathOptions} @@ -222,14 +238,14 @@ export default class ExecutionPathOptions { * @return {ExecutionPathOptions} */ setIgnoreLabel ( labelName ) { - return this.setIn( [ IGNORED_LABELS, labelName ], true ); + return this.setIn( [ OPTION_IGNORED_LABELS, labelName ], true ); } /** * @return {ExecutionPathOptions} */ setIgnoreNoLabels () { - return this.set( IGNORED_LABELS, null ); + return this.set( OPTION_IGNORED_LABELS, null ); } /** diff --git a/src/ast/nodes/ArrowFunctionExpression.js b/src/ast/nodes/ArrowFunctionExpression.js index 4d09bb43623..1272002f58e 100644 --- a/src/ast/nodes/ArrowFunctionExpression.js +++ b/src/ast/nodes/ArrowFunctionExpression.js @@ -25,7 +25,7 @@ export default class ArrowFunctionExpression extends Node { if ( path.length > 0 ) { return true; } - const innerOptions = this.scope.getOptionsWithReplacedParameters( callOptions.parameters, options ); + const innerOptions = this.scope.getOptionsWithReplacedParameters( callOptions.args, options ); return this.params.some( param => param.hasEffects( innerOptions ) ) || this.body.hasEffects( innerOptions ); } diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index dc8564c072d..6bb90c30a10 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -48,7 +48,7 @@ export default class CallExpression extends Node { } initialiseNode () { - this._callOptions = CallOptions.create( { withNew: false, parameters: this.arguments } ); + this._callOptions = CallOptions.create( { withNew: false, args: this.arguments } ); } someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index c2791bc7fed..16dda7df369 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -82,38 +82,36 @@ export default class MemberExpression extends Node { } if ( this.variable ) { this.variable.assignExpressionAtPath( path, expression ); - } else if ( this.computed ) { - this.object.bindAssignmentAtPath( [ UNKNOWN_KEY, ...path ], expression ); } else { - this.object.bindAssignmentAtPath( [ this.property.name, ...path ], expression ); + this.object.bindAssignmentAtPath( [ this._getPathSegment(), ...path ], expression ); } } hasEffects ( options ) { return super.hasEffects( options ) - || this.object.hasEffectsWhenAccessedAtPath( [ this.computed ? UNKNOWN_KEY : this.property.name ], options ); + || this.object.hasEffectsWhenAccessedAtPath( [ this._getPathSegment() ], options ); } hasEffectsWhenAccessedAtPath ( path, options ) { if ( this.variable ) { return this.variable.hasEffectsWhenAccessedAtPath( path, options ); } - return this.object.hasEffectsWhenAccessedAtPath( [ this.computed ? UNKNOWN_KEY : this.property.name, ...path ], options ); + return this.object.hasEffectsWhenAccessedAtPath( [ this._getPathSegment(), ...path ], options ); } hasEffectsWhenAssignedAtPath ( path, options ) { if ( this.variable ) { return this.variable.hasEffectsWhenAssignedAtPath( path, options ); } - return this.object.hasEffectsWhenAssignedAtPath( [ this.computed ? UNKNOWN_KEY : this.property.name, ...path ], options ); + return this.object.hasEffectsWhenAssignedAtPath( [ this._getPathSegment(), ...path ], options ); } hasEffectsWhenCalledAtPath ( path, callOptions, options ) { if ( this.variable ) { return this.variable.hasEffectsWhenCalledAtPath( path, callOptions, options ); } - return this.computed - || this.object.hasEffectsWhenCalledAtPath( [ this.property.name, ...path ], callOptions, options ); + return this._getPathSegment() === UNKNOWN_KEY + || this.object.hasEffectsWhenCalledAtPath( [ this._getPathSegment(), ...path ], callOptions, options ); } includeInBundle () { @@ -142,7 +140,15 @@ export default class MemberExpression extends Node { if ( this.variable ) { return this.variable.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ); } - return this.computed - || this.object.someReturnExpressionWhenCalledAtPath( [ this.property.name, ...path ], callOptions, predicateFunction, options ); + return this._getPathSegment() === UNKNOWN_KEY + || this.object.someReturnExpressionWhenCalledAtPath( [ this._getPathSegment(), ...path ], + callOptions, predicateFunction, options ); + } + + _getPathSegment () { + if ( this.computed ) { + return this.property.type === 'Literal' ? String( this.property.value ) : UNKNOWN_KEY; + } + return this.property.name; } } diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index 430176430f1..75e1aab1174 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -1,11 +1,9 @@ import Node from '../../Node.js'; import FunctionScope from '../../scopes/FunctionScope'; import VirtualObjectExpression from './VirtualObjectExpression'; -import { UNKNOWN_ASSIGNMENT } from '../../values'; export default class FunctionNode extends Node { bindNode () { - this.thisVariable = this.scope.findVariable( 'this' ); this.body.bindImplicitReturnExpressionToScope(); } @@ -37,9 +35,7 @@ export default class FunctionNode extends Node { if ( path.length > 0 ) { return true; } - const innerOptions = this.scope - .getOptionsWithReplacedParameters( callOptions.parameters, options ) - .replaceVariableInit( this.thisVariable, callOptions.withNew ? new VirtualObjectExpression() : UNKNOWN_ASSIGNMENT ); + const innerOptions = this.scope.getOptionsWhenCalledWith( callOptions, options ); return this.params.some( param => param.hasEffects( innerOptions ) ) || this.body.hasEffects( innerOptions ); } diff --git a/src/ast/scopes/FunctionScope.js b/src/ast/scopes/FunctionScope.js index 0e88ce89e0e..5ebd5726771 100644 --- a/src/ast/scopes/FunctionScope.js +++ b/src/ast/scopes/FunctionScope.js @@ -1,16 +1,23 @@ import ReturnValueScope from './ReturnValueScope'; +import ArgumentsVariable from '../variables/ArgumentsVariable'; import ThisVariable from '../variables/ThisVariable'; -import LocalVariable from '../variables/LocalVariable'; import { UNKNOWN_ASSIGNMENT } from '../values'; +import VirtualObjectExpression from '../nodes/shared/VirtualObjectExpression'; export default class FunctionScope extends ReturnValueScope { constructor ( options = {} ) { super( options ); - this.variables.arguments = new LocalVariable( 'arguments', null, UNKNOWN_ASSIGNMENT ); + this.variables.arguments = new ArgumentsVariable( super.getParameterVariables() ); this.variables.this = new ThisVariable(); } findLexicalBoundary () { return this; } + + getOptionsWhenCalledWith ( { args, withNew }, options ) { + return super.getOptionsWithReplacedParameters( args, options ) + .replaceVariableInit( this.variables.this, withNew ? new VirtualObjectExpression() : UNKNOWN_ASSIGNMENT ) + .setArgumentsVariables( args.map( ( parameter, index ) => super.getParameterVariables()[index] || parameter) ); + } } diff --git a/src/ast/scopes/ParameterScope.js b/src/ast/scopes/ParameterScope.js index 31b4714678e..b1be9e3c020 100644 --- a/src/ast/scopes/ParameterScope.js +++ b/src/ast/scopes/ParameterScope.js @@ -21,11 +21,15 @@ export default class ParameterScope extends Scope { return variable; } - getOptionsWithReplacedParameters ( parameterReplacements, options ) { + getOptionsWithReplacedParameters ( args, options ) { let newOptions = options; this._parameters.forEach( ( parameter, index ) => - newOptions = newOptions.replaceVariableInit( parameter, parameterReplacements[ index ] || UNKNOWN_ASSIGNMENT ) + newOptions = newOptions.replaceVariableInit( parameter, args[ index ] || UNKNOWN_ASSIGNMENT ) ); return newOptions; } + + getParameterVariables () { + return this._parameters; + } } diff --git a/src/ast/scopes/ReturnValueScope.js b/src/ast/scopes/ReturnValueScope.js index f32084706ef..18fef9ea1ba 100644 --- a/src/ast/scopes/ReturnValueScope.js +++ b/src/ast/scopes/ReturnValueScope.js @@ -11,7 +11,7 @@ export default class ReturnValueScope extends ParameterScope { } someReturnExpressionWhenCalled ( callOptions, predicateFunction, options ) { - const innerOptions = this.getOptionsWithReplacedParameters( callOptions.parameters, options ); + const innerOptions = this.getOptionsWithReplacedParameters( callOptions.args, options ); return Array.from( this._returnExpressions ).some( predicateFunction( innerOptions ) ); } } diff --git a/src/ast/variables/ArgumentsVariable.js b/src/ast/variables/ArgumentsVariable.js new file mode 100644 index 00000000000..7ea640b3a9d --- /dev/null +++ b/src/ast/variables/ArgumentsVariable.js @@ -0,0 +1,51 @@ +import LocalVariable from './LocalVariable'; +import { UNKNOWN_ASSIGNMENT } from '../values'; + +const getParameterVariable = ( path, options ) => + options.getArgumentsVariables()[ path[ 0 ] ] || UNKNOWN_ASSIGNMENT; + +export default class ArgumentsVariable extends LocalVariable { + constructor ( parameters ) { + super( 'arguments', null, UNKNOWN_ASSIGNMENT ); + this._parameters = parameters; + } + + assignExpressionAtPath ( path, expression ) { + if ( path.length >= 1 ) { + const parameter = this._parameters[ path[ 0 ] ]; + parameter && parameter.assignExpressionAtPath( path.slice( 1 ), expression ); + } + } + + hasEffectsWhenAccessedAtPath ( path, options ) { + if ( path.length < 2 ) { + return false; + } + return getParameterVariable( path, options ) + .hasEffectsWhenAccessedAtPath( path.slice( 1 ), options ); + } + + hasEffectsWhenAssignedAtPath ( path, options ) { + if ( path.length === 0 ) { + return true; + } + return getParameterVariable( path, options ) + .hasEffectsWhenAssignedAtPath( path.slice( 1 ), options ); + } + + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { + if ( path.length === 0 ) { + return true; + } + return getParameterVariable( path, options ) + .hasEffectsWhenCalledAtPath( path.slice( 1 ), callOptions, options ); + } + + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { + if ( path.length === 0 ) { + return true; + } + return getParameterVariable( path, options ) + .someReturnExpressionWhenCalledAtPath( path.slice( 1 ), callOptions, predicateFunction, options ); + } +} diff --git a/test/form/samples/function-call-parameters/_expected/amd.js b/test/form/samples/function-call-parameters/_expected/amd.js index db174f51f63..5358a209cfc 100644 --- a/test/form/samples/function-call-parameters/_expected/amd.js +++ b/test/form/samples/function-call-parameters/_expected/amd.js @@ -1,9 +1,47 @@ define(function () { 'use strict'; - const multiArgument2 = function ( func, obj ) { return func( obj ); }; - multiArgument2( obj => obj(), () => () => console.log( 'effect' ) )(); + // parameters are associated correctly + // parameters are associated correctly + const retained1 = function ( func, obj ) { return func( obj ); }; + retained1( obj => obj(), () => () => console.log( 'effect' ) )(); - const multiArgument4 = function ( func, obj ) { return func( obj ); }; - multiArgument4( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; + const retained2 = function ( func, obj ) { return func( obj ); }; + retained2( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; + + // parameters and arguments have the same values + function retained3 ( x ) { + x.foo.bar = 1; + } + + retained3( {} ); + + function retained4 ( x ) { + arguments[ 0 ].foo.bar = 1; + } + + retained4( {} ); + + // assigning to an argument will change the corresponding parameter + function retained5 ( x ) { + arguments[ 0 ] = {}; + x.foo.bar = 1; + } + + retained5( { foo: {} } ); + + // assigning to a parameter will change the corresponding argument + function retained6 ( x ) { + x = {}; + arguments[ 0 ].foo.bar = 1; + } + + retained6( { foo: {} } ); + + // the number of arguments does not depend on the number of parameters + function retained7 ( x ) { + arguments[ 1 ].foo.bar = 1; + } + + retained7( {}, {} ); }); diff --git a/test/form/samples/function-call-parameters/_expected/cjs.js b/test/form/samples/function-call-parameters/_expected/cjs.js index 1dec6b522ff..1870904f6ed 100644 --- a/test/form/samples/function-call-parameters/_expected/cjs.js +++ b/test/form/samples/function-call-parameters/_expected/cjs.js @@ -1,7 +1,45 @@ 'use strict'; -const multiArgument2 = function ( func, obj ) { return func( obj ); }; -multiArgument2( obj => obj(), () => () => console.log( 'effect' ) )(); +// parameters are associated correctly +// parameters are associated correctly +const retained1 = function ( func, obj ) { return func( obj ); }; +retained1( obj => obj(), () => () => console.log( 'effect' ) )(); -const multiArgument4 = function ( func, obj ) { return func( obj ); }; -multiArgument4( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; +const retained2 = function ( func, obj ) { return func( obj ); }; +retained2( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; + +// parameters and arguments have the same values +function retained3 ( x ) { + x.foo.bar = 1; +} + +retained3( {} ); + +function retained4 ( x ) { + arguments[ 0 ].foo.bar = 1; +} + +retained4( {} ); + +// assigning to an argument will change the corresponding parameter +function retained5 ( x ) { + arguments[ 0 ] = {}; + x.foo.bar = 1; +} + +retained5( { foo: {} } ); + +// assigning to a parameter will change the corresponding argument +function retained6 ( x ) { + x = {}; + arguments[ 0 ].foo.bar = 1; +} + +retained6( { foo: {} } ); + +// the number of arguments does not depend on the number of parameters +function retained7 ( x ) { + arguments[ 1 ].foo.bar = 1; +} + +retained7( {}, {} ); diff --git a/test/form/samples/function-call-parameters/_expected/es.js b/test/form/samples/function-call-parameters/_expected/es.js index 14ed8cb47c9..d6770b8be4f 100644 --- a/test/form/samples/function-call-parameters/_expected/es.js +++ b/test/form/samples/function-call-parameters/_expected/es.js @@ -1,5 +1,43 @@ -const multiArgument2 = function ( func, obj ) { return func( obj ); }; -multiArgument2( obj => obj(), () => () => console.log( 'effect' ) )(); +// parameters are associated correctly +// parameters are associated correctly +const retained1 = function ( func, obj ) { return func( obj ); }; +retained1( obj => obj(), () => () => console.log( 'effect' ) )(); -const multiArgument4 = function ( func, obj ) { return func( obj ); }; -multiArgument4( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; +const retained2 = function ( func, obj ) { return func( obj ); }; +retained2( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; + +// parameters and arguments have the same values +function retained3 ( x ) { + x.foo.bar = 1; +} + +retained3( {} ); + +function retained4 ( x ) { + arguments[ 0 ].foo.bar = 1; +} + +retained4( {} ); + +// assigning to an argument will change the corresponding parameter +function retained5 ( x ) { + arguments[ 0 ] = {}; + x.foo.bar = 1; +} + +retained5( { foo: {} } ); + +// assigning to a parameter will change the corresponding argument +function retained6 ( x ) { + x = {}; + arguments[ 0 ].foo.bar = 1; +} + +retained6( { foo: {} } ); + +// the number of arguments does not depend on the number of parameters +function retained7 ( x ) { + arguments[ 1 ].foo.bar = 1; +} + +retained7( {}, {} ); diff --git a/test/form/samples/function-call-parameters/_expected/iife.js b/test/form/samples/function-call-parameters/_expected/iife.js index 34f5ca801d9..8199579ce3d 100644 --- a/test/form/samples/function-call-parameters/_expected/iife.js +++ b/test/form/samples/function-call-parameters/_expected/iife.js @@ -1,10 +1,48 @@ (function () { 'use strict'; - const multiArgument2 = function ( func, obj ) { return func( obj ); }; - multiArgument2( obj => obj(), () => () => console.log( 'effect' ) )(); + // parameters are associated correctly + // parameters are associated correctly + const retained1 = function ( func, obj ) { return func( obj ); }; + retained1( obj => obj(), () => () => console.log( 'effect' ) )(); - const multiArgument4 = function ( func, obj ) { return func( obj ); }; - multiArgument4( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; + const retained2 = function ( func, obj ) { return func( obj ); }; + retained2( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; + + // parameters and arguments have the same values + function retained3 ( x ) { + x.foo.bar = 1; + } + + retained3( {} ); + + function retained4 ( x ) { + arguments[ 0 ].foo.bar = 1; + } + + retained4( {} ); + + // assigning to an argument will change the corresponding parameter + function retained5 ( x ) { + arguments[ 0 ] = {}; + x.foo.bar = 1; + } + + retained5( { foo: {} } ); + + // assigning to a parameter will change the corresponding argument + function retained6 ( x ) { + x = {}; + arguments[ 0 ].foo.bar = 1; + } + + retained6( { foo: {} } ); + + // the number of arguments does not depend on the number of parameters + function retained7 ( x ) { + arguments[ 1 ].foo.bar = 1; + } + + retained7( {}, {} ); }()); diff --git a/test/form/samples/function-call-parameters/_expected/umd.js b/test/form/samples/function-call-parameters/_expected/umd.js index 1665ec6c015..274b18fe6c9 100644 --- a/test/form/samples/function-call-parameters/_expected/umd.js +++ b/test/form/samples/function-call-parameters/_expected/umd.js @@ -4,10 +4,48 @@ (factory()); }(this, (function () { 'use strict'; - const multiArgument2 = function ( func, obj ) { return func( obj ); }; - multiArgument2( obj => obj(), () => () => console.log( 'effect' ) )(); + // parameters are associated correctly + // parameters are associated correctly + const retained1 = function ( func, obj ) { return func( obj ); }; + retained1( obj => obj(), () => () => console.log( 'effect' ) )(); - const multiArgument4 = function ( func, obj ) { return func( obj ); }; - multiArgument4( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; + const retained2 = function ( func, obj ) { return func( obj ); }; + retained2( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; + + // parameters and arguments have the same values + function retained3 ( x ) { + x.foo.bar = 1; + } + + retained3( {} ); + + function retained4 ( x ) { + arguments[ 0 ].foo.bar = 1; + } + + retained4( {} ); + + // assigning to an argument will change the corresponding parameter + function retained5 ( x ) { + arguments[ 0 ] = {}; + x.foo.bar = 1; + } + + retained5( { foo: {} } ); + + // assigning to a parameter will change the corresponding argument + function retained6 ( x ) { + x = {}; + arguments[ 0 ].foo.bar = 1; + } + + retained6( { foo: {} } ); + + // the number of arguments does not depend on the number of parameters + function retained7 ( x ) { + arguments[ 1 ].foo.bar = 1; + } + + retained7( {}, {} ); }))); diff --git a/test/form/samples/function-call-parameters/main.js b/test/form/samples/function-call-parameters/main.js index eb49637b605..2e0e35da1ad 100644 --- a/test/form/samples/function-call-parameters/main.js +++ b/test/form/samples/function-call-parameters/main.js @@ -1,11 +1,64 @@ -const multiArgument1 = function ( func, obj ) { return func( obj ); }; -multiArgument1( obj => obj(), () => () => {} )(); +// parameters are associated correctly +const removed1 = function ( func, obj ) { return func( obj ); }; +removed1( obj => obj(), () => () => {} )(); -const multiArgument2 = function ( func, obj ) { return func( obj ); }; -multiArgument2( obj => obj(), () => () => console.log( 'effect' ) )(); +const removed2 = function ( func, obj ) { return func( obj ); }; +removed2( obj => ({ foo: obj }), { bar: {} } ).foo.bar.baz = 1; -const multiArgument3 = function ( func, obj ) { return func( obj ); }; -multiArgument3( obj => ({ foo: obj }), { bar: {} } ).foo.bar.baz = 1; +// parameters and arguments have the same values +function removed3 ( x ) { + x.foo.bar = 1; + arguments[ 0 ].foo.bar = 1; +} -const multiArgument4 = function ( func, obj ) { return func( obj ); }; -multiArgument4( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; +removed3( { foo: {} } ); + +// the number of arguments does not depend on the number of parameters +function removed4 ( x ) { + arguments[ 1 ].foo.bar = 1; +} + +removed4( {}, { foo: {} } ); + +// parameters are associated correctly +const retained1 = function ( func, obj ) { return func( obj ); }; +retained1( obj => obj(), () => () => console.log( 'effect' ) )(); + +const retained2 = function ( func, obj ) { return func( obj ); }; +retained2( obj => ({ foo: obj }), {} ).foo.bar.baz = 1; + +// parameters and arguments have the same values +function retained3 ( x ) { + x.foo.bar = 1; +} + +retained3( {} ); + +function retained4 ( x ) { + arguments[ 0 ].foo.bar = 1; +} + +retained4( {} ); + +// assigning to an argument will change the corresponding parameter +function retained5 ( x ) { + arguments[ 0 ] = {}; + x.foo.bar = 1; +} + +retained5( { foo: {} } ); + +// assigning to a parameter will change the corresponding argument +function retained6 ( x ) { + x = {}; + arguments[ 0 ].foo.bar = 1; +} + +retained6( { foo: {} } ); + +// the number of arguments does not depend on the number of parameters +function retained7 ( x ) { + arguments[ 1 ].foo.bar = 1; +} + +retained7( {}, {} ); From fe29c11ffa66f36b39384109ac21d9090e4272bc Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Fri, 20 Oct 2017 07:30:49 +0200 Subject: [PATCH 54/76] Add "includeWithEffects" to variables --- src/ast/variables/ExportDefaultVariable.js | 11 +++++++++-- src/ast/variables/LocalVariable.js | 16 ++++++++++++---- src/ast/variables/NamespaceVariable.js | 18 +++++++++++++----- src/ast/variables/Variable.js | 20 ++++++++++++++++++++ 4 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/ast/variables/ExportDefaultVariable.js b/src/ast/variables/ExportDefaultVariable.js index 3ab2fea21c9..69694d06579 100644 --- a/src/ast/variables/ExportDefaultVariable.js +++ b/src/ast/variables/ExportDefaultVariable.js @@ -26,14 +26,21 @@ export default class ExportDefaultVariable extends LocalVariable { } includeVariable () { - if ( this.included ) { + if (!super.includeVariable()) { return false; } - this.included = true; this.declarations.forEach( declaration => declaration.includeDefaultExport() ); return true; } + includeWithEffects () { + if (!super.includeWithEffects()) { + return false; + } + this.declarations.forEach( declaration => declaration.includeWithEffects() ); + return true; + } + setOriginalVariable ( original ) { this._original = original; } diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index 1aa9061e117..09e9464859f 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -64,11 +64,19 @@ export default class LocalVariable extends Variable { } includeVariable () { - const hasBeenIncluded = super.includeVariable(); - if ( hasBeenIncluded ) { - this.declarations.forEach( identifier => identifier.includeInBundle() ); + if ( !super.includeVariable() ) { + return false; } - return hasBeenIncluded; + this.declarations.forEach( identifier => identifier.includeInBundle() ); + return true; + } + + includeWithEffects () { + if ( !super.includeWithEffects() ) { + return false; + } + this.declarations.forEach( identifier => identifier.includeWithEffects() ); + return true; } someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { diff --git a/src/ast/variables/NamespaceVariable.js b/src/ast/variables/NamespaceVariable.js index e16ad8a52d1..2ae352b6702 100644 --- a/src/ast/variables/NamespaceVariable.js +++ b/src/ast/variables/NamespaceVariable.js @@ -20,12 +20,20 @@ export default class NamespaceVariable extends Variable { } includeVariable () { - const hasBeenIncluded = super.includeVariable(); - if ( hasBeenIncluded ) { - this.needsNamespaceBlock = true; - forOwn( this.originals, original => original.includeVariable() ); + if ( !super.includeVariable() ) { + return false; } - return hasBeenIncluded; + this.needsNamespaceBlock = true; + forOwn( this.originals, original => original.includeVariable() ); + return true; + } + + includeWithEffects () { + if ( !super.includeWithEffects() ) { + return false; + } + forOwn( this.originals, original => original.includeWithEffects() ); + return true; } renderBlock ( es, legacy, indentString ) { diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index b4a67fde3b6..6ac161a5792 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -72,6 +72,26 @@ export default class Variable { return true; } + /** + * Marks this variable's value as being essential to a bundle. This is usually the case when + * a variable is exported or assigned to an exported variable. This also includes a variable. + * This is in contrast to variables which are necessary for the code to be valid but the value + * of which is not important. + * Once a variable is included with effects, it should take care all its declarations are + * included with effects. + * @returns {boolean} + */ + includeWithEffects () { + if ( this.includedWithEffects ) { + return false; + } + this.includedWithEffects = true; + if ( !this.included ) { + this.includeVariable(); + } + return true; + } + /** * @param {String[]} path * @param {CallOptions} callOptions From ba4d4b88ccd986cfbaef4dde7da7ad511de9997e Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Fri, 20 Oct 2017 07:38:21 +0200 Subject: [PATCH 55/76] Remove "isFullyIncluded" as it does not seem to have any measurable performance benefits. We should revisit this topic once all functionality is implemented and then do some actual measurements. --- src/ast/Node.js | 19 +++---------------- src/ast/nodes/BlockStatement.js | 1 - src/ast/nodes/SequenceExpression.js | 1 - src/ast/nodes/SwitchCase.js | 1 - src/ast/nodes/VariableDeclaration.js | 2 -- 5 files changed, 3 insertions(+), 21 deletions(-) diff --git a/src/ast/Node.js b/src/ast/Node.js index 248eddef0e4..24fa19b5826 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -111,7 +111,6 @@ export default class Node { * @return {boolean} */ includeInBundle () { - if ( this.isFullyIncluded() ) return false; let addedNewNodes = false; this.eachChild( childNode => { if ( childNode.includeInBundle() ) { @@ -178,18 +177,6 @@ export default class Node { } } - /** - * Shortcut to skip checking this node for effects when all children have already - * been included. - * @param {Scope} parentScope - */ - isFullyIncluded ( parentScope ) { - if ( this._fullyIncluded ) { - return true; - } - this._fullyIncluded = this.included && !this.someChild( child => !child.isFullyIncluded() ); - } - locate () { // useful for debugging const location = locate( this.module.code, this.start, { offsetLine: 1 } ); @@ -205,9 +192,9 @@ export default class Node { /** * Start a new execution path to determine if this node has an effect on the bundle and - * should therefore be included. Unless they are fully included, included nodes should - * always be included again in subsequent visits as the inclusion of additional variables - * may require the inclusion of more child nodes in e.g. block statements. + * should therefore be included. Included nodes should always be included again in subsequent + * visits as the inclusion of additional variables may require the inclusion of more child + * nodes in e.g. block statements. * @return {boolean} */ shouldBeIncluded () { diff --git a/src/ast/nodes/BlockStatement.js b/src/ast/nodes/BlockStatement.js index 7913d66ac06..c5350c02406 100644 --- a/src/ast/nodes/BlockStatement.js +++ b/src/ast/nodes/BlockStatement.js @@ -16,7 +16,6 @@ export default class BlockStatement extends Statement { } includeInBundle () { - if ( this.isFullyIncluded() ) return false; let addedNewNodes = false; this.body.forEach( node => { if ( node.shouldBeIncluded() ) { diff --git a/src/ast/nodes/SequenceExpression.js b/src/ast/nodes/SequenceExpression.js index cbc0a1805ed..e2160136e98 100644 --- a/src/ast/nodes/SequenceExpression.js +++ b/src/ast/nodes/SequenceExpression.js @@ -10,7 +10,6 @@ export default class SequenceExpression extends Node { } includeInBundle () { - if ( this.isFullyIncluded() ) return false; let addedNewNodes = false; if ( this.expressions[ this.expressions.length - 1 ].includeInBundle() ) { addedNewNodes = true; diff --git a/src/ast/nodes/SwitchCase.js b/src/ast/nodes/SwitchCase.js index 76f074568b1..e1a2a8cd4ca 100644 --- a/src/ast/nodes/SwitchCase.js +++ b/src/ast/nodes/SwitchCase.js @@ -2,7 +2,6 @@ import Node from '../Node'; export default class SwitchCase extends Node { includeInBundle () { - if ( this.isFullyIncluded() ) return false; let addedNewNodes = false; if (this.test && this.test.includeInBundle()) { addedNewNodes = true; diff --git a/src/ast/nodes/VariableDeclaration.js b/src/ast/nodes/VariableDeclaration.js index 8f0246e15f9..9dfa9c2feb6 100644 --- a/src/ast/nodes/VariableDeclaration.js +++ b/src/ast/nodes/VariableDeclaration.js @@ -27,7 +27,6 @@ export default class VariableDeclaration extends Node { } includeWithAllDeclarations () { - if ( this.isFullyIncluded() ) return false; let addedNewNodes = false; this.declarations.forEach( declarator => { if ( declarator.includeInBundle() ) { @@ -42,7 +41,6 @@ export default class VariableDeclaration extends Node { } includeInBundle () { - if ( this.isFullyIncluded() ) return false; let addedNewNodes = false; this.declarations.forEach( declarator => { if ( declarator.shouldBeIncluded() ) { From 4639bb112908529f8f6ea13e8b3db806b0a3ec3e Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Fri, 20 Oct 2017 07:53:05 +0200 Subject: [PATCH 56/76] Slightly simplify includeInBundle logic --- src/ast/Node.js | 9 +++------ src/ast/nodes/BlockStatement.js | 9 +++------ src/ast/nodes/SequenceExpression.js | 9 +++------ src/ast/nodes/SwitchCase.js | 9 +++------ src/ast/nodes/VariableDeclaration.js | 18 ++++++------------ 5 files changed, 18 insertions(+), 36 deletions(-) diff --git a/src/ast/Node.js b/src/ast/Node.js index 24fa19b5826..f7071b49cf2 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -111,17 +111,14 @@ export default class Node { * @return {boolean} */ includeInBundle () { - let addedNewNodes = false; + let addedNewNodes = !this.included; + this.included = true; this.eachChild( childNode => { if ( childNode.includeInBundle() ) { addedNewNodes = true; } } ); - if ( this.included && !addedNewNodes ) { - return false; - } - this.included = true; - return true; + return addedNewNodes; } /** diff --git a/src/ast/nodes/BlockStatement.js b/src/ast/nodes/BlockStatement.js index c5350c02406..cf96045bb14 100644 --- a/src/ast/nodes/BlockStatement.js +++ b/src/ast/nodes/BlockStatement.js @@ -16,7 +16,8 @@ export default class BlockStatement extends Statement { } includeInBundle () { - let addedNewNodes = false; + let addedNewNodes = !this.included; + this.included = true; this.body.forEach( node => { if ( node.shouldBeIncluded() ) { if ( node.includeInBundle() ) { @@ -24,11 +25,7 @@ export default class BlockStatement extends Statement { } } } ); - if ( !this.included || addedNewNodes ) { - this.included = true; - return true; - } - return false; + return addedNewNodes; } initialiseAndReplaceScope ( scope ) { diff --git a/src/ast/nodes/SequenceExpression.js b/src/ast/nodes/SequenceExpression.js index e2160136e98..0f9de2ca868 100644 --- a/src/ast/nodes/SequenceExpression.js +++ b/src/ast/nodes/SequenceExpression.js @@ -10,7 +10,8 @@ export default class SequenceExpression extends Node { } includeInBundle () { - let addedNewNodes = false; + let addedNewNodes = !this.included; + this.included = true; if ( this.expressions[ this.expressions.length - 1 ].includeInBundle() ) { addedNewNodes = true; } @@ -21,11 +22,7 @@ export default class SequenceExpression extends Node { } } } ); - if ( !this.included || addedNewNodes ) { - this.included = true; - return true; - } - return false; + return addedNewNodes; } render ( code, es ) { diff --git a/src/ast/nodes/SwitchCase.js b/src/ast/nodes/SwitchCase.js index e1a2a8cd4ca..60689ca1fb0 100644 --- a/src/ast/nodes/SwitchCase.js +++ b/src/ast/nodes/SwitchCase.js @@ -2,7 +2,8 @@ import Node from '../Node'; export default class SwitchCase extends Node { includeInBundle () { - let addedNewNodes = false; + let addedNewNodes = !this.included; + this.included = true; if (this.test && this.test.includeInBundle()) { addedNewNodes = true; } @@ -13,10 +14,6 @@ export default class SwitchCase extends Node { } } } ); - if ( !this.included || addedNewNodes ) { - this.included = true; - return true; - } - return false; + return addedNewNodes; } } diff --git a/src/ast/nodes/VariableDeclaration.js b/src/ast/nodes/VariableDeclaration.js index 9dfa9c2feb6..b1eccfef516 100644 --- a/src/ast/nodes/VariableDeclaration.js +++ b/src/ast/nodes/VariableDeclaration.js @@ -27,21 +27,19 @@ export default class VariableDeclaration extends Node { } includeWithAllDeclarations () { - let addedNewNodes = false; + let addedNewNodes = !this.included; + this.included = true; this.declarations.forEach( declarator => { if ( declarator.includeInBundle() ) { addedNewNodes = true; } } ); - if ( !this.included || addedNewNodes ) { - this.included = true; - return true; - } - return false; + return addedNewNodes; } includeInBundle () { - let addedNewNodes = false; + let addedNewNodes = !this.included; + this.included = true; this.declarations.forEach( declarator => { if ( declarator.shouldBeIncluded() ) { if ( declarator.includeInBundle() ) { @@ -49,11 +47,7 @@ export default class VariableDeclaration extends Node { } } } ); - if ( !this.included || addedNewNodes ) { - this.included = true; - return true; - } - return false; + return addedNewNodes; } initialiseChildren () { From 4ffbba893eb44fb18b535032aa7b2073469afcfe Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Fri, 20 Oct 2017 22:45:21 +0200 Subject: [PATCH 57/76] * Remove includeWithEffects again as this appears to be somewhat more complicated * Do not count inclusions as effects --- src/Module.js | 7 ----- src/ast/Node.js | 16 ++++++++-- src/ast/nodes/ArrowFunctionExpression.js | 2 +- src/ast/nodes/BlockStatement.js | 1 - src/ast/nodes/CallExpression.js | 3 +- src/ast/nodes/ConditionalExpression.js | 3 +- src/ast/nodes/DoWhileStatement.js | 3 +- src/ast/nodes/ExportNamedDeclaration.js | 2 +- src/ast/nodes/ForInStatement.js | 3 +- src/ast/nodes/ForOfStatement.js | 7 ++--- src/ast/nodes/ForStatement.js | 7 ++--- src/ast/nodes/NewExpression.js | 3 +- src/ast/nodes/Property.js | 3 +- src/ast/nodes/UnaryExpression.js | 3 +- src/ast/nodes/UpdateExpression.js | 3 +- src/ast/nodes/WhileStatement.js | 3 +- src/ast/nodes/shared/FunctionNode.js | 2 +- src/ast/variables/ExportDefaultVariable.js | 8 ----- src/ast/variables/LocalVariable.js | 8 ----- src/ast/variables/NamespaceVariable.js | 8 ----- src/ast/variables/Variable.js | 20 ------------- .../_expected/amd.js | 7 +++-- .../_expected/cjs.js | 7 +++-- .../_expected/es.js | 7 +++-- .../_expected/iife.js | 7 +++-- .../_expected/umd.js | 7 +++-- .../side-effects-getters-and-setters/main.js | 10 +++++-- test/test.js | 30 +++++++++++-------- 28 files changed, 78 insertions(+), 112 deletions(-) diff --git a/src/Module.js b/src/Module.js index d7098191bc2..61356931d32 100644 --- a/src/Module.js +++ b/src/Module.js @@ -277,13 +277,6 @@ export default class Module { for ( const node of this.ast.body ) { node.bind(); } - - // if ( this.declarations.default ) { - // if ( this.exports.default.identifier ) { - // const declaration = this.trace( this.exports.default.identifier ); - // if ( declaration ) this.declarations.default.bind( declaration ); - // } - // } } error ( props, pos ) { diff --git a/src/ast/Node.js b/src/ast/Node.js index f7071b49cf2..9f695318900 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -70,8 +70,7 @@ export default class Node { * @return {boolean} */ hasEffects ( options ) { - return this.included - || this.hasEffectsWhenAccessedAtPath( [], options ) + return this.hasEffectsWhenAccessedAtPath( [], options ) || this.someChild( child => child.hasEffects( options ) ); } @@ -103,6 +102,15 @@ export default class Node { return true; } + /** + * Returns true if this node or any of its children is included. + * @return {boolean} + */ + hasIncludedChild () { + return this.included + || this.someChild( child => child.hasIncludedChild() ); + } + /** * Includes the node in the bundle. Children are usually included if they are * necessary for this node (e.g. a function body) or if they have effects. @@ -195,7 +203,9 @@ export default class Node { * @return {boolean} */ shouldBeIncluded () { - return this.hasEffects( ExecutionPathOptions.create() ); + return this.included + || this.hasEffects( ExecutionPathOptions.create() ) + || this.hasIncludedChild(); } someChild ( callback ) { diff --git a/src/ast/nodes/ArrowFunctionExpression.js b/src/ast/nodes/ArrowFunctionExpression.js index 1272002f58e..ce2d178aad7 100644 --- a/src/ast/nodes/ArrowFunctionExpression.js +++ b/src/ast/nodes/ArrowFunctionExpression.js @@ -10,7 +10,7 @@ export default class ArrowFunctionExpression extends Node { } hasEffects () { - return this.included; + return false; } hasEffectsWhenAccessedAtPath ( path ) { diff --git a/src/ast/nodes/BlockStatement.js b/src/ast/nodes/BlockStatement.js index cf96045bb14..1aad2de6573 100644 --- a/src/ast/nodes/BlockStatement.js +++ b/src/ast/nodes/BlockStatement.js @@ -11,7 +11,6 @@ export default class BlockStatement extends Statement { } hasEffects ( options ) { - // Empty block statements do not have effects even though they may be included as e.g. function body return this.body.some( child => child.hasEffects( options ) ); } diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index 6bb90c30a10..8db1bbdccf8 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -24,8 +24,7 @@ export default class CallExpression extends Node { } hasEffects ( options ) { - return this.included - || this.arguments.some( child => child.hasEffects( options ) ) + return this.arguments.some( child => child.hasEffects( options ) ) || this.callee.hasEffectsWhenCalledAtPath( [], this._callOptions, options.getHasEffectsWhenCalledOptions() ); } diff --git a/src/ast/nodes/ConditionalExpression.js b/src/ast/nodes/ConditionalExpression.js index 83dcb27afbc..48fe36a5e93 100644 --- a/src/ast/nodes/ConditionalExpression.js +++ b/src/ast/nodes/ConditionalExpression.js @@ -22,8 +22,7 @@ export default class ConditionalExpression extends Node { hasEffects ( options ) { return ( - this.included - || this.test.hasEffects( options ) + this.test.hasEffects( options ) || this._someRelevantBranch( node => node.hasEffects( options ) ) ); } diff --git a/src/ast/nodes/DoWhileStatement.js b/src/ast/nodes/DoWhileStatement.js index 8f8d8ba11e0..5865a78aa89 100644 --- a/src/ast/nodes/DoWhileStatement.js +++ b/src/ast/nodes/DoWhileStatement.js @@ -3,8 +3,7 @@ import Statement from './shared/Statement.js'; export default class DoWhileStatement extends Statement { hasEffects ( options ) { return ( - this.included - || this.test.hasEffects( options ) + this.test.hasEffects( options ) || this.body.hasEffects( options.setIgnoreBreakStatements() ) ); } diff --git a/src/ast/nodes/ExportNamedDeclaration.js b/src/ast/nodes/ExportNamedDeclaration.js index 6ce0d121038..f392d286e41 100644 --- a/src/ast/nodes/ExportNamedDeclaration.js +++ b/src/ast/nodes/ExportNamedDeclaration.js @@ -7,7 +7,7 @@ export default class ExportNamedDeclaration extends Node { } hasEffects ( options ) { - return this.included || (this.declaration && this.declaration.hasEffects( options )); + return this.declaration && this.declaration.hasEffects( options ); } initialiseNode () { diff --git a/src/ast/nodes/ForInStatement.js b/src/ast/nodes/ForInStatement.js index b4e870ceb15..0d188cc2de7 100644 --- a/src/ast/nodes/ForInStatement.js +++ b/src/ast/nodes/ForInStatement.js @@ -4,8 +4,7 @@ import BlockScope from '../scopes/BlockScope'; export default class ForInStatement extends Statement { hasEffects ( options ) { return ( - this.included - || this.left && (this.left.hasEffects( options ) || this.left.hasEffectsWhenAssignedAtPath( [], options )) + this.left && (this.left.hasEffects( options ) || this.left.hasEffectsWhenAssignedAtPath( [], options )) || this.right && this.right.hasEffects( options ) || this.body.hasEffects( options.setIgnoreBreakStatements() ) ); diff --git a/src/ast/nodes/ForOfStatement.js b/src/ast/nodes/ForOfStatement.js index ba08ddb7092..5445647ba99 100644 --- a/src/ast/nodes/ForOfStatement.js +++ b/src/ast/nodes/ForOfStatement.js @@ -8,12 +8,9 @@ export default class ForOfStatement extends Statement { } hasEffects ( options ) { - return ( - this.included - || this.left && (this.left.hasEffects( options ) || this.left.hasEffectsWhenAssignedAtPath( [], options )) + return this.left && (this.left.hasEffects( options ) || this.left.hasEffectsWhenAssignedAtPath( [], options )) || this.right && this.right.hasEffects( options ) - || this.body.hasEffects( options.setIgnoreBreakStatements() ) - ); + || this.body.hasEffects( options.setIgnoreBreakStatements() ); } includeInBundle () { diff --git a/src/ast/nodes/ForStatement.js b/src/ast/nodes/ForStatement.js index f17c5940da2..22364fab801 100644 --- a/src/ast/nodes/ForStatement.js +++ b/src/ast/nodes/ForStatement.js @@ -3,13 +3,10 @@ import BlockScope from '../scopes/BlockScope'; export default class ForStatement extends Statement { hasEffects ( options ) { - return ( - this.included - || this.init && this.init.hasEffects( options ) + return this.init && this.init.hasEffects( options ) || this.test && this.test.hasEffects( options ) || this.update && this.update.hasEffects( options ) - || this.body.hasEffects( options.setIgnoreBreakStatements() ) - ); + || this.body.hasEffects( options.setIgnoreBreakStatements() ); } initialiseChildren () { diff --git a/src/ast/nodes/NewExpression.js b/src/ast/nodes/NewExpression.js index d7644803949..84d0b8aac95 100644 --- a/src/ast/nodes/NewExpression.js +++ b/src/ast/nodes/NewExpression.js @@ -3,8 +3,7 @@ import CallOptions from '../CallOptions'; export default class NewExpression extends Node { hasEffects ( options ) { - return this.included - || this.arguments.some( child => child.hasEffects( options ) ) + return this.arguments.some( child => child.hasEffects( options ) ) || this.callee.hasEffectsWhenCalledAtPath( [], this._callOptions, options.getHasEffectsWhenCalledOptions() ); } diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index f059a383f8b..0d87ddc3e1b 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -8,8 +8,7 @@ export default class Property extends Node { } hasEffects ( options ) { - return this.included - || this.key.hasEffects( options ) + return this.key.hasEffects( options ) || this.value.hasEffects( options ); } diff --git a/src/ast/nodes/UnaryExpression.js b/src/ast/nodes/UnaryExpression.js index 6090416e282..2542ca5438e 100644 --- a/src/ast/nodes/UnaryExpression.js +++ b/src/ast/nodes/UnaryExpression.js @@ -27,8 +27,7 @@ export default class UnaryExpression extends Node { } hasEffects ( options ) { - return this.included - || this.argument.hasEffects( options ) + return this.argument.hasEffects( options ) || (this.operator === 'delete' && this.argument.hasEffectsWhenAssignedAtPath( [], options )); } diff --git a/src/ast/nodes/UpdateExpression.js b/src/ast/nodes/UpdateExpression.js index e69b248cfdf..61e641f03aa 100644 --- a/src/ast/nodes/UpdateExpression.js +++ b/src/ast/nodes/UpdateExpression.js @@ -13,8 +13,7 @@ export default class UpdateExpression extends Node { } hasEffects ( options ) { - return this.included - || this.argument.hasEffects( options ) + return this.argument.hasEffects( options ) || this.argument.hasEffectsWhenAssignedAtPath( [], options ); } diff --git a/src/ast/nodes/WhileStatement.js b/src/ast/nodes/WhileStatement.js index 27967e0eb39..a743c48a0a5 100644 --- a/src/ast/nodes/WhileStatement.js +++ b/src/ast/nodes/WhileStatement.js @@ -3,8 +3,7 @@ import Statement from './shared/Statement.js'; export default class WhileStatement extends Statement { hasEffects ( options ) { return ( - this.included - || this.test.hasEffects( options ) + this.test.hasEffects( options ) || this.body.hasEffects( options.setIgnoreBreakStatements() ) ); } diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index 75e1aab1174..e28e19ea033 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -8,7 +8,7 @@ export default class FunctionNode extends Node { } hasEffects ( options ) { - return this.included || (this.id && this.id.hasEffects( options )); + return this.id && this.id.hasEffects( options ); } hasEffectsWhenAccessedAtPath ( path, options ) { diff --git a/src/ast/variables/ExportDefaultVariable.js b/src/ast/variables/ExportDefaultVariable.js index 69694d06579..77a6d05ee26 100644 --- a/src/ast/variables/ExportDefaultVariable.js +++ b/src/ast/variables/ExportDefaultVariable.js @@ -33,14 +33,6 @@ export default class ExportDefaultVariable extends LocalVariable { return true; } - includeWithEffects () { - if (!super.includeWithEffects()) { - return false; - } - this.declarations.forEach( declaration => declaration.includeWithEffects() ); - return true; - } - setOriginalVariable ( original ) { this._original = original; } diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index 09e9464859f..f3fa4cc04bf 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -71,14 +71,6 @@ export default class LocalVariable extends Variable { return true; } - includeWithEffects () { - if ( !super.includeWithEffects() ) { - return false; - } - this.declarations.forEach( identifier => identifier.includeWithEffects() ); - return true; - } - someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { return path.length > MAX_PATH_LENGTH || this.assignedExpressions.someAtPath( path, ( relativePath, node ) => diff --git a/src/ast/variables/NamespaceVariable.js b/src/ast/variables/NamespaceVariable.js index 2ae352b6702..1f73d307bd4 100644 --- a/src/ast/variables/NamespaceVariable.js +++ b/src/ast/variables/NamespaceVariable.js @@ -28,14 +28,6 @@ export default class NamespaceVariable extends Variable { return true; } - includeWithEffects () { - if ( !super.includeWithEffects() ) { - return false; - } - forOwn( this.originals, original => original.includeWithEffects() ); - return true; - } - renderBlock ( es, legacy, indentString ) { const members = keys( this.originals ).map( name => { const original = this.originals[ name ]; diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index 6ac161a5792..b4a67fde3b6 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -72,26 +72,6 @@ export default class Variable { return true; } - /** - * Marks this variable's value as being essential to a bundle. This is usually the case when - * a variable is exported or assigned to an exported variable. This also includes a variable. - * This is in contrast to variables which are necessary for the code to be valid but the value - * of which is not important. - * Once a variable is included with effects, it should take care all its declarations are - * included with effects. - * @returns {boolean} - */ - includeWithEffects () { - if ( this.includedWithEffects ) { - return false; - } - this.includedWithEffects = true; - if ( !this.included ) { - this.includeVariable(); - } - return true; - } - /** * @param {String[]} path * @param {CallOptions} callOptions diff --git a/test/form/samples/side-effects-getters-and-setters/_expected/amd.js b/test/form/samples/side-effects-getters-and-setters/_expected/amd.js index dc9afd5b113..aaaa2022694 100644 --- a/test/form/samples/side-effects-getters-and-setters/_expected/amd.js +++ b/test/form/samples/side-effects-getters-and-setters/_expected/amd.js @@ -5,7 +5,8 @@ define(function () { 'use strict'; console.log( 'effect' ); }, get noEffect () { - + const x = 1; + return x; } }; @@ -30,7 +31,9 @@ define(function () { 'use strict'; const retained7 = { foo: () => {}, - get foo () {} + get foo () { + return 1; + } }; retained7.foo(); diff --git a/test/form/samples/side-effects-getters-and-setters/_expected/cjs.js b/test/form/samples/side-effects-getters-and-setters/_expected/cjs.js index 0fd4b7afdd9..9eb023475b9 100644 --- a/test/form/samples/side-effects-getters-and-setters/_expected/cjs.js +++ b/test/form/samples/side-effects-getters-and-setters/_expected/cjs.js @@ -5,7 +5,8 @@ const retained1a = { console.log( 'effect' ); }, get noEffect () { - + const x = 1; + return x; } }; @@ -30,7 +31,9 @@ retained4[ 'eff' + 'ect' ] = 'retained'; const retained7 = { foo: () => {}, - get foo () {} + get foo () { + return 1; + } }; retained7.foo(); diff --git a/test/form/samples/side-effects-getters-and-setters/_expected/es.js b/test/form/samples/side-effects-getters-and-setters/_expected/es.js index e9ffdee2609..8362292d56c 100644 --- a/test/form/samples/side-effects-getters-and-setters/_expected/es.js +++ b/test/form/samples/side-effects-getters-and-setters/_expected/es.js @@ -3,7 +3,8 @@ const retained1a = { console.log( 'effect' ); }, get noEffect () { - + const x = 1; + return x; } }; @@ -28,7 +29,9 @@ retained4[ 'eff' + 'ect' ] = 'retained'; const retained7 = { foo: () => {}, - get foo () {} + get foo () { + return 1; + } }; retained7.foo(); diff --git a/test/form/samples/side-effects-getters-and-setters/_expected/iife.js b/test/form/samples/side-effects-getters-and-setters/_expected/iife.js index fba3f59051d..aec39303724 100644 --- a/test/form/samples/side-effects-getters-and-setters/_expected/iife.js +++ b/test/form/samples/side-effects-getters-and-setters/_expected/iife.js @@ -6,7 +6,8 @@ console.log( 'effect' ); }, get noEffect () { - + const x = 1; + return x; } }; @@ -31,7 +32,9 @@ const retained7 = { foo: () => {}, - get foo () {} + get foo () { + return 1; + } }; retained7.foo(); diff --git a/test/form/samples/side-effects-getters-and-setters/_expected/umd.js b/test/form/samples/side-effects-getters-and-setters/_expected/umd.js index 1f5631453f0..f9d03f83955 100644 --- a/test/form/samples/side-effects-getters-and-setters/_expected/umd.js +++ b/test/form/samples/side-effects-getters-and-setters/_expected/umd.js @@ -9,7 +9,8 @@ console.log( 'effect' ); }, get noEffect () { - + const x = 1; + return x; } }; @@ -34,7 +35,9 @@ const retained7 = { foo: () => {}, - get foo () {} + get foo () { + return 1; + } }; retained7.foo(); diff --git a/test/form/samples/side-effects-getters-and-setters/main.js b/test/form/samples/side-effects-getters-and-setters/main.js index b394f37b2b6..b3ce3d4a9b6 100644 --- a/test/form/samples/side-effects-getters-and-setters/main.js +++ b/test/form/samples/side-effects-getters-and-setters/main.js @@ -4,6 +4,7 @@ const retained1a = { }, get noEffect () { const x = 1; + return x; } }; @@ -14,10 +15,11 @@ const retained1c = retained1a[ 'eff' + 'ect' ]; const removed2a = { get shadowedEffect () { console.log( 'effect' ); + return 1; }, shadowedEffect: true, set shadowedEffect ( value ) { - console.log( 'effect' ); + console.log( value ); } }; @@ -50,7 +52,7 @@ removed5.noEffect = 'removed'; const removed6 = { set shadowedEffect ( value ) { - console.log( 'effect' ); + console.log( value ); }, shadowedEffect: true }; @@ -60,7 +62,9 @@ removed6.missingProp = true; const retained7 = { foo: () => {}, - get foo () {} + get foo () { + return 1; + } }; retained7.foo(); diff --git a/test/test.js b/test/test.js index d24c2f23f9a..30ec6cbf32b 100644 --- a/test/test.js +++ b/test/test.js @@ -1,15 +1,19 @@ -require('source-map-support').install(); -require('console-group').install(); +require( 'source-map-support' ).install(); +require( 'console-group' ).install(); -describe('rollup', function() { - this.timeout(10000); +describe( 'rollup', function () { + this.timeout( 10000 ); - require('./misc/index.js'); - require('./function/index.js'); - require('./form/index.js'); - require('./sourcemaps/index.js'); - require('./incremental/index.js'); - require('./hooks/index.js'); - require('./cli/index.js'); - require('./watch/index.js'); -}); + // To quickly check the effects of potential performance improvements + before( () => console.time( 'Total test time' ) ); + after( () => console.timeEnd( 'Total test time' ) ); + + require( './misc/index.js' ); + require( './function/index.js' ); + require( './form/index.js' ); + require( './sourcemaps/index.js' ); + require( './incremental/index.js' ); + require( './hooks/index.js' ); + require( './cli/index.js' ); + require( './watch/index.js' ); +} ); From 257f3efd57626569ccd67e7bfb0517597d6180be Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Sat, 21 Oct 2017 09:27:26 +0200 Subject: [PATCH 58/76] Handle accessing non-numerical properties of "arguments" --- src/ast/variables/ArgumentsVariable.js | 8 +++++--- .../samples/does-not-fail-for-arguments-test/_config.js | 3 +++ .../samples/does-not-fail-for-arguments-test/main.js | 3 +++ 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 test/function/samples/does-not-fail-for-arguments-test/_config.js create mode 100644 test/function/samples/does-not-fail-for-arguments-test/main.js diff --git a/src/ast/variables/ArgumentsVariable.js b/src/ast/variables/ArgumentsVariable.js index 7ea640b3a9d..ce9d510ed11 100644 --- a/src/ast/variables/ArgumentsVariable.js +++ b/src/ast/variables/ArgumentsVariable.js @@ -2,7 +2,8 @@ import LocalVariable from './LocalVariable'; import { UNKNOWN_ASSIGNMENT } from '../values'; const getParameterVariable = ( path, options ) => - options.getArgumentsVariables()[ path[ 0 ] ] || UNKNOWN_ASSIGNMENT; + (typeof path[ 0 ] === 'number' && options.getArgumentsVariables()[ path[ 0 ] ] ) + || UNKNOWN_ASSIGNMENT; export default class ArgumentsVariable extends LocalVariable { constructor ( parameters ) { @@ -12,8 +13,9 @@ export default class ArgumentsVariable extends LocalVariable { assignExpressionAtPath ( path, expression ) { if ( path.length >= 1 ) { - const parameter = this._parameters[ path[ 0 ] ]; - parameter && parameter.assignExpressionAtPath( path.slice( 1 ), expression ); + if ( typeof path[ 0 ] === 'number' && this._parameters[ path[ 0 ] ] ) { + this._parameters[ path[ 0 ] ].assignExpressionAtPath( path.slice( 1 ), expression ); + } } } diff --git a/test/function/samples/does-not-fail-for-arguments-test/_config.js b/test/function/samples/does-not-fail-for-arguments-test/_config.js new file mode 100644 index 00000000000..b9ad98c8d17 --- /dev/null +++ b/test/function/samples/does-not-fail-for-arguments-test/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'does not fail when non-integer arguments properties are accessed' +}; diff --git a/test/function/samples/does-not-fail-for-arguments-test/main.js b/test/function/samples/does-not-fail-for-arguments-test/main.js new file mode 100644 index 00000000000..2d9f8e62e99 --- /dev/null +++ b/test/function/samples/does-not-fail-for-arguments-test/main.js @@ -0,0 +1,3 @@ +var hasArgsEnumBug = (function() { + return arguments.propertyIsEnumerable('length'); +}()); From b78926990c45527e936aea2e0ae7ef4d25de876c Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Sat, 21 Oct 2017 21:52:48 +0200 Subject: [PATCH 59/76] * Make sure accessing a valid expression at path.length === 0 is not an effect * Fix an issue when non-numeric paths of "arguments" are accessed --- src/ast/nodes/AssignmentExpression.js | 3 ++- src/ast/nodes/CallExpression.js | 3 ++- src/ast/nodes/ConditionalExpression.js | 5 +++-- src/ast/nodes/LogicalExpression.js | 5 +++-- src/ast/nodes/MemberExpression.js | 3 +++ src/ast/nodes/ThisExpression.js | 3 ++- src/ast/variables/ArgumentsVariable.js | 14 ++++++------- test/form/samples/curried-function/T.js | 4 ++++ test/form/samples/curried-function/_config.js | 3 +++ .../samples/curried-function/_expected/amd.js | 17 +++++++++++++++ .../samples/curried-function/_expected/cjs.js | 15 +++++++++++++ .../samples/curried-function/_expected/es.js | 13 ++++++++++++ .../curried-function/_expected/iife.js | 18 ++++++++++++++++ .../samples/curried-function/_expected/umd.js | 21 +++++++++++++++++++ test/form/samples/curried-function/always.js | 8 +++++++ test/form/samples/curried-function/curry1.js | 5 +++++ test/form/samples/curried-function/main.js | 1 + 17 files changed, 126 insertions(+), 15 deletions(-) create mode 100644 test/form/samples/curried-function/T.js create mode 100644 test/form/samples/curried-function/_config.js create mode 100644 test/form/samples/curried-function/_expected/amd.js create mode 100644 test/form/samples/curried-function/_expected/cjs.js create mode 100644 test/form/samples/curried-function/_expected/es.js create mode 100644 test/form/samples/curried-function/_expected/iife.js create mode 100644 test/form/samples/curried-function/_expected/umd.js create mode 100644 test/form/samples/curried-function/always.js create mode 100644 test/form/samples/curried-function/curry1.js create mode 100644 test/form/samples/curried-function/main.js diff --git a/src/ast/nodes/AssignmentExpression.js b/src/ast/nodes/AssignmentExpression.js index 108afcfd582..5db827c55de 100644 --- a/src/ast/nodes/AssignmentExpression.js +++ b/src/ast/nodes/AssignmentExpression.js @@ -12,6 +12,7 @@ export default class AssignmentExpression extends Node { } hasEffectsWhenAccessedAtPath ( path, options ) { - return this.right.hasEffectsWhenAccessedAtPath( path, options ); + return path.length > 0 + && this.right.hasEffectsWhenAccessedAtPath( path, options ); } } diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index 8db1bbdccf8..9f0443313b8 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -29,7 +29,8 @@ export default class CallExpression extends Node { } hasEffectsWhenAccessedAtPath ( path, options ) { - return !options.hasReturnExpressionBeenAccessedAtPath( path, this ) + return path.length > 0 + && !options.hasReturnExpressionBeenAccessedAtPath( path, this ) && this.callee.someReturnExpressionWhenCalledAtPath( [], this._callOptions, innerOptions => node => node.hasEffectsWhenAccessedAtPath( path, innerOptions.addAccessedReturnExpressionAtPath( path, this ) ), options ); } diff --git a/src/ast/nodes/ConditionalExpression.js b/src/ast/nodes/ConditionalExpression.js index 48fe36a5e93..c6adb61c9e7 100644 --- a/src/ast/nodes/ConditionalExpression.js +++ b/src/ast/nodes/ConditionalExpression.js @@ -28,8 +28,9 @@ export default class ConditionalExpression extends Node { } hasEffectsWhenAccessedAtPath ( path, options ) { - return this._someRelevantBranch( node => - node.hasEffectsWhenAccessedAtPath( path, options ) ); + return path.length > 0 + && this._someRelevantBranch( node => + node.hasEffectsWhenAccessedAtPath( path, options ) ); } hasEffectsWhenAssignedAtPath ( path, options ) { diff --git a/src/ast/nodes/LogicalExpression.js b/src/ast/nodes/LogicalExpression.js index 4a34bff84f9..b87eb06a18f 100644 --- a/src/ast/nodes/LogicalExpression.js +++ b/src/ast/nodes/LogicalExpression.js @@ -23,8 +23,9 @@ export default class LogicalExpression extends Node { } hasEffectsWhenAccessedAtPath ( path, options ) { - return this._someRelevantBranch( node => - node.hasEffectsWhenAccessedAtPath( path, options ) ); + return path.length > 0 + && this._someRelevantBranch( node => + node.hasEffectsWhenAccessedAtPath( path, options ) ); } hasEffectsWhenAssignedAtPath ( path, options ) { diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index 16dda7df369..4d11fd21e90 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -93,6 +93,9 @@ export default class MemberExpression extends Node { } hasEffectsWhenAccessedAtPath ( path, options ) { + if (path.length === 0) { + return false; + } if ( this.variable ) { return this.variable.hasEffectsWhenAccessedAtPath( path, options ); } diff --git a/src/ast/nodes/ThisExpression.js b/src/ast/nodes/ThisExpression.js index 3b4a4427ad4..93aa50218f8 100644 --- a/src/ast/nodes/ThisExpression.js +++ b/src/ast/nodes/ThisExpression.js @@ -21,7 +21,8 @@ export default class ThisExpression extends Node { } hasEffectsWhenAccessedAtPath ( path, options ) { - return this.variable.hasEffectsWhenAccessedAtPath( path, options ); + return path.length > 0 + && this.variable.hasEffectsWhenAccessedAtPath( path, options ); } hasEffectsWhenAssignedAtPath ( path, options ) { diff --git a/src/ast/variables/ArgumentsVariable.js b/src/ast/variables/ArgumentsVariable.js index ce9d510ed11..4b5721628f0 100644 --- a/src/ast/variables/ArgumentsVariable.js +++ b/src/ast/variables/ArgumentsVariable.js @@ -2,7 +2,7 @@ import LocalVariable from './LocalVariable'; import { UNKNOWN_ASSIGNMENT } from '../values'; const getParameterVariable = ( path, options ) => - (typeof path[ 0 ] === 'number' && options.getArgumentsVariables()[ path[ 0 ] ] ) + (path[ 0 ] < options.getArgumentsVariables().length && options.getArgumentsVariables()[ path[ 0 ] ] ) || UNKNOWN_ASSIGNMENT; export default class ArgumentsVariable extends LocalVariable { @@ -12,19 +12,17 @@ export default class ArgumentsVariable extends LocalVariable { } assignExpressionAtPath ( path, expression ) { - if ( path.length >= 1 ) { - if ( typeof path[ 0 ] === 'number' && this._parameters[ path[ 0 ] ] ) { + if ( path.length > 0 ) { + if ( path[ 0 ] >= 0 && this._parameters[ path[ 0 ] ] ) { this._parameters[ path[ 0 ] ].assignExpressionAtPath( path.slice( 1 ), expression ); } } } hasEffectsWhenAccessedAtPath ( path, options ) { - if ( path.length < 2 ) { - return false; - } - return getParameterVariable( path, options ) - .hasEffectsWhenAccessedAtPath( path.slice( 1 ), options ); + return path.length > 1 + && getParameterVariable( path, options ) + .hasEffectsWhenAccessedAtPath( path.slice( 1 ), options ); } hasEffectsWhenAssignedAtPath ( path, options ) { diff --git a/test/form/samples/curried-function/T.js b/test/form/samples/curried-function/T.js new file mode 100644 index 00000000000..3fd320de6d3 --- /dev/null +++ b/test/form/samples/curried-function/T.js @@ -0,0 +1,4 @@ +import always from './always'; + +var T = always(true); +export default T; diff --git a/test/form/samples/curried-function/_config.js b/test/form/samples/curried-function/_config.js new file mode 100644 index 00000000000..8b932f491dd --- /dev/null +++ b/test/form/samples/curried-function/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'properly handles a curried function' +}; diff --git a/test/form/samples/curried-function/_expected/amd.js b/test/form/samples/curried-function/_expected/amd.js new file mode 100644 index 00000000000..0c289e306d9 --- /dev/null +++ b/test/form/samples/curried-function/_expected/amd.js @@ -0,0 +1,17 @@ +define(function () { 'use strict'; + + function curry1 ( fn ) { + return function f1 ( a ) { + return fn.apply( this, arguments ); + }; + } + + var always = curry1( function always ( val ) { + return function () { + return val; + }; + } ); + + var T = always(true); + +}); diff --git a/test/form/samples/curried-function/_expected/cjs.js b/test/form/samples/curried-function/_expected/cjs.js new file mode 100644 index 00000000000..128eb256047 --- /dev/null +++ b/test/form/samples/curried-function/_expected/cjs.js @@ -0,0 +1,15 @@ +'use strict'; + +function curry1 ( fn ) { + return function f1 ( a ) { + return fn.apply( this, arguments ); + }; +} + +var always = curry1( function always ( val ) { + return function () { + return val; + }; +} ); + +var T = always(true); diff --git a/test/form/samples/curried-function/_expected/es.js b/test/form/samples/curried-function/_expected/es.js new file mode 100644 index 00000000000..82da171f127 --- /dev/null +++ b/test/form/samples/curried-function/_expected/es.js @@ -0,0 +1,13 @@ +function curry1 ( fn ) { + return function f1 ( a ) { + return fn.apply( this, arguments ); + }; +} + +var always = curry1( function always ( val ) { + return function () { + return val; + }; +} ); + +var T = always(true); diff --git a/test/form/samples/curried-function/_expected/iife.js b/test/form/samples/curried-function/_expected/iife.js new file mode 100644 index 00000000000..70a03376fa3 --- /dev/null +++ b/test/form/samples/curried-function/_expected/iife.js @@ -0,0 +1,18 @@ +(function () { + 'use strict'; + + function curry1 ( fn ) { + return function f1 ( a ) { + return fn.apply( this, arguments ); + }; + } + + var always = curry1( function always ( val ) { + return function () { + return val; + }; + } ); + + var T = always(true); + +}()); diff --git a/test/form/samples/curried-function/_expected/umd.js b/test/form/samples/curried-function/_expected/umd.js new file mode 100644 index 00000000000..bae9178e089 --- /dev/null +++ b/test/form/samples/curried-function/_expected/umd.js @@ -0,0 +1,21 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + function curry1 ( fn ) { + return function f1 ( a ) { + return fn.apply( this, arguments ); + }; + } + + var always = curry1( function always ( val ) { + return function () { + return val; + }; + } ); + + var T = always(true); + +}))); diff --git a/test/form/samples/curried-function/always.js b/test/form/samples/curried-function/always.js new file mode 100644 index 00000000000..bfb84411f77 --- /dev/null +++ b/test/form/samples/curried-function/always.js @@ -0,0 +1,8 @@ +import curry1 from './curry1'; + +var always = curry1( function always ( val ) { + return function () { + return val; + }; +} ); +export default always; diff --git a/test/form/samples/curried-function/curry1.js b/test/form/samples/curried-function/curry1.js new file mode 100644 index 00000000000..8656c49a9f8 --- /dev/null +++ b/test/form/samples/curried-function/curry1.js @@ -0,0 +1,5 @@ +export default function curry1 ( fn ) { + return function f1 ( a ) { + return fn.apply( this, arguments ); + }; +} diff --git a/test/form/samples/curried-function/main.js b/test/form/samples/curried-function/main.js new file mode 100644 index 00000000000..bac30b15713 --- /dev/null +++ b/test/form/samples/curried-function/main.js @@ -0,0 +1 @@ +import './T.js'; From 03c1bf9585a08cf055f97859f74e28137c2951db Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Sat, 21 Oct 2017 22:16:03 +0200 Subject: [PATCH 60/76] Make rollup without tree-shaking a little faster and fix an issue that could lead to a maximum call-stack error --- src/Module.js | 5 ++++- test/form/samples/no-treeshake-conflict/_expected/amd.js | 2 -- test/form/samples/no-treeshake-conflict/_expected/cjs.js | 2 -- test/form/samples/no-treeshake-conflict/_expected/es.js | 2 -- test/form/samples/no-treeshake-conflict/_expected/iife.js | 2 -- test/form/samples/no-treeshake-conflict/_expected/umd.js | 2 -- 6 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/Module.js b/src/Module.js index 61356931d32..bf67e9520e2 100644 --- a/src/Module.js +++ b/src/Module.js @@ -32,7 +32,10 @@ function tryParse ( module, acornOptions ) { } function includeFully ( node ) { - node.includeInBundle(); + node.included = true; + if ( node.variable && !node.variable.included ) { + node.variable.includeVariable(); + } node.eachChild( includeFully ); } diff --git a/test/form/samples/no-treeshake-conflict/_expected/amd.js b/test/form/samples/no-treeshake-conflict/_expected/amd.js index af371b23e5c..301c4950be6 100644 --- a/test/form/samples/no-treeshake-conflict/_expected/amd.js +++ b/test/form/samples/no-treeshake-conflict/_expected/amd.js @@ -4,8 +4,6 @@ define(function () { 'use strict'; something: 'here' }; - other$1; - const other = { somethingElse: 'here' }; diff --git a/test/form/samples/no-treeshake-conflict/_expected/cjs.js b/test/form/samples/no-treeshake-conflict/_expected/cjs.js index 18a42b74169..303709858d9 100644 --- a/test/form/samples/no-treeshake-conflict/_expected/cjs.js +++ b/test/form/samples/no-treeshake-conflict/_expected/cjs.js @@ -4,8 +4,6 @@ const other$1 = { something: 'here' }; -other$1; - const other = { somethingElse: 'here' }; diff --git a/test/form/samples/no-treeshake-conflict/_expected/es.js b/test/form/samples/no-treeshake-conflict/_expected/es.js index ccc4f98d049..5884bd0c8ea 100644 --- a/test/form/samples/no-treeshake-conflict/_expected/es.js +++ b/test/form/samples/no-treeshake-conflict/_expected/es.js @@ -2,8 +2,6 @@ const other$1 = { something: 'here' }; -other$1; - const other = { somethingElse: 'here' }; diff --git a/test/form/samples/no-treeshake-conflict/_expected/iife.js b/test/form/samples/no-treeshake-conflict/_expected/iife.js index 575d0c7c937..278e907160c 100644 --- a/test/form/samples/no-treeshake-conflict/_expected/iife.js +++ b/test/form/samples/no-treeshake-conflict/_expected/iife.js @@ -5,8 +5,6 @@ something: 'here' }; - other$1; - const other = { somethingElse: 'here' }; diff --git a/test/form/samples/no-treeshake-conflict/_expected/umd.js b/test/form/samples/no-treeshake-conflict/_expected/umd.js index 593d2cf72b5..99513c94cc9 100644 --- a/test/form/samples/no-treeshake-conflict/_expected/umd.js +++ b/test/form/samples/no-treeshake-conflict/_expected/umd.js @@ -8,8 +8,6 @@ something: 'here' }; - other$1; - const other = { somethingElse: 'here' }; From a529b156dcca639016f7e63b46d719056be8f742 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Tue, 31 Oct 2017 22:24:36 +0100 Subject: [PATCH 61/76] Bind call parameters to variables to properly detect side-effects involving variables that have been mutated as function arguments. TODO: Arrow functions, binding involving other node types --- src/ast/Node.js | 8 +++++ src/ast/nodes/CallExpression.js | 1 + src/ast/nodes/Identifier.js | 13 +++++-- src/ast/nodes/MemberExpression.js | 15 ++++++-- src/ast/nodes/shared/FunctionNode.js | 6 ++++ src/ast/scopes/ParameterScope.js | 5 +++ src/ast/scopes/Scope.js | 2 +- src/ast/values.js | 1 + src/ast/variables/ArgumentsVariable.js | 4 +-- src/ast/variables/LocalVariable.js | 36 +++++++++++++------ ...laceableInitStructuredAssignmentTracker.js | 16 +++++++-- src/ast/variables/ReplaceableInitVariable.js | 8 +++-- .../variables/StructuredAssignmentTracker.js | 18 ++++++++++ src/ast/variables/Variable.js | 10 +++++- .../_config.js | 3 +- .../side-effects-object-literal-calls/main.js | 8 ++--- .../_config.js | 8 +++++ .../main.js | 14 ++++++++ .../_config.js | 8 +++++ .../main.js | 16 +++++++++ .../_config.js | 8 +++++ .../associate-parameter-mutations-2/main.js | 16 +++++++++ .../associate-parameter-mutations/_config.js | 8 +++++ .../associate-parameter-mutations/main.js | 13 +++++++ 24 files changed, 216 insertions(+), 29 deletions(-) create mode 100644 test/function/samples/associate-method-parameter-mutations-2/_config.js create mode 100644 test/function/samples/associate-method-parameter-mutations-2/main.js create mode 100644 test/function/samples/associate-method-parameter-mutations/_config.js create mode 100644 test/function/samples/associate-method-parameter-mutations/main.js create mode 100644 test/function/samples/associate-parameter-mutations-2/_config.js create mode 100644 test/function/samples/associate-parameter-mutations-2/main.js create mode 100644 test/function/samples/associate-parameter-mutations/_config.js create mode 100644 test/function/samples/associate-parameter-mutations/main.js diff --git a/src/ast/Node.js b/src/ast/Node.js index 9f695318900..4404169d35b 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -44,6 +44,14 @@ export default class Node { */ bindAssignmentAtPath ( path, expression ) {} + /** + * 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 + */ + bindCallAtPath ( path, callOptions ) {} + eachChild ( callback ) { this.keys.forEach( key => { const value = this[ key ]; diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index 9f0443313b8..b4aeae34380 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -21,6 +21,7 @@ export default class CallExpression extends Node { }, this.start ); } } + this.callee.bindCallAtPath( [], this._callOptions ); } hasEffects ( options ) { diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index fb8d13fa49f..6356d64c560 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -3,17 +3,24 @@ import isReference from 'is-reference'; import { UNKNOWN_ASSIGNMENT } from '../values'; export default class Identifier extends Node { - bindNode () { + bindAssignmentAtPath ( path, expression ) { this._bindVariableIfMissing(); + if ( this.variable ) { + this.variable.bindAssignmentAtPath( path, expression ); + } } - bindAssignmentAtPath ( path, expression ) { + bindCallAtPath ( path, callOptions ) { this._bindVariableIfMissing(); if ( this.variable ) { - this.variable.assignExpressionAtPath( path, expression ); + this.variable.bindCallAtPath( path, callOptions ); } } + bindNode () { + this._bindVariableIfMissing(); + } + _bindVariableIfMissing () { if ( !this.variable && isReference( this, this.parent ) ) { this.variable = this.scope.findVariable( this.name ); diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index 4d11fd21e90..f04685ba516 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -81,19 +81,30 @@ export default class MemberExpression extends Node { this.bind(); } if ( this.variable ) { - this.variable.assignExpressionAtPath( path, expression ); + this.variable.bindAssignmentAtPath( path, expression ); } else { this.object.bindAssignmentAtPath( [ this._getPathSegment(), ...path ], expression ); } } + bindCallAtPath ( path, callOptions ) { + if ( !this._bound ) { + this.bind(); + } + if ( this.variable ) { + this.variable.bindCallAtPath( path, callOptions ); + } else { + this.object.bindCallAtPath( [ this._getPathSegment(), ...path ], callOptions ); + } + } + hasEffects ( options ) { return super.hasEffects( options ) || this.object.hasEffectsWhenAccessedAtPath( [ this._getPathSegment() ], options ); } hasEffectsWhenAccessedAtPath ( path, options ) { - if (path.length === 0) { + if ( path.length === 0 ) { return false; } if ( this.variable ) { diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index e28e19ea033..201c7896480 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -3,6 +3,12 @@ import FunctionScope from '../../scopes/FunctionScope'; import VirtualObjectExpression from './VirtualObjectExpression'; export default class FunctionNode extends Node { + bindCallAtPath ( path, { args } ) { + if ( path.length === 0 ) { + this.scope.bindCallArguments( args ); + } + } + bindNode () { this.body.bindImplicitReturnExpressionToScope(); } diff --git a/src/ast/scopes/ParameterScope.js b/src/ast/scopes/ParameterScope.js index b1be9e3c020..e0cf6d6a98d 100644 --- a/src/ast/scopes/ParameterScope.js +++ b/src/ast/scopes/ParameterScope.js @@ -21,6 +21,11 @@ export default class ParameterScope extends Scope { return variable; } + bindCallArguments ( args ) { + this._parameters.forEach( ( parameter, index ) => + parameter.bindInitialization( args[ index ] || UNKNOWN_ASSIGNMENT ) ); + } + getOptionsWithReplacedParameters ( args, options ) { let newOptions = options; this._parameters.forEach( ( parameter, index ) => diff --git a/src/ast/scopes/Scope.js b/src/ast/scopes/Scope.js index 93230ad20ed..56e3263c4dc 100644 --- a/src/ast/scopes/Scope.js +++ b/src/ast/scopes/Scope.js @@ -27,7 +27,7 @@ export default class Scope { if ( this.variables[ name ] ) { const variable = this.variables[ name ]; variable.addDeclaration( identifier ); - options.init && variable.assignExpressionAtPath( [], options.init ); + options.init && variable.bindAssignmentAtPath( [], options.init ); } else { this.variables[ name ] = new LocalVariable( identifier.name, identifier, options.init || new UndefinedIdentifier() ); } diff --git a/src/ast/values.js b/src/ast/values.js index 131b1a4dbb4..04ce1d31a7d 100644 --- a/src/ast/values.js +++ b/src/ast/values.js @@ -3,6 +3,7 @@ export const UNKNOWN_VALUE = { toString: () => '[[UNKNOWN]]' }; export const UNKNOWN_ASSIGNMENT = { type: 'UNKNOWN', bindAssignmentAtPath: () => {}, + bindCallAtPath: () => {}, hasEffectsWhenAccessedAtPath: path => path.length > 0, hasEffectsWhenAssignedAtPath: () => true, hasEffectsWhenCalledAtPath: () => true, diff --git a/src/ast/variables/ArgumentsVariable.js b/src/ast/variables/ArgumentsVariable.js index 4b5721628f0..4212a47c7a4 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; } - assignExpressionAtPath ( path, expression ) { + bindAssignmentAtPath ( path, expression ) { if ( path.length > 0 ) { if ( path[ 0 ] >= 0 && this._parameters[ path[ 0 ] ] ) { - this._parameters[ path[ 0 ] ].assignExpressionAtPath( path.slice( 1 ), expression ); + this._parameters[ path[ 0 ] ].bindAssignmentAtPath( path.slice( 1 ), expression ); } } } diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index f3fa4cc04bf..49e8af18830 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -10,23 +10,38 @@ export default class LocalVariable extends Variable { this.isReassigned = false; this.exportName = null; this.declarations = new Set( declarator ? [ declarator ] : null ); - this.assignedExpressions = new StructuredAssignmentTracker(); - init && this.assignedExpressions.addAtPath( [], init ); + this.boundExpressions = new StructuredAssignmentTracker(); + init && this.boundExpressions.addAtPath( [], init ); + this.boundCalls = new StructuredAssignmentTracker(); } addDeclaration ( identifier ) { this.declarations.add( identifier ); } - assignExpressionAtPath ( path, expression ) { - if ( path.length > MAX_PATH_LENGTH || this.assignedExpressions.hasAtPath( path, expression ) ) return; - this.assignedExpressions.addAtPath( path, expression ); + bindAssignmentAtPath ( path, expression ) { + 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 ); + } + } ); if ( path.length > 0 ) { - this.assignedExpressions.forEachAtPath( path.slice( 0, -1 ), ( relativePath, node ) => + this.boundExpressions.forEachAtPath( path.slice( 0, -1 ), ( relativePath, node ) => node.bindAssignmentAtPath( [ ...relativePath, ...path.slice( -1 ) ], expression ) ); } else { this.isReassigned = true; } + this.boundCalls.forEachAtPath( path, ( relativePath, callOptions ) => + expression.bindCallAtPath( relativePath, callOptions ) ); + } + + bindCallAtPath ( path, callOptions ) { + 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 ) ); } getName ( es ) { @@ -38,7 +53,7 @@ export default class LocalVariable extends Variable { hasEffectsWhenAccessedAtPath ( path, options ) { return path.length > MAX_PATH_LENGTH - || this.assignedExpressions.someAtPath( path, ( relativePath, node ) => + || this.boundExpressions.someAtPath( path, ( relativePath, node ) => !options.hasNodeBeenAccessedAtPath( relativePath, node ) && node.hasEffectsWhenAccessedAtPath( relativePath, options .addAccessedNodeAtPath( relativePath, node ) ) ); @@ -47,7 +62,7 @@ export default class LocalVariable extends Variable { hasEffectsWhenAssignedAtPath ( path, options ) { return this.included || path.length > MAX_PATH_LENGTH - || this.assignedExpressions.someAtPath( path, ( relativePath, node ) => + || this.boundExpressions.someAtPath( path, ( relativePath, node ) => relativePath.length > 0 && !options.hasNodeBeenAssignedAtPath( relativePath, node ) && node.hasEffectsWhenAssignedAtPath( relativePath, options @@ -56,7 +71,8 @@ export default class LocalVariable extends Variable { hasEffectsWhenCalledAtPath ( path, callOptions, options ) { return path.length > MAX_PATH_LENGTH - || this.assignedExpressions.someAtPath( path, ( relativePath, node ) => + || (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 ) ) @@ -73,7 +89,7 @@ export default class LocalVariable extends Variable { someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { return path.length > MAX_PATH_LENGTH - || this.assignedExpressions.someAtPath( path, ( relativePath, node ) => + || this.boundExpressions.someAtPath( path, ( relativePath, node ) => !callOptions.hasNodeBeenCalledAtPath( relativePath, node ) && node.someReturnExpressionWhenCalledAtPath( relativePath, callOptions .addCalledNodeAtPath( relativePath, node ), predicateFunction, options ) ); diff --git a/src/ast/variables/ReplaceableInitStructuredAssignmentTracker.js b/src/ast/variables/ReplaceableInitStructuredAssignmentTracker.js index 611489d1ca6..96d28f0189e 100644 --- a/src/ast/variables/ReplaceableInitStructuredAssignmentTracker.js +++ b/src/ast/variables/ReplaceableInitStructuredAssignmentTracker.js @@ -1,9 +1,21 @@ import StructuredAssignmentTracker from './StructuredAssignmentTracker'; +import LocalVariable from './LocalVariable'; export default class ReplaceableInitStructuredAssignmentTracker extends StructuredAssignmentTracker { - constructor ( init ) { + constructor () { super(); - this._init = init; + this._init = new LocalVariable( {}, null, null ); + } + + addAtPath ( path, assignment ) { + if ( path.length > 0 ) { + this._init.bindAssignmentAtPath( path, assignment ); + } + super.addAtPath( path, assignment ); + } + + addInit ( assignment ) { + this._init.bindAssignmentAtPath( [], assignment ); } forEachAtPath ( path, callback ) { diff --git a/src/ast/variables/ReplaceableInitVariable.js b/src/ast/variables/ReplaceableInitVariable.js index dcde1e57f7c..e77150f237c 100644 --- a/src/ast/variables/ReplaceableInitVariable.js +++ b/src/ast/variables/ReplaceableInitVariable.js @@ -5,7 +5,11 @@ import { UNKNOWN_ASSIGNMENT } from '../values'; export default class ReplaceableInitVariable extends LocalVariable { constructor ( name, declarator ) { super( name, declarator, null ); - this.assignedExpressions = new ReplaceableInitStructuredAssignmentTracker( UNKNOWN_ASSIGNMENT ); + this.boundExpressions = new ReplaceableInitStructuredAssignmentTracker(); + } + + bindInitialization ( expression ) { + this.boundExpressions.addInit( expression ); } getName () { @@ -33,6 +37,6 @@ export default class ReplaceableInitVariable extends LocalVariable { } _updateInit ( options ) { - this.assignedExpressions.setInit( options.getReplacedVariableInit( this ) || UNKNOWN_ASSIGNMENT ); + this.boundExpressions.setInit( options.getReplacedVariableInit( this ) || UNKNOWN_ASSIGNMENT ); } } diff --git a/src/ast/variables/StructuredAssignmentTracker.js b/src/ast/variables/StructuredAssignmentTracker.js index 4336dddc0c7..d591dc9865e 100644 --- a/src/ast/variables/StructuredAssignmentTracker.js +++ b/src/ast/variables/StructuredAssignmentTracker.js @@ -39,6 +39,24 @@ export default class StructuredAssignmentTracker { } } + forEachAssignedToPath ( path, callback ) { + if ( path.length > 0 ) { + const [ nextPath, ...remainingPath ] = path; + if ( this._assignments.has( nextPath ) ) { + this._assignments.get( nextPath ).forEachAssignedToPath( remainingPath, callback ); + } + } else { + this._assignments.forEach( ( assignment, subPath ) => { + if ( subPath === SET_KEY ) { + assignment.forEach( subAssignment => callback( [], subAssignment ) ); + } else { + assignment.forEachAssignedToPath( [], + ( relativePath, assignment ) => callback( [ subPath, ...relativePath ], assignment ) ); + } + } ); + } + } + hasAtPath ( path, assignment ) { if ( path.length === 0 ) { return this._assignments.get( SET_KEY ).has( assignment ); diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index b4a67fde3b6..8c9dc7c2f1b 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -20,7 +20,15 @@ export default class Variable { * @param {String[]} path * @param {Node} expression */ - assignExpressionAtPath ( path, expression ) {} + bindAssignmentAtPath ( path, expression ) {} + + /** + * Binds a call to this node. Necessary to be able to bind arguments + * to parameters. + * @param {String[]} path + * @param {CallOptions} callOptions + */ + bindCallAtPath ( path, callOptions ) {} /** * @returns {String} diff --git a/test/form/samples/side-effects-object-literal-calls/_config.js b/test/form/samples/side-effects-object-literal-calls/_config.js index 423a24bd345..ffc52c259c1 100644 --- a/test/form/samples/side-effects-object-literal-calls/_config.js +++ b/test/form/samples/side-effects-object-literal-calls/_config.js @@ -1,4 +1,3 @@ module.exports = { - description: 'detects side-effects when mutating object literals', - options: { name: 'bundle' } + description: 'detects side-effects when mutating object literals' }; 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 434d04938a2..82c05d7d7ad 100644 --- a/test/form/samples/side-effects-object-literal-calls/main.js +++ b/test/form/samples/side-effects-object-literal-calls/main.js @@ -1,6 +1,10 @@ const removed1 = { x: () => {} }; removed1.x(); +const removed2 = { x: { y: () => {} } }; +removed2.x.y = function () {}; +removed2.x.y(); + const retained1 = { x: () => {} }; retained1.y(); @@ -8,10 +12,6 @@ const retained2 = { x: () => {} }; retained2.x = {}; retained2.x(); -const removed2 = { x: { y: () => {} } }; -removed2.x.y = function () {}; -removed2.x.y(); - const retained3 = { x: { y: () => console.log('effect') } }; const retained4 = { x: { y: {} } }; retained4.x = retained3.x; diff --git a/test/function/samples/associate-method-parameter-mutations-2/_config.js b/test/function/samples/associate-method-parameter-mutations-2/_config.js new file mode 100644 index 00000000000..76501c9aac0 --- /dev/null +++ b/test/function/samples/associate-method-parameter-mutations-2/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'Associates method parameters with their call arguments with regard to mutations', + exports: function ( exports ) { + assert.equal( exports.bar, 'present' ); + } +}; diff --git a/test/function/samples/associate-method-parameter-mutations-2/main.js b/test/function/samples/associate-method-parameter-mutations-2/main.js new file mode 100644 index 00000000000..29cbedcedc7 --- /dev/null +++ b/test/function/samples/associate-method-parameter-mutations-2/main.js @@ -0,0 +1,14 @@ +const foo = { mightBeExported: {} }; +const exported = {}; + +function Bar () {} + +Bar.prototype.assignExported = function ( obj ) { + obj.mightBeExported = exported; +}; + +const bar = new Bar(); +bar.assignExported( foo ); +foo.mightBeExported.bar = 'present'; + +export default exported; diff --git a/test/function/samples/associate-method-parameter-mutations/_config.js b/test/function/samples/associate-method-parameter-mutations/_config.js new file mode 100644 index 00000000000..6af3b80ab95 --- /dev/null +++ b/test/function/samples/associate-method-parameter-mutations/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'Associates method parameters with their call arguments with regard to mutations', + exports: function ( exports ) { + assert.equal( exports.baz, 'present' ); + } +}; diff --git a/test/function/samples/associate-method-parameter-mutations/main.js b/test/function/samples/associate-method-parameter-mutations/main.js new file mode 100644 index 00000000000..fd6850f0ef7 --- /dev/null +++ b/test/function/samples/associate-method-parameter-mutations/main.js @@ -0,0 +1,16 @@ +const foo = { noEffect: () => {} }; +const bar = {}; + +function Baz () {} + +Baz.prototype.addEffect = function ( obj ) { + obj.noEffect = () => { + bar.baz = 'present'; + }; +}; + +const baz = new Baz(); +baz.addEffect( foo ); +foo.noEffect(); + +export default bar; diff --git a/test/function/samples/associate-parameter-mutations-2/_config.js b/test/function/samples/associate-parameter-mutations-2/_config.js new file mode 100644 index 00000000000..e1e23899c8a --- /dev/null +++ b/test/function/samples/associate-parameter-mutations-2/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'Associates function parameters with their call arguments with regard to mutations', + exports: function ( exports ) { + assert.equal( exports.bar, 'present' ); + } +}; diff --git a/test/function/samples/associate-parameter-mutations-2/main.js b/test/function/samples/associate-parameter-mutations-2/main.js new file mode 100644 index 00000000000..f06f8e91628 --- /dev/null +++ b/test/function/samples/associate-parameter-mutations-2/main.js @@ -0,0 +1,16 @@ +const foo = { mightBeExported: {} }; +const exported = {}; + +function assignExported ( obj ) { + obj.mightBeExported = exported; +} + +// Maybe we can keep the second line if instead of crippling +// assignments, we properly associate assignments +// because foo included => assignment is kept +// maybe it is also enough to keep calls to included vars if +// assignment works +assignExported( foo ); +foo.mightBeExported.bar = 'present'; + +export default exported; diff --git a/test/function/samples/associate-parameter-mutations/_config.js b/test/function/samples/associate-parameter-mutations/_config.js new file mode 100644 index 00000000000..df6111b74db --- /dev/null +++ b/test/function/samples/associate-parameter-mutations/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'Associates function parameters with their call arguments with regard to mutations', + exports: function ( exports ) { + assert.equal( exports.baz, 'present' ); + } +}; diff --git a/test/function/samples/associate-parameter-mutations/main.js b/test/function/samples/associate-parameter-mutations/main.js new file mode 100644 index 00000000000..9ad9b2aa8cc --- /dev/null +++ b/test/function/samples/associate-parameter-mutations/main.js @@ -0,0 +1,13 @@ +const foo = { noEffect: () => {} }; +const bar = {}; + +function addEffect ( obj ) { + obj.noEffect = () => { + bar.baz = 'present'; + }; +} + +addEffect( foo ); +foo.noEffect(); + +export default bar; From 382f5bdd2f387a9e7846ef5b09d89d22bab6a672 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Wed, 1 Nov 2017 09:55:02 +0100 Subject: [PATCH 62/76] Add another test for reverse assignment resolution --- .../resolve-unordered-assignments/_config.js | 8 ++++++++ .../samples/resolve-unordered-assignments/main.js | 15 +++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 test/function/samples/resolve-unordered-assignments/_config.js create mode 100644 test/function/samples/resolve-unordered-assignments/main.js diff --git a/test/function/samples/resolve-unordered-assignments/_config.js b/test/function/samples/resolve-unordered-assignments/_config.js new file mode 100644 index 00000000000..28c45880da0 --- /dev/null +++ b/test/function/samples/resolve-unordered-assignments/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'Assignments should be correctly bound independent of their order', + exports: function ( exports ) { + assert.equal( exports.baz, 'present' ); + } +}; diff --git a/test/function/samples/resolve-unordered-assignments/main.js b/test/function/samples/resolve-unordered-assignments/main.js new file mode 100644 index 00000000000..13a7e3d6989 --- /dev/null +++ b/test/function/samples/resolve-unordered-assignments/main.js @@ -0,0 +1,15 @@ +const foo1 = { noEffect: () => {} }; +let foo2 = { noEffect: () => {} }; +const bar = {}; + +assignFoo(); +foo2.noEffect = () => { + bar.baz = 'present'; +}; +foo1.noEffect(); + +function assignFoo () { + foo2 = foo1; +} + +export default bar; From 4d1f946de3acbc7588189403dcfc87ccb442632a Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Wed, 1 Nov 2017 10:07:32 +0100 Subject: [PATCH 63/76] Bind arrow function call parameters --- src/ast/nodes/ArrowFunctionExpression.js | 6 ++++++ .../_config.js | 8 ++++++++ .../main.js | 13 +++++++++++++ .../_config.js | 0 .../main.js | 11 +++++++++++ .../_config.js | 0 .../main.js | 0 .../associate-parameter-mutations-2/main.js | 16 ---------------- 8 files changed, 38 insertions(+), 16 deletions(-) create mode 100644 test/function/samples/associate-arrow-function-parameter-mutations/_config.js create mode 100644 test/function/samples/associate-arrow-function-parameter-mutations/main.js rename test/function/samples/{associate-parameter-mutations-2 => associate-function-parameter-mutations-2}/_config.js (100%) create mode 100644 test/function/samples/associate-function-parameter-mutations-2/main.js rename test/function/samples/{associate-parameter-mutations => associate-function-parameter-mutations}/_config.js (100%) rename test/function/samples/{associate-parameter-mutations => associate-function-parameter-mutations}/main.js (100%) delete mode 100644 test/function/samples/associate-parameter-mutations-2/main.js diff --git a/src/ast/nodes/ArrowFunctionExpression.js b/src/ast/nodes/ArrowFunctionExpression.js index ce2d178aad7..3bbd88a14ba 100644 --- a/src/ast/nodes/ArrowFunctionExpression.js +++ b/src/ast/nodes/ArrowFunctionExpression.js @@ -3,6 +3,12 @@ import Scope from '../scopes/Scope'; import ReturnValueScope from '../scopes/ReturnValueScope'; export default class ArrowFunctionExpression extends Node { + bindCallAtPath ( path, { args } ) { + if ( path.length === 0 ) { + this.scope.bindCallArguments( args ); + } + } + bindNode () { this.body.bindImplicitReturnExpressionToScope ? this.body.bindImplicitReturnExpressionToScope() diff --git a/test/function/samples/associate-arrow-function-parameter-mutations/_config.js b/test/function/samples/associate-arrow-function-parameter-mutations/_config.js new file mode 100644 index 00000000000..fbb6c6d6b5c --- /dev/null +++ b/test/function/samples/associate-arrow-function-parameter-mutations/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'Associates arrow function parameters with their call arguments with regard to mutations', + exports: function ( exports ) { + assert.equal( exports.baz, 'present' ); + } +}; diff --git a/test/function/samples/associate-arrow-function-parameter-mutations/main.js b/test/function/samples/associate-arrow-function-parameter-mutations/main.js new file mode 100644 index 00000000000..d6d5688cde4 --- /dev/null +++ b/test/function/samples/associate-arrow-function-parameter-mutations/main.js @@ -0,0 +1,13 @@ +const foo = { noEffect: () => {} }; +const bar = {}; + +const addEffect = obj => { + obj.noEffect = () => { + bar.baz = 'present'; + }; +}; + +addEffect( foo ); +foo.noEffect(); + +export default bar; diff --git a/test/function/samples/associate-parameter-mutations-2/_config.js b/test/function/samples/associate-function-parameter-mutations-2/_config.js similarity index 100% rename from test/function/samples/associate-parameter-mutations-2/_config.js rename to test/function/samples/associate-function-parameter-mutations-2/_config.js diff --git a/test/function/samples/associate-function-parameter-mutations-2/main.js b/test/function/samples/associate-function-parameter-mutations-2/main.js new file mode 100644 index 00000000000..bf4ad3ed9d5 --- /dev/null +++ b/test/function/samples/associate-function-parameter-mutations-2/main.js @@ -0,0 +1,11 @@ +const foo = { mightBeExported: {} }; +const exported = {}; + +function assignExported ( obj ) { + obj.mightBeExported = exported; +} + +assignExported( foo ); +foo.mightBeExported.bar = 'present'; + +export default exported; diff --git a/test/function/samples/associate-parameter-mutations/_config.js b/test/function/samples/associate-function-parameter-mutations/_config.js similarity index 100% rename from test/function/samples/associate-parameter-mutations/_config.js rename to test/function/samples/associate-function-parameter-mutations/_config.js diff --git a/test/function/samples/associate-parameter-mutations/main.js b/test/function/samples/associate-function-parameter-mutations/main.js similarity index 100% rename from test/function/samples/associate-parameter-mutations/main.js rename to test/function/samples/associate-function-parameter-mutations/main.js diff --git a/test/function/samples/associate-parameter-mutations-2/main.js b/test/function/samples/associate-parameter-mutations-2/main.js deleted file mode 100644 index f06f8e91628..00000000000 --- a/test/function/samples/associate-parameter-mutations-2/main.js +++ /dev/null @@ -1,16 +0,0 @@ -const foo = { mightBeExported: {} }; -const exported = {}; - -function assignExported ( obj ) { - obj.mightBeExported = exported; -} - -// Maybe we can keep the second line if instead of crippling -// assignments, we properly associate assignments -// because foo included => assignment is kept -// maybe it is also enough to keep calls to included vars if -// assignment works -assignExported( foo ); -foo.mightBeExported.bar = 'present'; - -export default exported; From 80dff0fa7c36d13471f404ccf108bacabe5ce355 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Wed, 1 Nov 2017 10:42:54 +0100 Subject: [PATCH 64/76] Bind calls involving conditional and logical expressions --- src/ast/Node.js | 3 +-- src/ast/nodes/ConditionalExpression.js | 24 +++++++++++++------ src/ast/nodes/LogicalExpression.js | 22 +++++++++++++++++ .../_config.js | 8 +++++++ .../main.js | 11 +++++++++ 5 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 test/function/samples/associate-parameter-mutations-across-other-expressions/_config.js create mode 100644 test/function/samples/associate-parameter-mutations-across-other-expressions/main.js diff --git a/src/ast/Node.js b/src/ast/Node.js index 4404169d35b..4adc160e3f4 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -1,9 +1,8 @@ /* eslint-disable no-unused-vars */ import { locate } from 'locate-character'; -import { UNKNOWN_VALUE } from './values.js'; +import { UNKNOWN_ASSIGNMENT, UNKNOWN_VALUE } from './values.js'; import ExecutionPathOptions from './ExecutionPathOptions'; -import { UNKNOWN_ASSIGNMENT } from './values'; export default class Node { constructor () { diff --git a/src/ast/nodes/ConditionalExpression.js b/src/ast/nodes/ConditionalExpression.js index c6adb61c9e7..23db5feb950 100644 --- a/src/ast/nodes/ConditionalExpression.js +++ b/src/ast/nodes/ConditionalExpression.js @@ -3,16 +3,15 @@ import { UNKNOWN_VALUE } from '../values.js'; export default class ConditionalExpression extends Node { bindAssignmentAtPath ( path, expression ) { - if ( this.testValue === UNKNOWN_VALUE ) { - this.consequent.bindAssignmentAtPath( path, expression ); - this.alternate.bindAssignmentAtPath( path, expression ); - } else { - this.testValue - ? this.consequent.bindAssignmentAtPath( path, expression ) - : this.alternate.bindAssignmentAtPath( path, expression ); + if ( path.length > 0 ) { + this._forEachRelevantBranch( node => node.bindAssignmentAtPath( path, expression ) ); } } + bindCallAtPath ( path, callOptions ) { + this._forEachRelevantBranch( node => node.bindCallAtPath( path, callOptions ) ); + } + getValue () { const testValue = this.test.getValue(); if ( testValue === UNKNOWN_VALUE ) return UNKNOWN_VALUE; @@ -90,6 +89,17 @@ export default class ConditionalExpression extends Node { node.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ) ); } + _forEachRelevantBranch ( callback ) { + if ( this.testValue === UNKNOWN_VALUE ) { + callback( this.consequent ); + callback( this.alternate ); + } else { + this.testValue + ? callback( this.consequent ) + : callback( this.alternate ); + } + } + _someRelevantBranch ( predicateFunction ) { return this.testValue === UNKNOWN_VALUE ? predicateFunction( this.consequent ) || predicateFunction( this.alternate ) diff --git a/src/ast/nodes/LogicalExpression.js b/src/ast/nodes/LogicalExpression.js index b87eb06a18f..6aadf848426 100644 --- a/src/ast/nodes/LogicalExpression.js +++ b/src/ast/nodes/LogicalExpression.js @@ -2,6 +2,16 @@ 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 ) ); + } + } + + bindCallAtPath ( path, callOptions ) { + this._forEachRelevantBranch( node => node.bindCallAtPath( path, callOptions ) ); + } + getValue () { const leftValue = this.left.getValue(); if ( leftValue === UNKNOWN_VALUE ) return UNKNOWN_VALUE; @@ -43,6 +53,18 @@ export default class LogicalExpression extends Node { node.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ) ); } + _forEachRelevantBranch ( callback ) { + const leftValue = this.left.getValue(); + if ( leftValue === UNKNOWN_VALUE ) { + callback( this.left ); + callback( this.right ); + } else if ( (leftValue && this.operator === '||') || (!leftValue && this.operator === '&&') ) { + callback( this.left ); + } else { + callback( this.right ); + } + } + _someRelevantBranch ( predicateFunction ) { const leftValue = this.left.getValue(); if ( leftValue === UNKNOWN_VALUE ) { diff --git a/test/function/samples/associate-parameter-mutations-across-other-expressions/_config.js b/test/function/samples/associate-parameter-mutations-across-other-expressions/_config.js new file mode 100644 index 00000000000..80e59f6aa6d --- /dev/null +++ b/test/function/samples/associate-parameter-mutations-across-other-expressions/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'Associates parameters with their call arguments across other expressions', + exports: function ( exports ) { + assert.equal( exports.bar, 'present' ); + } +}; diff --git a/test/function/samples/associate-parameter-mutations-across-other-expressions/main.js b/test/function/samples/associate-parameter-mutations-across-other-expressions/main.js new file mode 100644 index 00000000000..1c7271b268b --- /dev/null +++ b/test/function/samples/associate-parameter-mutations-across-other-expressions/main.js @@ -0,0 +1,11 @@ +const foo = { mightBeExported: {} }; +const exported = {}; + +function assignExported ( obj ) { + obj.mightBeExported = exported; +} + +(Math.random() < 0.5 ? true && assignExported : false || assignExported)( foo ); +foo.mightBeExported.bar = 'present'; + +export default exported; From 2fb0c34326b63d83946f5b7ec0663ab7a4dbe790 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Wed, 1 Nov 2017 10:56:29 +0100 Subject: [PATCH 65/76] Bind calls to object expression members --- src/ast/nodes/ObjectExpression.js | 8 ++++++++ src/ast/nodes/Property.js | 4 ++++ .../_config.js | 8 ++++++++ .../main.js | 12 ++++++++++++ 4 files changed, 32 insertions(+) create mode 100644 test/function/samples/associate-object-expression-parameter-mutations/_config.js create mode 100644 test/function/samples/associate-object-expression-parameter-mutations/main.js diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index d0e184f1c69..b6146c57dfd 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -13,6 +13,14 @@ export default class ObjectExpression extends Node { 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 ) ); + } + _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 0d87ddc3e1b..34f92de1cb0 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -7,6 +7,10 @@ export default class Property extends Node { this.value.bindAssignmentAtPath( path, expression ); } + bindCallAtPath ( path, callOptions ) { + this.value.bindCallAtPath( path, callOptions ); + } + hasEffects ( options ) { return this.key.hasEffects( options ) || this.value.hasEffects( options ); diff --git a/test/function/samples/associate-object-expression-parameter-mutations/_config.js b/test/function/samples/associate-object-expression-parameter-mutations/_config.js new file mode 100644 index 00000000000..9d29f4c142b --- /dev/null +++ b/test/function/samples/associate-object-expression-parameter-mutations/_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-parameter-mutations/main.js b/test/function/samples/associate-object-expression-parameter-mutations/main.js new file mode 100644 index 00000000000..bd4967a118b --- /dev/null +++ b/test/function/samples/associate-object-expression-parameter-mutations/main.js @@ -0,0 +1,12 @@ +const foo = { mightBeExported: {} }; +const exported = {}; +const assigner = { + assignExported ( obj ) { + obj.mightBeExported = exported; + } +}; + +assigner.assignExported( foo ); +foo.mightBeExported.bar = 'present'; + +export default exported; From 503b8aaf95dbe5118268a38c92534d7dbf7999ad Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Wed, 1 Nov 2017 11:19:23 +0100 Subject: [PATCH 66/76] Bind calls to ES5 constructors --- src/ast/nodes/NewExpression.js | 6 +++++- .../_config.js | 8 ++++++++ .../main.js | 11 +++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 test/function/samples/associate-es5-constructor-parameter-mutations/_config.js create mode 100644 test/function/samples/associate-es5-constructor-parameter-mutations/main.js diff --git a/src/ast/nodes/NewExpression.js b/src/ast/nodes/NewExpression.js index 84d0b8aac95..e538abcd36a 100644 --- a/src/ast/nodes/NewExpression.js +++ b/src/ast/nodes/NewExpression.js @@ -2,6 +2,10 @@ import Node from '../Node.js'; import CallOptions from '../CallOptions'; export default class NewExpression extends Node { + bindNode () { + this.callee.bindCallAtPath( [], this._callOptions ); + } + hasEffects ( options ) { return this.arguments.some( child => child.hasEffects( options ) ) || this.callee.hasEffectsWhenCalledAtPath( [], this._callOptions, options.getHasEffectsWhenCalledOptions() ); @@ -12,6 +16,6 @@ export default class NewExpression extends Node { } initialiseNode () { - this._callOptions = CallOptions.create( { withNew: true } ); + this._callOptions = CallOptions.create( { withNew: true, args: this.arguments } ); } } diff --git a/test/function/samples/associate-es5-constructor-parameter-mutations/_config.js b/test/function/samples/associate-es5-constructor-parameter-mutations/_config.js new file mode 100644 index 00000000000..ad2ad3811af --- /dev/null +++ b/test/function/samples/associate-es5-constructor-parameter-mutations/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'Associates ES5 constructor parameters with their call arguments', + exports: function ( exports ) { + assert.equal( exports.bar, 'present' ); + } +}; diff --git a/test/function/samples/associate-es5-constructor-parameter-mutations/main.js b/test/function/samples/associate-es5-constructor-parameter-mutations/main.js new file mode 100644 index 00000000000..d96e29e9c73 --- /dev/null +++ b/test/function/samples/associate-es5-constructor-parameter-mutations/main.js @@ -0,0 +1,11 @@ +const foo = { mightBeExported: {} }; +const exported = {}; + +function AssignExported ( obj ) { + obj.mightBeExported = exported; +} + +const bar = new AssignExported( foo ); +foo.mightBeExported.bar = 'present'; + +export default exported; From 63e44536ae24b18d53676bd99082cd14c67f96a5 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Wed, 1 Nov 2017 12:02:49 +0100 Subject: [PATCH 67/76] Bind calls to ES6 constructors --- src/ast/nodes/ClassBody.js | 6 ++++++ src/ast/nodes/MethodDefinition.js | 12 ++++++++---- src/ast/nodes/shared/ClassNode.js | 5 +++++ .../_config.js | 8 ++++++++ .../main.js | 13 +++++++++++++ 5 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 test/function/samples/associate-es6-constructor-parameter-mutations/_config.js create mode 100644 test/function/samples/associate-es6-constructor-parameter-mutations/main.js diff --git a/src/ast/nodes/ClassBody.js b/src/ast/nodes/ClassBody.js index dab3873e2bd..d1e6e49ef16 100644 --- a/src/ast/nodes/ClassBody.js +++ b/src/ast/nodes/ClassBody.js @@ -1,6 +1,12 @@ import Node from '../Node'; export default class ClassBody extends Node { + bindCallAtPath ( path, callOptions ) { + if ( path.length === 0 && this.classConstructor ) { + this.classConstructor.bindCallAtPath( path, callOptions ); + } + } + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { if ( path.length > 0 ) { return true; diff --git a/src/ast/nodes/MethodDefinition.js b/src/ast/nodes/MethodDefinition.js index e14ad731f18..a856621797a 100644 --- a/src/ast/nodes/MethodDefinition.js +++ b/src/ast/nodes/MethodDefinition.js @@ -1,14 +1,18 @@ import Node from '../Node'; export default class MethodDefinition extends Node { + bindCallAtPath ( path, callOptions ) { + if ( path.length === 0 ) { + this.value.bindCallAtPath( path, callOptions ); + } + } + hasEffects ( options ) { return this.key.hasEffects( options ); } hasEffectsWhenCalledAtPath ( path, callOptions, options ) { - if ( path.length > 0 ) { - return true; - } - return this.value.hasEffectsWhenCalledAtPath( [], callOptions, options ); + return path.length > 0 + || this.value.hasEffectsWhenCalledAtPath( [], callOptions, options ); } } diff --git a/src/ast/nodes/shared/ClassNode.js b/src/ast/nodes/shared/ClassNode.js index c7fe7773d2f..e6efd63c6c9 100644 --- a/src/ast/nodes/shared/ClassNode.js +++ b/src/ast/nodes/shared/ClassNode.js @@ -2,6 +2,11 @@ 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 ); + } + hasEffectsWhenAccessedAtPath ( path ) { return path.length > 1; } diff --git a/test/function/samples/associate-es6-constructor-parameter-mutations/_config.js b/test/function/samples/associate-es6-constructor-parameter-mutations/_config.js new file mode 100644 index 00000000000..ad2ad3811af --- /dev/null +++ b/test/function/samples/associate-es6-constructor-parameter-mutations/_config.js @@ -0,0 +1,8 @@ +var assert = require( 'assert' ); + +module.exports = { + description: 'Associates ES5 constructor parameters with their call arguments', + exports: function ( exports ) { + assert.equal( exports.bar, 'present' ); + } +}; diff --git a/test/function/samples/associate-es6-constructor-parameter-mutations/main.js b/test/function/samples/associate-es6-constructor-parameter-mutations/main.js new file mode 100644 index 00000000000..1e3e360dbfd --- /dev/null +++ b/test/function/samples/associate-es6-constructor-parameter-mutations/main.js @@ -0,0 +1,13 @@ +const foo = { mightBeExported: {} }; +const exported = {}; + +class AssignExported { + constructor ( obj ) { + obj.mightBeExported = exported; + } +} + +const bar = new AssignExported( foo ); +foo.mightBeExported.bar = 'present'; + +export default exported; From dcfe5d4bd79b9588cc2c0576ec98c107f5e7434a Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Thu, 2 Nov 2017 17:15:47 +0100 Subject: [PATCH 68/76] 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; From e2daaec95e0821bdf24e3b4e53b48c4c96c25b56 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Fri, 3 Nov 2017 09:18:34 +0100 Subject: [PATCH 69/76] Support getter return values and setter parameters. --- src/ast/nodes/CallExpression.js | 6 +++ src/ast/nodes/ObjectExpression.js | 34 +++++++------- src/ast/nodes/Property.js | 44 ++++++++++++++----- .../_config.js | 8 ++++ .../main.js | 11 +++++ .../_config.js | 8 ++++ .../associate-getter-return-values-2/main.js | 8 ++++ .../associate-getter-return-values/_config.js | 8 ++++ .../associate-getter-return-values/main.js | 8 ++++ .../associate-setter-parameters/_config.js | 8 ++++ .../associate-setter-parameters/main.js | 12 +++++ 11 files changed, 125 insertions(+), 30 deletions(-) create mode 100644 test/function/samples/associate-function-return-values-3/_config.js create mode 100644 test/function/samples/associate-function-return-values-3/main.js create mode 100644 test/function/samples/associate-getter-return-values-2/_config.js create mode 100644 test/function/samples/associate-getter-return-values-2/main.js create mode 100644 test/function/samples/associate-getter-return-values/_config.js create mode 100644 test/function/samples/associate-getter-return-values/main.js create mode 100644 test/function/samples/associate-setter-parameters/_config.js create mode 100644 test/function/samples/associate-setter-parameters/main.js 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; From 52f46847cc1654381a7e4270f67162a506da459f Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Sat, 4 Nov 2017 13:47:50 +0100 Subject: [PATCH 70/76] * Add ExecutionPathOptions to all bind calls * Make sure bindxyz and hasEffectsWhenxyz are as symmetric as possible so that in the future, we could make this the same function and possibly remove assignment/call binding from the bind phase * Add the calling node to CallOptions to have a break-off condition that works for well return value assertions This all resolves a maximum call stack error --- src/ast/CallOptions.js | 22 ++------ src/ast/Node.js | 35 +++++++------ src/ast/nodes/ArrayPattern.js | 11 ++-- src/ast/nodes/ArrowFunctionExpression.js | 12 ++--- src/ast/nodes/AssignmentExpression.js | 3 +- src/ast/nodes/AssignmentPattern.js | 14 ++--- src/ast/nodes/CallExpression.js | 37 ++++++------- src/ast/nodes/ClassBody.js | 14 +++-- src/ast/nodes/ConditionalExpression.js | 25 ++++----- src/ast/nodes/ForOfStatement.js | 3 +- src/ast/nodes/Identifier.js | 21 ++++---- src/ast/nodes/LogicalExpression.js | 19 ++++--- src/ast/nodes/MemberExpression.js | 18 +++---- src/ast/nodes/MethodDefinition.js | 7 ++- src/ast/nodes/NewExpression.js | 5 +- src/ast/nodes/ObjectExpression.js | 24 +++++---- src/ast/nodes/ObjectPattern.js | 11 ++-- src/ast/nodes/Property.js | 41 ++++++++------- src/ast/nodes/RestElement.js | 8 +-- src/ast/nodes/TaggedTemplateExpression.js | 2 +- src/ast/nodes/UnaryExpression.js | 3 +- src/ast/nodes/UpdateExpression.js | 3 +- src/ast/nodes/VariableDeclaration.js | 3 +- src/ast/nodes/VariableDeclarator.js | 4 +- src/ast/nodes/shared/ClassNode.js | 6 +-- src/ast/nodes/shared/FunctionNode.js | 7 ++- src/ast/scopes/ReturnValueScope.js | 5 +- src/ast/scopes/Scope.js | 1 - src/ast/variables/ArgumentsVariable.js | 4 +- src/ast/variables/LocalVariable.js | 52 +++++++++---------- .../variables/StructuredAssignmentTracker.js | 12 +++++ src/ast/variables/Variable.js | 10 ++-- .../_config.js | 2 +- .../samples/build-promise-chain/_config.js | 5 ++ .../samples/build-promise-chain/main.js | 4 ++ 35 files changed, 230 insertions(+), 223 deletions(-) create mode 100644 test/function/samples/build-promise-chain/_config.js create mode 100644 test/function/samples/build-promise-chain/main.js 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( () => {} ); From 5132d3b7ce61dd9feff9811db8a69f5817d61f89 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Sat, 4 Nov 2017 15:58:38 +0100 Subject: [PATCH 71/76] * Fix some missing options * Replace UndefinedIdentifier -> UNKNOWN_ASSIGNMENT to keep assignment sets smaller --- src/ast/nodes/BlockStatement.js | 4 ++-- src/ast/nodes/UnaryExpression.js | 5 ++--- src/ast/nodes/shared/FunctionNode.js | 5 +++++ src/ast/nodes/shared/UndefinedIdentifier.js | 19 ------------------- src/ast/scopes/ModuleScope.js | 4 ++-- src/ast/scopes/Scope.js | 4 ++-- src/ast/values.js | 3 ++- src/ast/variables/ArgumentsVariable.js | 9 ++++----- ...laceableInitStructuredAssignmentTracker.js | 5 +++-- .../variables/StructuredAssignmentTracker.js | 12 ------------ 10 files changed, 22 insertions(+), 48 deletions(-) delete mode 100644 src/ast/nodes/shared/UndefinedIdentifier.js diff --git a/src/ast/nodes/BlockStatement.js b/src/ast/nodes/BlockStatement.js index 1aad2de6573..c344510567c 100644 --- a/src/ast/nodes/BlockStatement.js +++ b/src/ast/nodes/BlockStatement.js @@ -1,12 +1,12 @@ import Statement from './shared/Statement.js'; import BlockScope from '../scopes/BlockScope'; -import UndefinedIdentifier from './shared/UndefinedIdentifier'; +import { UNKNOWN_ASSIGNMENT } from '../values'; export default class BlockStatement extends Statement { bindImplicitReturnExpressionToScope () { const lastStatement = this.body[ this.body.length - 1 ]; if ( !lastStatement || lastStatement.type !== 'ReturnStatement' ) { - this.scope.addReturnExpression( new UndefinedIdentifier() ); + this.scope.addReturnExpression( UNKNOWN_ASSIGNMENT ); } } diff --git a/src/ast/nodes/UnaryExpression.js b/src/ast/nodes/UnaryExpression.js index 4e50e2b875b..0b79f177472 100644 --- a/src/ast/nodes/UnaryExpression.js +++ b/src/ast/nodes/UnaryExpression.js @@ -1,6 +1,5 @@ import Node from '../Node.js'; -import { UNKNOWN_VALUE } from '../values'; -import UndefinedIdentifier from './shared/UndefinedIdentifier'; +import { UNKNOWN_ASSIGNMENT, UNKNOWN_VALUE } from '../values'; import ExecutionPathOptions from '../ExecutionPathOptions'; const operators = { @@ -16,7 +15,7 @@ const operators = { export default class UnaryExpression extends Node { bindNode () { if ( this.operator === 'delete' ) { - this.argument.bindAssignmentAtPath( [], new UndefinedIdentifier(), ExecutionPathOptions.create() ); + this.argument.bindAssignmentAtPath( [], UNKNOWN_ASSIGNMENT, ExecutionPathOptions.create() ); } } diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index b3116c07d8a..3a7d1a16470 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -51,6 +51,11 @@ export default class FunctionNode extends Node { || this.body.hasEffects( innerOptions ); } + includeInBundle () { + this.scope.variables.arguments.includeVariable(); + return super.includeInBundle(); + } + initialiseNode () { this.prototypeObject = new VirtualObjectExpression(); } diff --git a/src/ast/nodes/shared/UndefinedIdentifier.js b/src/ast/nodes/shared/UndefinedIdentifier.js deleted file mode 100644 index 46a561a09f6..00000000000 --- a/src/ast/nodes/shared/UndefinedIdentifier.js +++ /dev/null @@ -1,19 +0,0 @@ -import Node from '../../Node'; - -export default class UndefinedIdentifier extends Node { - hasEffects () { - return false; - } - - hasEffectsWhenAccessedAtPath ( path ) { - return path.length > 0; - } - - hasEffectsWhenAssignedAtPath ( path ) { - return path.length > 0; - } - - toString () { - return 'undefined'; - } -} diff --git a/src/ast/scopes/ModuleScope.js b/src/ast/scopes/ModuleScope.js index ebdc5c58cfd..6d9c472e997 100644 --- a/src/ast/scopes/ModuleScope.js +++ b/src/ast/scopes/ModuleScope.js @@ -2,7 +2,7 @@ import { forOwn } from '../../utils/object.js'; import relativeId from '../../utils/relativeId.js'; import Scope from './Scope.js'; import LocalVariable from '../variables/LocalVariable'; -import UndefinedIdentifier from '../nodes/shared/UndefinedIdentifier'; +import { UNKNOWN_ASSIGNMENT } from '../values'; export default class ModuleScope extends Scope { constructor ( module ) { @@ -12,7 +12,7 @@ export default class ModuleScope extends Scope { } ); this.module = module; - this.variables.this = new LocalVariable( 'this', null, new UndefinedIdentifier() ); + this.variables.this = new LocalVariable( 'this', null, UNKNOWN_ASSIGNMENT ); } deshadow ( names ) { diff --git a/src/ast/scopes/Scope.js b/src/ast/scopes/Scope.js index 28e9a409fcf..cc8548e6b67 100644 --- a/src/ast/scopes/Scope.js +++ b/src/ast/scopes/Scope.js @@ -1,7 +1,7 @@ import { blank, keys } from '../../utils/object.js'; import LocalVariable from '../variables/LocalVariable'; import ExportDefaultVariable from '../variables/ExportDefaultVariable'; -import UndefinedIdentifier from '../nodes/shared/UndefinedIdentifier'; +import { UNKNOWN_ASSIGNMENT } from '../values'; export default class Scope { constructor ( options = {} ) { @@ -28,7 +28,7 @@ export default class Scope { variable.addDeclaration( identifier ); options.init && variable.bindAssignmentAtPath( [], options.init ); } else { - this.variables[ name ] = new LocalVariable( identifier.name, identifier, options.init || new UndefinedIdentifier() ); + this.variables[ name ] = new LocalVariable( identifier.name, identifier, options.init || UNKNOWN_ASSIGNMENT ); } return this.variables[ name ]; } diff --git a/src/ast/values.js b/src/ast/values.js index 04ce1d31a7d..8d97af1d64f 100644 --- a/src/ast/values.js +++ b/src/ast/values.js @@ -4,8 +4,9 @@ export const UNKNOWN_ASSIGNMENT = { type: 'UNKNOWN', bindAssignmentAtPath: () => {}, bindCallAtPath: () => {}, + forEachReturnExpressionWhenCalledAtPath: () => {}, hasEffectsWhenAccessedAtPath: path => path.length > 0, - hasEffectsWhenAssignedAtPath: () => true, + hasEffectsWhenAssignedAtPath: path => path.length > 0, hasEffectsWhenCalledAtPath: () => true, someReturnExpressionWhenCalledAtPath: () => true, toString: () => '[[UNKNOWN]]' diff --git a/src/ast/variables/ArgumentsVariable.js b/src/ast/variables/ArgumentsVariable.js index 4199623a79d..0dd1e0c7b68 100644 --- a/src/ast/variables/ArgumentsVariable.js +++ b/src/ast/variables/ArgumentsVariable.js @@ -26,11 +26,10 @@ export default class ArgumentsVariable extends LocalVariable { } hasEffectsWhenAssignedAtPath ( path, options ) { - if ( path.length === 0 ) { - return true; - } - return getParameterVariable( path, options ) - .hasEffectsWhenAssignedAtPath( path.slice( 1 ), options ); + return path.length === 0 + || this.included + || getParameterVariable( path, options ) + .hasEffectsWhenAssignedAtPath( path.slice( 1 ), options ); } hasEffectsWhenCalledAtPath ( path, callOptions, options ) { diff --git a/src/ast/variables/ReplaceableInitStructuredAssignmentTracker.js b/src/ast/variables/ReplaceableInitStructuredAssignmentTracker.js index 96d28f0189e..83ed45bb8ba 100644 --- a/src/ast/variables/ReplaceableInitStructuredAssignmentTracker.js +++ b/src/ast/variables/ReplaceableInitStructuredAssignmentTracker.js @@ -1,5 +1,6 @@ import StructuredAssignmentTracker from './StructuredAssignmentTracker'; import LocalVariable from './LocalVariable'; +import ExecutionPathOptions from '../ExecutionPathOptions'; export default class ReplaceableInitStructuredAssignmentTracker extends StructuredAssignmentTracker { constructor () { @@ -9,13 +10,13 @@ export default class ReplaceableInitStructuredAssignmentTracker extends Structur addAtPath ( path, assignment ) { if ( path.length > 0 ) { - this._init.bindAssignmentAtPath( path, assignment ); + this._init.bindAssignmentAtPath( path, assignment, ExecutionPathOptions.create() ); } super.addAtPath( path, assignment ); } addInit ( assignment ) { - this._init.bindAssignmentAtPath( [], assignment ); + this._init.bindAssignmentAtPath( [], assignment, ExecutionPathOptions.create() ); } forEachAtPath ( path, callback ) { diff --git a/src/ast/variables/StructuredAssignmentTracker.js b/src/ast/variables/StructuredAssignmentTracker.js index d1b2500460f..d591dc9865e 100644 --- a/src/ast/variables/StructuredAssignmentTracker.js +++ b/src/ast/variables/StructuredAssignmentTracker.js @@ -69,18 +69,6 @@ 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 ) ) From eb1c5810ff198c84571865f740af8c41c52c7277 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Sat, 4 Nov 2017 16:51:13 +0100 Subject: [PATCH 72/76] Simplify tracked assignments when something unknown is assigned. --- src/ast/variables/StructuredAssignmentTracker.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/ast/variables/StructuredAssignmentTracker.js b/src/ast/variables/StructuredAssignmentTracker.js index d591dc9865e..f84fefb10e0 100644 --- a/src/ast/variables/StructuredAssignmentTracker.js +++ b/src/ast/variables/StructuredAssignmentTracker.js @@ -1,14 +1,23 @@ +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 ] ) ] ] ); + export default class StructuredAssignmentTracker { constructor () { this._assignments = new Map( [ [ SET_KEY, new Set() ] ] ); } addAtPath ( path, assignment ) { + if ( this._assignments === UNKNOWN_ASSIGNMENTS ) return; if ( path.length === 0 ) { - this._assignments.get( SET_KEY ).add( assignment ); + if ( assignment === UNKNOWN_ASSIGNMENT ) { + this._assignments = UNKNOWN_ASSIGNMENTS; + } else { + this._assignments.get( SET_KEY ).add( assignment ); + } } else { const [ nextPath, ...remainingPath ] = path; if ( !this._assignments.has( nextPath ) ) { From 75f36d15024a9fa0c8edfeb5217527836ebd818e Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Sat, 4 Nov 2017 22:28:35 +0100 Subject: [PATCH 73/76] Remove parameter binding logic as it has abysmal performance in its current form. This also makes bindCallAtPath obsolete again. Instead we always assume that parameters have unknown values when determining side effects. The hack that calling a member of an included variable is always a side effect also needs to stay in place until we find a way to determine with absolute certainty it was not overridden e.g. by assuming unknown function calls always mutate their parameters. --- src/ast/Node.js | 9 --- src/ast/nodes/ArrowFunctionExpression.js | 10 +--- src/ast/nodes/CallExpression.js | 7 --- src/ast/nodes/ClassBody.js | 6 -- src/ast/nodes/ConditionalExpression.js | 4 -- src/ast/nodes/Identifier.js | 6 -- src/ast/nodes/LogicalExpression.js | 4 -- src/ast/nodes/MemberExpression.js | 9 --- src/ast/nodes/MethodDefinition.js | 5 -- src/ast/nodes/NewExpression.js | 4 -- src/ast/nodes/ObjectExpression.js | 8 --- src/ast/nodes/Property.js | 17 +----- src/ast/nodes/shared/ClassNode.js | 5 -- src/ast/nodes/shared/FunctionNode.js | 6 -- src/ast/scopes/FunctionScope.js | 4 +- src/ast/scopes/ParameterScope.js | 14 ----- src/ast/scopes/ReturnValueScope.js | 6 +- src/ast/values.js | 1 - src/ast/variables/LocalVariable.js | 12 +--- src/ast/variables/Variable.js | 9 --- .../_expected/amd.js | 36 +++++------ .../_expected/cjs.js | 36 +++++------ .../_expected/es.js | 36 +++++------ .../_expected/iife.js | 36 +++++------ .../_expected/umd.js | 36 +++++------ .../arrow-function-call-parameters/main.js | 60 +++++-------------- .../function-call-parameters/_expected/amd.js | 1 - .../function-call-parameters/_expected/cjs.js | 1 - .../function-call-parameters/_expected/es.js | 1 - .../_expected/iife.js | 1 - .../function-call-parameters/_expected/umd.js | 1 - .../samples/function-call-parameters/main.js | 22 ------- 32 files changed, 113 insertions(+), 300 deletions(-) diff --git a/src/ast/Node.js b/src/ast/Node.js index 6a87b511032..caf3fbead86 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -31,15 +31,6 @@ export default class Node { */ 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, options ) {} - /** * Override to control on which children "bind" is called. */ diff --git a/src/ast/nodes/ArrowFunctionExpression.js b/src/ast/nodes/ArrowFunctionExpression.js index 43f77b5174a..f7c1c2ff6df 100644 --- a/src/ast/nodes/ArrowFunctionExpression.js +++ b/src/ast/nodes/ArrowFunctionExpression.js @@ -3,11 +3,6 @@ import Scope from '../scopes/Scope'; import ReturnValueScope from '../scopes/ReturnValueScope'; export default class ArrowFunctionExpression extends Node { - bindCallAtPath ( path, { args } ) { - path.length === 0 - && this.scope.bindCallArguments( args ); - } - bindNode () { this.body.bindImplicitReturnExpressionToScope ? this.body.bindImplicitReturnExpressionToScope() @@ -35,9 +30,8 @@ export default class ArrowFunctionExpression extends Node { if ( path.length > 0 ) { return true; } - const innerOptions = this.scope.getOptionsWithReplacedParameters( callOptions.args, options ); - return this.params.some( param => param.hasEffects( innerOptions ) ) - || this.body.hasEffects( innerOptions ); + return this.params.some( param => param.hasEffects( options ) ) + || this.body.hasEffects( options ); } initialiseChildren () { diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index 189c5f7dde7..e51257b4e0c 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -9,12 +9,6 @@ export default class CallExpression extends Node { node.bindAssignmentAtPath( path, expression, innerOptions.addAssignedReturnExpressionAtPath( path, this ) ), options ); } - 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 () { if ( this.callee.type === 'Identifier' ) { const variable = this.scope.findVariable( this.callee.name ); @@ -34,7 +28,6 @@ export default class CallExpression extends Node { }, this.start ); } } - this.callee.bindCallAtPath( [], this._callOptions, ExecutionPathOptions.create() ); } forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) { diff --git a/src/ast/nodes/ClassBody.js b/src/ast/nodes/ClassBody.js index 6ece0dd9644..1bce2c0aec6 100644 --- a/src/ast/nodes/ClassBody.js +++ b/src/ast/nodes/ClassBody.js @@ -1,12 +1,6 @@ import Node from '../Node'; export default class ClassBody extends Node { - 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; diff --git a/src/ast/nodes/ConditionalExpression.js b/src/ast/nodes/ConditionalExpression.js index 1dc2b3af395..178f4bc655e 100644 --- a/src/ast/nodes/ConditionalExpression.js +++ b/src/ast/nodes/ConditionalExpression.js @@ -7,10 +7,6 @@ export default class ConditionalExpression extends Node { && this._forEachRelevantBranch( node => node.bindAssignmentAtPath( path, expression, options ) ); } - bindCallAtPath ( path, callOptions, options ) { - this._forEachRelevantBranch( node => node.bindCallAtPath( path, callOptions, options ) ); - } - forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) { this._forEachRelevantBranch( node => node.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback, options ) ); } diff --git a/src/ast/nodes/Identifier.js b/src/ast/nodes/Identifier.js index b256d56ba6f..153b4c0e3f4 100644 --- a/src/ast/nodes/Identifier.js +++ b/src/ast/nodes/Identifier.js @@ -9,12 +9,6 @@ export default class Identifier extends Node { && this.variable.bindAssignmentAtPath( path, expression, options ); } - bindCallAtPath ( path, callOptions, options ) { - this._bindVariableIfMissing(); - this.variable - && this.variable.bindCallAtPath( path, callOptions, options ); - } - bindNode () { this._bindVariableIfMissing(); } diff --git a/src/ast/nodes/LogicalExpression.js b/src/ast/nodes/LogicalExpression.js index fd66e15e6de..3048df12d88 100644 --- a/src/ast/nodes/LogicalExpression.js +++ b/src/ast/nodes/LogicalExpression.js @@ -7,10 +7,6 @@ export default class LogicalExpression extends Node { && this._forEachRelevantBranch( node => node.bindAssignmentAtPath( path, expression, options ) ); } - bindCallAtPath ( path, callOptions, options ) { - this._forEachRelevantBranch( node => node.bindCallAtPath( path, callOptions, options ) ); - } - forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) { this._forEachRelevantBranch( node => node.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback, options ) ); } diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index a781f75f60a..c417dcd3fdf 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -85,15 +85,6 @@ export default class MemberExpression extends Node { } } - bindCallAtPath ( path, callOptions, options ) { - if ( !this._bound ) this.bind(); - if ( this.variable ) { - this.variable.bindCallAtPath( path, callOptions, options ); - } else { - this.object.bindCallAtPath( [ this._getPathSegment(), ...path ], callOptions, options ); - } - } - forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) { if ( !this._bound ) this.bind(); if ( this.variable ) { diff --git a/src/ast/nodes/MethodDefinition.js b/src/ast/nodes/MethodDefinition.js index f192f1f1b66..2e64d5e5f92 100644 --- a/src/ast/nodes/MethodDefinition.js +++ b/src/ast/nodes/MethodDefinition.js @@ -1,11 +1,6 @@ import Node from '../Node'; export default class MethodDefinition extends Node { - bindCallAtPath ( path, callOptions, options ) { - path.length === 0 - && this.value.bindCallAtPath( path, callOptions, options ); - } - hasEffects ( options ) { return this.key.hasEffects( options ); } diff --git a/src/ast/nodes/NewExpression.js b/src/ast/nodes/NewExpression.js index 4717bca4213..e19781c6e6a 100644 --- a/src/ast/nodes/NewExpression.js +++ b/src/ast/nodes/NewExpression.js @@ -3,10 +3,6 @@ import CallOptions from '../CallOptions'; import ExecutionPathOptions from '../ExecutionPathOptions'; export default class NewExpression extends Node { - bindNode () { - this.callee.bindCallAtPath( [], this._callOptions, ExecutionPathOptions.create() ); - } - hasEffects ( options ) { return this.arguments.some( child => child.hasEffects( options ) ) || this.callee.hasEffectsWhenCalledAtPath( [], this._callOptions, options.getHasEffectsWhenCalledOptions() ); diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index 901b307b011..a4c33728bf8 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -15,14 +15,6 @@ export default class ObjectExpression extends Node { && property.bindAssignmentAtPath( path.slice( 1 ), expression, options ) ); } - bindCallAtPath ( path, callOptions, options ) { - if ( path.length === 0 ) return; - - 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, options ) { if ( path.length === 0 ) return; diff --git a/src/ast/nodes/Property.js b/src/ast/nodes/Property.js index af37a8c341e..d71b688e8c0 100644 --- a/src/ast/nodes/Property.js +++ b/src/ast/nodes/Property.js @@ -8,28 +8,13 @@ export default class Property extends Node { 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 ], caller: this } ), options ); - } else { + } else if ( this.kind !== 'set' ) { this.value.bindAssignmentAtPath( path, expression, options ); } } - bindCallAtPath ( path, callOptions, options ) { - if ( this.kind === 'get' ) { - 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, options ); - } - } - forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) { if ( this.kind === 'get' ) { - this.value.bindCallAtPath( [], this._accessorCallOptions, options ); this.value.forEachReturnExpressionWhenCalledAtPath( [], this._accessorCallOptions, innerOptions => node => node.forEachReturnExpressionWhenCalledAtPath( path, callOptions, callback, innerOptions ), options ); } else { diff --git a/src/ast/nodes/shared/ClassNode.js b/src/ast/nodes/shared/ClassNode.js index e61848ad841..c7fe7773d2f 100644 --- a/src/ast/nodes/shared/ClassNode.js +++ b/src/ast/nodes/shared/ClassNode.js @@ -2,11 +2,6 @@ import Node from '../../Node.js'; import Scope from '../../scopes/Scope'; export default class ClassNode extends Node { - bindCallAtPath ( path, callOptions, options ) { - this.body.bindCallAtPath( path, callOptions, options ); - this.superClass && this.superClass.bindCallAtPath( path, callOptions, options ); - } - hasEffectsWhenAccessedAtPath ( path ) { return path.length > 1; } diff --git a/src/ast/nodes/shared/FunctionNode.js b/src/ast/nodes/shared/FunctionNode.js index 3a7d1a16470..4ff27a3d3a4 100644 --- a/src/ast/nodes/shared/FunctionNode.js +++ b/src/ast/nodes/shared/FunctionNode.js @@ -3,12 +3,6 @@ import FunctionScope from '../../scopes/FunctionScope'; import VirtualObjectExpression from './VirtualObjectExpression'; export default class FunctionNode extends Node { - bindCallAtPath ( path, { args } ) { - if ( path.length === 0 ) { - this.scope.bindCallArguments( args ); - } - } - bindNode () { this.body.bindImplicitReturnExpressionToScope(); } diff --git a/src/ast/scopes/FunctionScope.js b/src/ast/scopes/FunctionScope.js index 5ebd5726771..f97abb261ec 100644 --- a/src/ast/scopes/FunctionScope.js +++ b/src/ast/scopes/FunctionScope.js @@ -16,8 +16,8 @@ export default class FunctionScope extends ReturnValueScope { } getOptionsWhenCalledWith ( { args, withNew }, options ) { - return super.getOptionsWithReplacedParameters( args, options ) + return options .replaceVariableInit( this.variables.this, withNew ? new VirtualObjectExpression() : UNKNOWN_ASSIGNMENT ) - .setArgumentsVariables( args.map( ( parameter, index ) => super.getParameterVariables()[index] || parameter) ); + .setArgumentsVariables( args.map( ( parameter, index ) => super.getParameterVariables()[ index ] || parameter ) ); } } diff --git a/src/ast/scopes/ParameterScope.js b/src/ast/scopes/ParameterScope.js index e0cf6d6a98d..247a0875863 100644 --- a/src/ast/scopes/ParameterScope.js +++ b/src/ast/scopes/ParameterScope.js @@ -1,6 +1,5 @@ import Scope from './Scope'; import ParameterVariable from '../variables/ParameterVariable'; -import { UNKNOWN_ASSIGNMENT } from '../values'; export default class ParameterScope extends Scope { constructor ( options = {} ) { @@ -21,19 +20,6 @@ export default class ParameterScope extends Scope { return variable; } - bindCallArguments ( args ) { - this._parameters.forEach( ( parameter, index ) => - parameter.bindInitialization( args[ index ] || UNKNOWN_ASSIGNMENT ) ); - } - - getOptionsWithReplacedParameters ( args, options ) { - let newOptions = options; - this._parameters.forEach( ( parameter, index ) => - newOptions = newOptions.replaceVariableInit( parameter, args[ index ] || UNKNOWN_ASSIGNMENT ) - ); - return newOptions; - } - getParameterVariables () { return this._parameters; } diff --git a/src/ast/scopes/ReturnValueScope.js b/src/ast/scopes/ReturnValueScope.js index b628c063e3b..497ff7fc895 100644 --- a/src/ast/scopes/ReturnValueScope.js +++ b/src/ast/scopes/ReturnValueScope.js @@ -11,12 +11,10 @@ export default class ReturnValueScope extends ParameterScope { } forEachReturnExpressionWhenCalled ( callOptions, callback, options ) { - const innerOptions = this.getOptionsWithReplacedParameters( callOptions.args, options ); - this._returnExpressions.forEach( callback( innerOptions ) ); + this._returnExpressions.forEach( callback( options ) ); } someReturnExpressionWhenCalled ( callOptions, predicateFunction, options ) { - const innerOptions = this.getOptionsWithReplacedParameters( callOptions.args, options ); - return Array.from( this._returnExpressions ).some( predicateFunction( innerOptions ) ); + return Array.from( this._returnExpressions ).some( predicateFunction( options ) ); } } diff --git a/src/ast/values.js b/src/ast/values.js index 8d97af1d64f..41207c9cfee 100644 --- a/src/ast/values.js +++ b/src/ast/values.js @@ -3,7 +3,6 @@ export const UNKNOWN_VALUE = { toString: () => '[[UNKNOWN]]' }; export const UNKNOWN_ASSIGNMENT = { type: 'UNKNOWN', bindAssignmentAtPath: () => {}, - bindCallAtPath: () => {}, forEachReturnExpressionWhenCalledAtPath: () => {}, hasEffectsWhenAccessedAtPath: path => path.length > 0, hasEffectsWhenAssignedAtPath: path => path.length > 0, diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index ceda4c8a3c9..8142f18f112 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -2,7 +2,7 @@ import Variable from './Variable'; import StructuredAssignmentTracker from './StructuredAssignmentTracker'; // To avoid infinite recursions -const MAX_PATH_LENGTH = 8; +const MAX_PATH_LENGTH = 6; export default class LocalVariable extends Variable { constructor ( name, declarator, init ) { @@ -12,7 +12,6 @@ export default class LocalVariable extends Variable { this.declarations = new Set( declarator ? [ declarator ] : null ); this.boundExpressions = new StructuredAssignmentTracker(); init && this.boundExpressions.addAtPath( [], init ); - this.boundCalls = new StructuredAssignmentTracker(); } addDeclaration ( identifier ) { @@ -32,15 +31,6 @@ export default class LocalVariable extends Variable { } else { this.isReassigned = true; } - this.boundCalls.forEachAtPath( path, ( relativePath, callOptions ) => - expression.bindCallAtPath( relativePath, callOptions, options ) ); - } - - 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, options ) ); } forEachReturnExpressionWhenCalledAtPath ( path, callOptions, callback, options ) { diff --git a/src/ast/variables/Variable.js b/src/ast/variables/Variable.js index 27f98d89b0a..1c443a494eb 100644 --- a/src/ast/variables/Variable.js +++ b/src/ast/variables/Variable.js @@ -23,15 +23,6 @@ export default class Variable { */ 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, options ) {} - /** * @param {String[]} path * @param {CallOptions} callOptions diff --git a/test/form/samples/arrow-function-call-parameters/_expected/amd.js b/test/form/samples/arrow-function-call-parameters/_expected/amd.js index 3e609d33362..3e20fc5fe5f 100644 --- a/test/form/samples/arrow-function-call-parameters/_expected/amd.js +++ b/test/form/samples/arrow-function-call-parameters/_expected/amd.js @@ -1,33 +1,33 @@ define(function () { 'use strict'; - const callArg2 = arg => arg(); - callArg2( () => console.log( 'effect' ) ); + const callArg = arg => arg(); + callArg( () => console.log( 'effect' ) ); - const assignArg2 = arg => arg.foo.bar = 1; - assignArg2( {} ); + const assignArg = arg => arg.foo.bar = 1; + assignArg( {} ); + + const returnArg = arg => arg; + returnArg( () => console.log( 'effect' ) )(); const returnArg2 = arg => arg; - returnArg2( () => console.log( 'effect' ) )(); + returnArg2( {} ).foo.bar = 1; - const returnArg4 = arg => arg; - returnArg4( {} ).foo.bar = 1; + const returnArg3 = arg => arg; + returnArg3( () => () => console.log( 'effect' ) )()(); - const returnArg6 = arg => arg; - returnArg6( () => () => console.log( 'effect' ) )()(); + const returnArgReturn = arg => arg(); + returnArgReturn( () => () => console.log( 'effect' ) )(); const returnArgReturn2 = arg => arg(); - returnArgReturn2( () => () => console.log( 'effect' ) )(); + returnArgReturn2( () => ({}) ).foo.bar = 1; - const returnArgReturn4 = arg => arg(); - returnArgReturn4( () => ({}) ).foo.bar = 1; + const returnArgReturn3 = arg => arg(); + returnArgReturn3( () => () => () => console.log( 'effect' ) )()(); - const returnArgReturn6 = arg => arg(); - returnArgReturn6( () => () => () => console.log( 'effect' ) )()(); + const multiArgument = ( func, obj ) => func( obj ); + multiArgument( obj => obj(), () => console.log( 'effect' ) ); const multiArgument2 = ( func, obj ) => func( obj ); - multiArgument2( obj => obj(), () => console.log( 'effect' ) ); - - const multiArgument4 = ( func, obj ) => func( obj ); - multiArgument4( obj => obj.foo.bar = 1, {} ); + multiArgument2( obj => obj.foo.bar = 1, {} ); }); diff --git a/test/form/samples/arrow-function-call-parameters/_expected/cjs.js b/test/form/samples/arrow-function-call-parameters/_expected/cjs.js index 02a5835d466..afc702f801c 100644 --- a/test/form/samples/arrow-function-call-parameters/_expected/cjs.js +++ b/test/form/samples/arrow-function-call-parameters/_expected/cjs.js @@ -1,31 +1,31 @@ 'use strict'; -const callArg2 = arg => arg(); -callArg2( () => console.log( 'effect' ) ); +const callArg = arg => arg(); +callArg( () => console.log( 'effect' ) ); -const assignArg2 = arg => arg.foo.bar = 1; -assignArg2( {} ); +const assignArg = arg => arg.foo.bar = 1; +assignArg( {} ); + +const returnArg = arg => arg; +returnArg( () => console.log( 'effect' ) )(); const returnArg2 = arg => arg; -returnArg2( () => console.log( 'effect' ) )(); +returnArg2( {} ).foo.bar = 1; -const returnArg4 = arg => arg; -returnArg4( {} ).foo.bar = 1; +const returnArg3 = arg => arg; +returnArg3( () => () => console.log( 'effect' ) )()(); -const returnArg6 = arg => arg; -returnArg6( () => () => console.log( 'effect' ) )()(); +const returnArgReturn = arg => arg(); +returnArgReturn( () => () => console.log( 'effect' ) )(); const returnArgReturn2 = arg => arg(); -returnArgReturn2( () => () => console.log( 'effect' ) )(); +returnArgReturn2( () => ({}) ).foo.bar = 1; -const returnArgReturn4 = arg => arg(); -returnArgReturn4( () => ({}) ).foo.bar = 1; +const returnArgReturn3 = arg => arg(); +returnArgReturn3( () => () => () => console.log( 'effect' ) )()(); -const returnArgReturn6 = arg => arg(); -returnArgReturn6( () => () => () => console.log( 'effect' ) )()(); +const multiArgument = ( func, obj ) => func( obj ); +multiArgument( obj => obj(), () => console.log( 'effect' ) ); const multiArgument2 = ( func, obj ) => func( obj ); -multiArgument2( obj => obj(), () => console.log( 'effect' ) ); - -const multiArgument4 = ( func, obj ) => func( obj ); -multiArgument4( obj => obj.foo.bar = 1, {} ); +multiArgument2( obj => obj.foo.bar = 1, {} ); diff --git a/test/form/samples/arrow-function-call-parameters/_expected/es.js b/test/form/samples/arrow-function-call-parameters/_expected/es.js index 3f1479c5d0d..02f05c52ae2 100644 --- a/test/form/samples/arrow-function-call-parameters/_expected/es.js +++ b/test/form/samples/arrow-function-call-parameters/_expected/es.js @@ -1,29 +1,29 @@ -const callArg2 = arg => arg(); -callArg2( () => console.log( 'effect' ) ); +const callArg = arg => arg(); +callArg( () => console.log( 'effect' ) ); -const assignArg2 = arg => arg.foo.bar = 1; -assignArg2( {} ); +const assignArg = arg => arg.foo.bar = 1; +assignArg( {} ); + +const returnArg = arg => arg; +returnArg( () => console.log( 'effect' ) )(); const returnArg2 = arg => arg; -returnArg2( () => console.log( 'effect' ) )(); +returnArg2( {} ).foo.bar = 1; -const returnArg4 = arg => arg; -returnArg4( {} ).foo.bar = 1; +const returnArg3 = arg => arg; +returnArg3( () => () => console.log( 'effect' ) )()(); -const returnArg6 = arg => arg; -returnArg6( () => () => console.log( 'effect' ) )()(); +const returnArgReturn = arg => arg(); +returnArgReturn( () => () => console.log( 'effect' ) )(); const returnArgReturn2 = arg => arg(); -returnArgReturn2( () => () => console.log( 'effect' ) )(); +returnArgReturn2( () => ({}) ).foo.bar = 1; -const returnArgReturn4 = arg => arg(); -returnArgReturn4( () => ({}) ).foo.bar = 1; +const returnArgReturn3 = arg => arg(); +returnArgReturn3( () => () => () => console.log( 'effect' ) )()(); -const returnArgReturn6 = arg => arg(); -returnArgReturn6( () => () => () => console.log( 'effect' ) )()(); +const multiArgument = ( func, obj ) => func( obj ); +multiArgument( obj => obj(), () => console.log( 'effect' ) ); const multiArgument2 = ( func, obj ) => func( obj ); -multiArgument2( obj => obj(), () => console.log( 'effect' ) ); - -const multiArgument4 = ( func, obj ) => func( obj ); -multiArgument4( obj => obj.foo.bar = 1, {} ); +multiArgument2( obj => obj.foo.bar = 1, {} ); diff --git a/test/form/samples/arrow-function-call-parameters/_expected/iife.js b/test/form/samples/arrow-function-call-parameters/_expected/iife.js index 923053db68a..bf1dab98c27 100644 --- a/test/form/samples/arrow-function-call-parameters/_expected/iife.js +++ b/test/form/samples/arrow-function-call-parameters/_expected/iife.js @@ -1,34 +1,34 @@ (function () { 'use strict'; - const callArg2 = arg => arg(); - callArg2( () => console.log( 'effect' ) ); + const callArg = arg => arg(); + callArg( () => console.log( 'effect' ) ); - const assignArg2 = arg => arg.foo.bar = 1; - assignArg2( {} ); + const assignArg = arg => arg.foo.bar = 1; + assignArg( {} ); + + const returnArg = arg => arg; + returnArg( () => console.log( 'effect' ) )(); const returnArg2 = arg => arg; - returnArg2( () => console.log( 'effect' ) )(); + returnArg2( {} ).foo.bar = 1; - const returnArg4 = arg => arg; - returnArg4( {} ).foo.bar = 1; + const returnArg3 = arg => arg; + returnArg3( () => () => console.log( 'effect' ) )()(); - const returnArg6 = arg => arg; - returnArg6( () => () => console.log( 'effect' ) )()(); + const returnArgReturn = arg => arg(); + returnArgReturn( () => () => console.log( 'effect' ) )(); const returnArgReturn2 = arg => arg(); - returnArgReturn2( () => () => console.log( 'effect' ) )(); + returnArgReturn2( () => ({}) ).foo.bar = 1; - const returnArgReturn4 = arg => arg(); - returnArgReturn4( () => ({}) ).foo.bar = 1; + const returnArgReturn3 = arg => arg(); + returnArgReturn3( () => () => () => console.log( 'effect' ) )()(); - const returnArgReturn6 = arg => arg(); - returnArgReturn6( () => () => () => console.log( 'effect' ) )()(); + const multiArgument = ( func, obj ) => func( obj ); + multiArgument( obj => obj(), () => console.log( 'effect' ) ); const multiArgument2 = ( func, obj ) => func( obj ); - multiArgument2( obj => obj(), () => console.log( 'effect' ) ); - - const multiArgument4 = ( func, obj ) => func( obj ); - multiArgument4( obj => obj.foo.bar = 1, {} ); + multiArgument2( obj => obj.foo.bar = 1, {} ); }()); diff --git a/test/form/samples/arrow-function-call-parameters/_expected/umd.js b/test/form/samples/arrow-function-call-parameters/_expected/umd.js index 5f39801aad8..039cd740cdf 100644 --- a/test/form/samples/arrow-function-call-parameters/_expected/umd.js +++ b/test/form/samples/arrow-function-call-parameters/_expected/umd.js @@ -4,34 +4,34 @@ (factory()); }(this, (function () { 'use strict'; - const callArg2 = arg => arg(); - callArg2( () => console.log( 'effect' ) ); + const callArg = arg => arg(); + callArg( () => console.log( 'effect' ) ); - const assignArg2 = arg => arg.foo.bar = 1; - assignArg2( {} ); + const assignArg = arg => arg.foo.bar = 1; + assignArg( {} ); + + const returnArg = arg => arg; + returnArg( () => console.log( 'effect' ) )(); const returnArg2 = arg => arg; - returnArg2( () => console.log( 'effect' ) )(); + returnArg2( {} ).foo.bar = 1; - const returnArg4 = arg => arg; - returnArg4( {} ).foo.bar = 1; + const returnArg3 = arg => arg; + returnArg3( () => () => console.log( 'effect' ) )()(); - const returnArg6 = arg => arg; - returnArg6( () => () => console.log( 'effect' ) )()(); + const returnArgReturn = arg => arg(); + returnArgReturn( () => () => console.log( 'effect' ) )(); const returnArgReturn2 = arg => arg(); - returnArgReturn2( () => () => console.log( 'effect' ) )(); + returnArgReturn2( () => ({}) ).foo.bar = 1; - const returnArgReturn4 = arg => arg(); - returnArgReturn4( () => ({}) ).foo.bar = 1; + const returnArgReturn3 = arg => arg(); + returnArgReturn3( () => () => () => console.log( 'effect' ) )()(); - const returnArgReturn6 = arg => arg(); - returnArgReturn6( () => () => () => console.log( 'effect' ) )()(); + const multiArgument = ( func, obj ) => func( obj ); + multiArgument( obj => obj(), () => console.log( 'effect' ) ); const multiArgument2 = ( func, obj ) => func( obj ); - multiArgument2( obj => obj(), () => console.log( 'effect' ) ); - - const multiArgument4 = ( func, obj ) => func( obj ); - multiArgument4( obj => obj.foo.bar = 1, {} ); + multiArgument2( obj => obj.foo.bar = 1, {} ); }))); diff --git a/test/form/samples/arrow-function-call-parameters/main.js b/test/form/samples/arrow-function-call-parameters/main.js index 4334ee1bcad..02f05c52ae2 100644 --- a/test/form/samples/arrow-function-call-parameters/main.js +++ b/test/form/samples/arrow-function-call-parameters/main.js @@ -1,59 +1,29 @@ -const callArg1 = arg => arg(); -callArg1( () => {} ); +const callArg = arg => arg(); +callArg( () => console.log( 'effect' ) ); -const callArg2 = arg => arg(); -callArg2( () => console.log( 'effect' ) ); +const assignArg = arg => arg.foo.bar = 1; +assignArg( {} ); -const assignArg1 = arg => arg.foo.bar = 1; -assignArg1( { foo: {} } ); - -const assignArg2 = arg => arg.foo.bar = 1; -assignArg2( {} ); - -const returnArg1 = arg => arg; -returnArg1( () => {} )(); +const returnArg = arg => arg; +returnArg( () => console.log( 'effect' ) )(); const returnArg2 = arg => arg; -returnArg2( () => console.log( 'effect' ) )(); +returnArg2( {} ).foo.bar = 1; const returnArg3 = arg => arg; -returnArg3( { foo: {} } ).foo.bar = 1; - -const returnArg4 = arg => arg; -returnArg4( {} ).foo.bar = 1; - -const returnArg5 = arg => arg; -returnArg5( () => () => {} )()(); - -const returnArg6 = arg => arg; -returnArg6( () => () => console.log( 'effect' ) )()(); +returnArg3( () => () => console.log( 'effect' ) )()(); -const returnArgReturn1 = arg => arg(); -returnArgReturn1( () => () => {} )(); +const returnArgReturn = arg => arg(); +returnArgReturn( () => () => console.log( 'effect' ) )(); const returnArgReturn2 = arg => arg(); -returnArgReturn2( () => () => console.log( 'effect' ) )(); +returnArgReturn2( () => ({}) ).foo.bar = 1; const returnArgReturn3 = arg => arg(); -returnArgReturn3( () => ({ foo: {} }) ).foo.bar = 1; +returnArgReturn3( () => () => () => console.log( 'effect' ) )()(); -const returnArgReturn4 = arg => arg(); -returnArgReturn4( () => ({}) ).foo.bar = 1; - -const returnArgReturn5 = arg => arg(); -returnArgReturn5( () => () => () => {} )()(); - -const returnArgReturn6 = arg => arg(); -returnArgReturn6( () => () => () => console.log( 'effect' ) )()(); - -const multiArgument1 = ( func, obj ) => func( obj ); -multiArgument1( obj => obj(), () => {} ); +const multiArgument = ( func, obj ) => func( obj ); +multiArgument( obj => obj(), () => console.log( 'effect' ) ); const multiArgument2 = ( func, obj ) => func( obj ); -multiArgument2( obj => obj(), () => console.log( 'effect' ) ); - -const multiArgument3 = ( func, obj ) => func( obj ); -multiArgument3( obj => obj.foo.bar = 1, { foo: {} } ); - -const multiArgument4 = ( func, obj ) => func( obj ); -multiArgument4( obj => obj.foo.bar = 1, {} ); +multiArgument2( obj => obj.foo.bar = 1, {} ); diff --git a/test/form/samples/function-call-parameters/_expected/amd.js b/test/form/samples/function-call-parameters/_expected/amd.js index 5358a209cfc..d864d70c3c9 100644 --- a/test/form/samples/function-call-parameters/_expected/amd.js +++ b/test/form/samples/function-call-parameters/_expected/amd.js @@ -1,6 +1,5 @@ define(function () { 'use strict'; - // parameters are associated correctly // parameters are associated correctly const retained1 = function ( func, obj ) { return func( obj ); }; retained1( obj => obj(), () => () => console.log( 'effect' ) )(); diff --git a/test/form/samples/function-call-parameters/_expected/cjs.js b/test/form/samples/function-call-parameters/_expected/cjs.js index 1870904f6ed..69e2e8f7186 100644 --- a/test/form/samples/function-call-parameters/_expected/cjs.js +++ b/test/form/samples/function-call-parameters/_expected/cjs.js @@ -1,6 +1,5 @@ 'use strict'; -// parameters are associated correctly // parameters are associated correctly const retained1 = function ( func, obj ) { return func( obj ); }; retained1( obj => obj(), () => () => console.log( 'effect' ) )(); diff --git a/test/form/samples/function-call-parameters/_expected/es.js b/test/form/samples/function-call-parameters/_expected/es.js index d6770b8be4f..1ab81a10219 100644 --- a/test/form/samples/function-call-parameters/_expected/es.js +++ b/test/form/samples/function-call-parameters/_expected/es.js @@ -1,5 +1,4 @@ // parameters are associated correctly -// parameters are associated correctly const retained1 = function ( func, obj ) { return func( obj ); }; retained1( obj => obj(), () => () => console.log( 'effect' ) )(); diff --git a/test/form/samples/function-call-parameters/_expected/iife.js b/test/form/samples/function-call-parameters/_expected/iife.js index 8199579ce3d..4c7db0eab57 100644 --- a/test/form/samples/function-call-parameters/_expected/iife.js +++ b/test/form/samples/function-call-parameters/_expected/iife.js @@ -1,7 +1,6 @@ (function () { 'use strict'; - // parameters are associated correctly // parameters are associated correctly const retained1 = function ( func, obj ) { return func( obj ); }; retained1( obj => obj(), () => () => console.log( 'effect' ) )(); diff --git a/test/form/samples/function-call-parameters/_expected/umd.js b/test/form/samples/function-call-parameters/_expected/umd.js index 274b18fe6c9..dc1acdbfe00 100644 --- a/test/form/samples/function-call-parameters/_expected/umd.js +++ b/test/form/samples/function-call-parameters/_expected/umd.js @@ -4,7 +4,6 @@ (factory()); }(this, (function () { 'use strict'; - // parameters are associated correctly // parameters are associated correctly const retained1 = function ( func, obj ) { return func( obj ); }; retained1( obj => obj(), () => () => console.log( 'effect' ) )(); diff --git a/test/form/samples/function-call-parameters/main.js b/test/form/samples/function-call-parameters/main.js index 2e0e35da1ad..1ab81a10219 100644 --- a/test/form/samples/function-call-parameters/main.js +++ b/test/form/samples/function-call-parameters/main.js @@ -1,25 +1,3 @@ -// parameters are associated correctly -const removed1 = function ( func, obj ) { return func( obj ); }; -removed1( obj => obj(), () => () => {} )(); - -const removed2 = function ( func, obj ) { return func( obj ); }; -removed2( obj => ({ foo: obj }), { bar: {} } ).foo.bar.baz = 1; - -// parameters and arguments have the same values -function removed3 ( x ) { - x.foo.bar = 1; - arguments[ 0 ].foo.bar = 1; -} - -removed3( { foo: {} } ); - -// the number of arguments does not depend on the number of parameters -function removed4 ( x ) { - arguments[ 1 ].foo.bar = 1; -} - -removed4( {}, { foo: {} } ); - // parameters are associated correctly const retained1 = function ( func, obj ) { return func( obj ); }; retained1( obj => obj(), () => () => console.log( 'effect' ) )(); From ba7f8b21b833c0a772848f80346f6682dd22e463 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Mon, 6 Nov 2017 07:23:18 +0100 Subject: [PATCH 74/76] Assume that accessing members of a pure global function or its prototype is never a side-effect. --- src/ast/nodes/CallExpression.js | 1 - src/ast/nodes/NewExpression.js | 1 - src/ast/variables/GlobalVariable.js | 6 +- .../_config.js | 5 + .../_expected/amd.js | 6 + .../_expected/cjs.js | 4 + .../_expected/es.js | 2 + .../_expected/iife.js | 7 + .../_expected/umd.js | 10 + .../main.js | 6 + .../removes-unused-babel-helpers/_config.js | 5 + .../_expected/amd.js | 5 + .../_expected/cjs.js | 2 + .../_expected/es.js | 1 + .../_expected/iife.js | 6 + .../_expected/umd.js | 9 + .../removes-unused-babel-helpers/main.js | 558 ++++++++++++++++++ 17 files changed, 631 insertions(+), 3 deletions(-) create mode 100644 test/form/samples/globals-removes-access-to-pure-function-members/_config.js create mode 100644 test/form/samples/globals-removes-access-to-pure-function-members/_expected/amd.js create mode 100644 test/form/samples/globals-removes-access-to-pure-function-members/_expected/cjs.js create mode 100644 test/form/samples/globals-removes-access-to-pure-function-members/_expected/es.js create mode 100644 test/form/samples/globals-removes-access-to-pure-function-members/_expected/iife.js create mode 100644 test/form/samples/globals-removes-access-to-pure-function-members/_expected/umd.js create mode 100644 test/form/samples/globals-removes-access-to-pure-function-members/main.js create mode 100644 test/form/samples/removes-unused-babel-helpers/_config.js create mode 100644 test/form/samples/removes-unused-babel-helpers/_expected/amd.js create mode 100644 test/form/samples/removes-unused-babel-helpers/_expected/cjs.js create mode 100644 test/form/samples/removes-unused-babel-helpers/_expected/es.js create mode 100644 test/form/samples/removes-unused-babel-helpers/_expected/iife.js create mode 100644 test/form/samples/removes-unused-babel-helpers/_expected/umd.js create mode 100644 test/form/samples/removes-unused-babel-helpers/main.js diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index e51257b4e0c..b1ab507a2a5 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -1,6 +1,5 @@ import Node from '../Node.js'; import CallOptions from '../CallOptions'; -import ExecutionPathOptions from '../ExecutionPathOptions'; export default class CallExpression extends Node { bindAssignmentAtPath ( path, expression, options ) { diff --git a/src/ast/nodes/NewExpression.js b/src/ast/nodes/NewExpression.js index e19781c6e6a..da0e2ec06cf 100644 --- a/src/ast/nodes/NewExpression.js +++ b/src/ast/nodes/NewExpression.js @@ -1,6 +1,5 @@ import Node from '../Node.js'; import CallOptions from '../CallOptions'; -import ExecutionPathOptions from '../ExecutionPathOptions'; export default class NewExpression extends Node { hasEffects ( options ) { diff --git a/src/ast/variables/GlobalVariable.js b/src/ast/variables/GlobalVariable.js index a5e53f7f128..a837b5aa967 100644 --- a/src/ast/variables/GlobalVariable.js +++ b/src/ast/variables/GlobalVariable.js @@ -17,7 +17,11 @@ export default class GlobalVariable extends Variable { hasEffectsWhenAccessedAtPath ( path ) { // path.length == 0 can also have an effect but we postpone this for now return path.length > 0 - && !pureFunctions[ [ this.name, ...path ].join( '.' ) ]; + && !pureFunctions[ [ this.name, ...path ].join( '.' ) ] + && !pureFunctions[ [ this.name, ...path.slice( 0, -1 ) ].join( '.' ) ] + && !(path.length > 1 + && pureFunctions[ [ this.name, ...path.slice( 0, -2 ) ].join( '.' ) ] + && path[ path.length - 2 ] === 'prototype'); } hasEffectsWhenCalledAtPath ( path ) { diff --git a/test/form/samples/globals-removes-access-to-pure-function-members/_config.js b/test/form/samples/globals-removes-access-to-pure-function-members/_config.js new file mode 100644 index 00000000000..b43eb5d1fed --- /dev/null +++ b/test/form/samples/globals-removes-access-to-pure-function-members/_config.js @@ -0,0 +1,5 @@ +var path = require('path'); + +module.exports = { + description: 'accessing members of pure functions and their prototypes is not a side-effect' +}; diff --git a/test/form/samples/globals-removes-access-to-pure-function-members/_expected/amd.js b/test/form/samples/globals-removes-access-to-pure-function-members/_expected/amd.js new file mode 100644 index 00000000000..b8c20625cd5 --- /dev/null +++ b/test/form/samples/globals-removes-access-to-pure-function-members/_expected/amd.js @@ -0,0 +1,6 @@ +define(function () { 'use strict'; + + Unknown.staticMember; + Unknown.prototype.method; + +}); diff --git a/test/form/samples/globals-removes-access-to-pure-function-members/_expected/cjs.js b/test/form/samples/globals-removes-access-to-pure-function-members/_expected/cjs.js new file mode 100644 index 00000000000..f36e1b4aefd --- /dev/null +++ b/test/form/samples/globals-removes-access-to-pure-function-members/_expected/cjs.js @@ -0,0 +1,4 @@ +'use strict'; + +Unknown.staticMember; +Unknown.prototype.method; diff --git a/test/form/samples/globals-removes-access-to-pure-function-members/_expected/es.js b/test/form/samples/globals-removes-access-to-pure-function-members/_expected/es.js new file mode 100644 index 00000000000..f9bbbbd3550 --- /dev/null +++ b/test/form/samples/globals-removes-access-to-pure-function-members/_expected/es.js @@ -0,0 +1,2 @@ +Unknown.staticMember; +Unknown.prototype.method; diff --git a/test/form/samples/globals-removes-access-to-pure-function-members/_expected/iife.js b/test/form/samples/globals-removes-access-to-pure-function-members/_expected/iife.js new file mode 100644 index 00000000000..5bc83f4bb61 --- /dev/null +++ b/test/form/samples/globals-removes-access-to-pure-function-members/_expected/iife.js @@ -0,0 +1,7 @@ +(function () { + 'use strict'; + + Unknown.staticMember; + Unknown.prototype.method; + +}()); diff --git a/test/form/samples/globals-removes-access-to-pure-function-members/_expected/umd.js b/test/form/samples/globals-removes-access-to-pure-function-members/_expected/umd.js new file mode 100644 index 00000000000..5a0cf15b9d7 --- /dev/null +++ b/test/form/samples/globals-removes-access-to-pure-function-members/_expected/umd.js @@ -0,0 +1,10 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + Unknown.staticMember; + Unknown.prototype.method; + +}))); diff --git a/test/form/samples/globals-removes-access-to-pure-function-members/main.js b/test/form/samples/globals-removes-access-to-pure-function-members/main.js new file mode 100644 index 00000000000..f4a07ef00dd --- /dev/null +++ b/test/form/samples/globals-removes-access-to-pure-function-members/main.js @@ -0,0 +1,6 @@ +Symbol.iterator; +Array.prototype.reverse; +Object.prototype.hasOwnProperty; + +Unknown.staticMember; +Unknown.prototype.method; diff --git a/test/form/samples/removes-unused-babel-helpers/_config.js b/test/form/samples/removes-unused-babel-helpers/_config.js new file mode 100644 index 00000000000..e222270e308 --- /dev/null +++ b/test/form/samples/removes-unused-babel-helpers/_config.js @@ -0,0 +1,5 @@ +const assert = require( 'assert' ); + +module.exports = { + description: 'Removes unused babel helpers from the build (#1595)' +}; diff --git a/test/form/samples/removes-unused-babel-helpers/_expected/amd.js b/test/form/samples/removes-unused-babel-helpers/_expected/amd.js new file mode 100644 index 00000000000..f9f8229aa40 --- /dev/null +++ b/test/form/samples/removes-unused-babel-helpers/_expected/amd.js @@ -0,0 +1,5 @@ +define(function () { 'use strict'; + + + +}); diff --git a/test/form/samples/removes-unused-babel-helpers/_expected/cjs.js b/test/form/samples/removes-unused-babel-helpers/_expected/cjs.js new file mode 100644 index 00000000000..eb109abbed0 --- /dev/null +++ b/test/form/samples/removes-unused-babel-helpers/_expected/cjs.js @@ -0,0 +1,2 @@ +'use strict'; + diff --git a/test/form/samples/removes-unused-babel-helpers/_expected/es.js b/test/form/samples/removes-unused-babel-helpers/_expected/es.js new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/test/form/samples/removes-unused-babel-helpers/_expected/es.js @@ -0,0 +1 @@ + diff --git a/test/form/samples/removes-unused-babel-helpers/_expected/iife.js b/test/form/samples/removes-unused-babel-helpers/_expected/iife.js new file mode 100644 index 00000000000..43ef5426880 --- /dev/null +++ b/test/form/samples/removes-unused-babel-helpers/_expected/iife.js @@ -0,0 +1,6 @@ +(function () { + 'use strict'; + + + +}()); diff --git a/test/form/samples/removes-unused-babel-helpers/_expected/umd.js b/test/form/samples/removes-unused-babel-helpers/_expected/umd.js new file mode 100644 index 00000000000..07ce27e42f1 --- /dev/null +++ b/test/form/samples/removes-unused-babel-helpers/_expected/umd.js @@ -0,0 +1,9 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + + +}))); diff --git a/test/form/samples/removes-unused-babel-helpers/main.js b/test/form/samples/removes-unused-babel-helpers/main.js new file mode 100644 index 00000000000..c7afe38e308 --- /dev/null +++ b/test/form/samples/removes-unused-babel-helpers/main.js @@ -0,0 +1,558 @@ +var babelHelpers = {}; +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { + return typeof obj; +} : function (obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; +}; + +var jsx = function () { + var REACT_ELEMENT_TYPE = typeof Symbol === "function" && Symbol.for && Symbol.for("react.element") || 0xeac7; + return function createRawReactElement(type, props, key, children) { + var defaultProps = type && type.defaultProps; + var childrenLength = arguments.length - 3; + + if (!props && childrenLength !== 0) { + props = {}; + } + + if (props && defaultProps) { + for (var propName in defaultProps) { + if (props[propName] === void 0) { + props[propName] = defaultProps[propName]; + } + } + } else if (!props) { + props = defaultProps || {}; + } + + if (childrenLength === 1) { + props.children = children; + } else if (childrenLength > 1) { + var childArray = Array(childrenLength); + + for (var i = 0; i < childrenLength; i++) { + childArray[i] = arguments[i + 3]; + } + + props.children = childArray; + } + + return { + $$typeof: REACT_ELEMENT_TYPE, + type: type, + key: key === undefined ? null : '' + key, + ref: null, + props: props, + _owner: null + }; + }; +}(); + +var asyncIterator = function (iterable) { + if (typeof Symbol === "function") { + if (Symbol.asyncIterator) { + var method = iterable[Symbol.asyncIterator]; + if (method != null) return method.call(iterable); + } + + if (Symbol.iterator) { + return iterable[Symbol.iterator](); + } + } + + throw new TypeError("Object is not async iterable"); +}; + +var asyncGenerator = function () { + function AwaitValue(value) { + this.value = value; + } + + function AsyncGenerator(gen) { + var front, back; + + function send(key, arg) { + return new Promise(function (resolve, reject) { + var request = { + key: key, + arg: arg, + resolve: resolve, + reject: reject, + next: null + }; + + if (back) { + back = back.next = request; + } else { + front = back = request; + resume(key, arg); + } + }); + } + + function resume(key, arg) { + try { + var result = gen[key](arg); + var value = result.value; + + if (value instanceof AwaitValue) { + Promise.resolve(value.value).then(function (arg) { + resume("next", arg); + }, function (arg) { + resume("throw", arg); + }); + } else { + settle(result.done ? "return" : "normal", result.value); + } + } catch (err) { + settle("throw", err); + } + } + + function settle(type, value) { + switch (type) { + case "return": + front.resolve({ + value: value, + done: true + }); + break; + + case "throw": + front.reject(value); + break; + + default: + front.resolve({ + value: value, + done: false + }); + break; + } + + front = front.next; + + if (front) { + resume(front.key, front.arg); + } else { + back = null; + } + } + + this._invoke = send; + + if (typeof gen.return !== "function") { + this.return = undefined; + } + } + + if (typeof Symbol === "function" && Symbol.asyncIterator) { + AsyncGenerator.prototype[Symbol.asyncIterator] = function () { + return this; + }; + } + + AsyncGenerator.prototype.next = function (arg) { + return this._invoke("next", arg); + }; + + AsyncGenerator.prototype.throw = function (arg) { + return this._invoke("throw", arg); + }; + + AsyncGenerator.prototype.return = function (arg) { + return this._invoke("return", arg); + }; + + return { + wrap: function (fn) { + return function () { + return new AsyncGenerator(fn.apply(this, arguments)); + }; + }, + await: function (value) { + return new AwaitValue(value); + } + }; +}(); + +var asyncGeneratorDelegate = function (inner, awaitWrap) { + var iter = {}, + waiting = false; + + function pump(key, value) { + waiting = true; + value = new Promise(function (resolve) { + resolve(inner[key](value)); + }); + return { + done: false, + value: awaitWrap(value) + }; + } + + + + if (typeof Symbol === "function" && Symbol.iterator) { + iter[Symbol.iterator] = function () { + return this; + }; + } + + iter.next = function (value) { + if (waiting) { + waiting = false; + return value; + } + + return pump("next", value); + }; + + if (typeof inner.throw === "function") { + iter.throw = function (value) { + if (waiting) { + waiting = false; + throw value; + } + + return pump("throw", value); + }; + } + + if (typeof inner.return === "function") { + iter.return = function (value) { + return pump("return", value); + }; + } + + return iter; +}; + +var asyncToGenerator = function (fn) { + return function () { + var gen = fn.apply(this, arguments); + return new Promise(function (resolve, reject) { + function step(key, arg) { + try { + var info = gen[key](arg); + var value = info.value; + } catch (error) { + reject(error); + return; + } + + if (info.done) { + resolve(value); + } else { + return Promise.resolve(value).then(function (value) { + step("next", value); + }, function (err) { + step("throw", err); + }); + } + } + + return step("next"); + }); + }; +}; + +var classCallCheck = function (instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +}; + +var createClass = function () { + function defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + return function (Constructor, protoProps, staticProps) { + if (protoProps) defineProperties(Constructor.prototype, protoProps); + if (staticProps) defineProperties(Constructor, staticProps); + return Constructor; + }; +}(); + +var defineEnumerableProperties = function (obj, descs) { + for (var key in descs) { + var desc = descs[key]; + desc.configurable = desc.enumerable = true; + if ("value" in desc) desc.writable = true; + Object.defineProperty(obj, key, desc); + } + + return obj; +}; + +var defaults = function (obj, defaults) { + var keys = Object.getOwnPropertyNames(defaults); + + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var value = Object.getOwnPropertyDescriptor(defaults, key); + + if (value && value.configurable && obj[key] === undefined) { + Object.defineProperty(obj, key, value); + } + } + + return obj; +}; + +var defineProperty = function (obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + + return obj; +}; + +var _extends = Object.assign || function (target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + + return target; +}; + +var get = function get(object, property, receiver) { + if (object === null) object = Function.prototype; + var desc = Object.getOwnPropertyDescriptor(object, property); + + if (desc === undefined) { + var parent = Object.getPrototypeOf(object); + + if (parent === null) { + return undefined; + } else { + return get(parent, property, receiver); + } + } else if ("value" in desc) { + return desc.value; + } else { + var getter = desc.get; + + if (getter === undefined) { + return undefined; + } + + return getter.call(receiver); + } +}; + +var inherits = function (subClass, superClass) { + if (typeof superClass !== "function" && superClass !== null) { + throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); + } + + subClass.prototype = Object.create(superClass && superClass.prototype, { + constructor: { + value: subClass, + enumerable: false, + writable: true, + configurable: true + } + }); + if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; +}; + +var _instanceof = function (left, right) { + if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { + return right[Symbol.hasInstance](left); + } else { + return left instanceof right; + } +}; + +var interopRequireDefault = function (obj) { + return obj && obj.__esModule ? obj : { + default: obj + }; +}; + +var interopRequireWildcard = function (obj) { + if (obj && obj.__esModule) { + return obj; + } else { + var newObj = {}; + + if (obj != null) { + for (var key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; + } + } + + newObj.default = obj; + return newObj; + } +}; + +var newArrowCheck = function (innerThis, boundThis) { + if (innerThis !== boundThis) { + throw new TypeError("Cannot instantiate an arrow function"); + } +}; + +var objectDestructuringEmpty = function (obj) { + if (obj == null) throw new TypeError("Cannot destructure undefined"); +}; + +var objectWithoutProperties = function (obj, keys) { + var target = {}; + + for (var i in obj) { + if (keys.indexOf(i) >= 0) continue; + if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; + target[i] = obj[i]; + } + + return target; +}; + +var possibleConstructorReturn = function (self, call) { + if (!self) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + + return call && (typeof call === "object" || typeof call === "function") ? call : self; +}; + +var selfGlobal = typeof global === "undefined" ? self : global; + +var set = function set(object, property, value, receiver) { + var desc = Object.getOwnPropertyDescriptor(object, property); + + if (desc === undefined) { + var parent = Object.getPrototypeOf(object); + + if (parent !== null) { + set(parent, property, value, receiver); + } + } else if ("value" in desc && desc.writable) { + desc.value = value; + } else { + var setter = desc.set; + + if (setter !== undefined) { + setter.call(receiver, value); + } + } + + return value; +}; + +var slicedToArray = function () { + function sliceIterator(arr, i) { + var _arr = []; + var _n = true; + var _d = false; + var _e = undefined; + + try { + for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + + if (i && _arr.length === i) break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i["return"]) _i["return"](); + } finally { + if (_d) throw _e; + } + } + + return _arr; + } + + return function (arr, i) { + if (Array.isArray(arr)) { + return arr; + } else if (Symbol.iterator in Object(arr)) { + return sliceIterator(arr, i); + } else { + throw new TypeError("Invalid attempt to destructure non-iterable instance"); + } + }; +}(); + +var slicedToArrayLoose = function (arr, i) { + if (Array.isArray(arr)) { + return arr; + } else if (Symbol.iterator in Object(arr)) { + var _arr = []; + + for (var _iterator = arr[Symbol.iterator](), _step; !(_step = _iterator.next()).done;) { + _arr.push(_step.value); + + if (i && _arr.length === i) break; + } + + return _arr; + } else { + throw new TypeError("Invalid attempt to destructure non-iterable instance"); + } +}; + +var taggedTemplateLiteral = function (strings, raw) { + return Object.freeze(Object.defineProperties(strings, { + raw: { + value: Object.freeze(raw) + } + })); +}; + +var taggedTemplateLiteralLoose = function (strings, raw) { + strings.raw = raw; + return strings; +}; + +var temporalRef = function (val, name, undef) { + if (val === undef) { + throw new ReferenceError(name + " is not defined - temporal dead zone"); + } else { + return val; + } +}; + +var temporalUndefined = {}; + +var toArray = function (arr) { + return Array.isArray(arr) ? arr : Array.from(arr); +}; + +var toConsumableArray = function (arr) { + if (Array.isArray(arr)) { + for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; + + return arr2; + } else { + return Array.from(arr); + } +}; + +babelHelpers; From 00a5b9cbddedfb0a93bad168783dda2a934c8f83 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Mon, 6 Nov 2017 07:57:50 +0100 Subject: [PATCH 75/76] * Rename StructuredAssignmentTracker -> ObjectShapeTracker * Rename ReplaceableInitVariable -> ReplaceableInitializationVariable and simplify it as most of its logic (including the awkwardly named ReplaceableInitStructuredAssignmentTracker) are no longer needed now we assume all parameters to have unknown initial values --- src/ast/nodes/MemberExpression.js | 2 +- src/ast/nodes/ObjectExpression.js | 2 +- src/ast/variables/LocalVariable.js | 4 +- src/ast/variables/ParameterVariable.js | 6 +-- ...laceableInitStructuredAssignmentTracker.js | 40 ------------------ src/ast/variables/ReplaceableInitVariable.js | 42 ------------------- .../ReplaceableInitializationVariable.js | 36 ++++++++++++++++ src/ast/variables/ThisVariable.js | 4 +- ...mentTracker.js => VariableShapeTracker.js} | 4 +- 9 files changed, 47 insertions(+), 93 deletions(-) delete mode 100644 src/ast/variables/ReplaceableInitStructuredAssignmentTracker.js delete mode 100644 src/ast/variables/ReplaceableInitVariable.js create mode 100644 src/ast/variables/ReplaceableInitializationVariable.js rename src/ast/variables/{StructuredAssignmentTracker.js => VariableShapeTracker.js} (96%) diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index c417dcd3fdf..3fc704f1ef7 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/StructuredAssignmentTracker'; +import { UNKNOWN_KEY } from '../variables/VariableShapeTracker'; const validProp = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; diff --git a/src/ast/nodes/ObjectExpression.js b/src/ast/nodes/ObjectExpression.js index a4c33728bf8..a662d1b8eec 100644 --- a/src/ast/nodes/ObjectExpression.js +++ b/src/ast/nodes/ObjectExpression.js @@ -1,5 +1,5 @@ import Node from '../Node.js'; -import { UNKNOWN_KEY } from '../variables/StructuredAssignmentTracker'; +import { UNKNOWN_KEY } from '../variables/VariableShapeTracker'; const PROPERTY_KINDS_READ = [ 'init', 'get' ]; const PROPERTY_KINDS_WRITE = [ 'init', 'set' ]; diff --git a/src/ast/variables/LocalVariable.js b/src/ast/variables/LocalVariable.js index 8142f18f112..e600698079a 100644 --- a/src/ast/variables/LocalVariable.js +++ b/src/ast/variables/LocalVariable.js @@ -1,5 +1,5 @@ import Variable from './Variable'; -import StructuredAssignmentTracker from './StructuredAssignmentTracker'; +import VariableShapeTracker from './VariableShapeTracker'; // To avoid infinite recursions const MAX_PATH_LENGTH = 6; @@ -10,7 +10,7 @@ export default class LocalVariable extends Variable { this.isReassigned = false; this.exportName = null; this.declarations = new Set( declarator ? [ declarator ] : null ); - this.boundExpressions = new StructuredAssignmentTracker(); + this.boundExpressions = new VariableShapeTracker(); init && this.boundExpressions.addAtPath( [], init ); } diff --git a/src/ast/variables/ParameterVariable.js b/src/ast/variables/ParameterVariable.js index 57db225141f..78cf3c9730b 100644 --- a/src/ast/variables/ParameterVariable.js +++ b/src/ast/variables/ParameterVariable.js @@ -1,8 +1,8 @@ -import ReplaceableInitVariable from './ReplaceableInitVariable'; +import ReplaceableInitializationVariable from './ReplaceableInitializationVariable'; -export default class ParameterVariable extends ReplaceableInitVariable { +export default class ParameterVariable extends ReplaceableInitializationVariable { constructor ( identifier ) { - super( identifier.name, identifier ); + super( identifier.name, identifier, null ); } getName () { diff --git a/src/ast/variables/ReplaceableInitStructuredAssignmentTracker.js b/src/ast/variables/ReplaceableInitStructuredAssignmentTracker.js deleted file mode 100644 index 83ed45bb8ba..00000000000 --- a/src/ast/variables/ReplaceableInitStructuredAssignmentTracker.js +++ /dev/null @@ -1,40 +0,0 @@ -import StructuredAssignmentTracker from './StructuredAssignmentTracker'; -import LocalVariable from './LocalVariable'; -import ExecutionPathOptions from '../ExecutionPathOptions'; - -export default class ReplaceableInitStructuredAssignmentTracker extends StructuredAssignmentTracker { - constructor () { - super(); - this._init = new LocalVariable( {}, null, null ); - } - - addAtPath ( path, assignment ) { - if ( path.length > 0 ) { - this._init.bindAssignmentAtPath( path, assignment, ExecutionPathOptions.create() ); - } - super.addAtPath( path, assignment ); - } - - addInit ( assignment ) { - this._init.bindAssignmentAtPath( [], assignment, ExecutionPathOptions.create() ); - } - - forEachAtPath ( path, callback ) { - callback( path, this._init ); - super.forEachAtPath( path, callback ); - } - - hasAtPath ( path, assignment ) { - return (path.length === 0 && assignment === this._init) - || super.hasAtPath( path, assignment ); - } - - setInit ( init ) { - this._init = init; - } - - someAtPath ( path, predicateFunction ) { - return predicateFunction( path, this._init ) - || super.someAtPath( path, predicateFunction ); - } -} diff --git a/src/ast/variables/ReplaceableInitVariable.js b/src/ast/variables/ReplaceableInitVariable.js deleted file mode 100644 index e77150f237c..00000000000 --- a/src/ast/variables/ReplaceableInitVariable.js +++ /dev/null @@ -1,42 +0,0 @@ -import LocalVariable from './LocalVariable'; -import ReplaceableInitStructuredAssignmentTracker from './ReplaceableInitStructuredAssignmentTracker'; -import { UNKNOWN_ASSIGNMENT } from '../values'; - -export default class ReplaceableInitVariable extends LocalVariable { - constructor ( name, declarator ) { - super( name, declarator, null ); - this.boundExpressions = new ReplaceableInitStructuredAssignmentTracker(); - } - - bindInitialization ( expression ) { - this.boundExpressions.addInit( expression ); - } - - getName () { - return this.name; - } - - hasEffectsWhenAccessedAtPath ( path, options ) { - this._updateInit( options ); - return super.hasEffectsWhenAccessedAtPath( path, options ); - } - - hasEffectsWhenAssignedAtPath ( path, options ) { - this._updateInit( options ); - return super.hasEffectsWhenAssignedAtPath( path, options ); - } - - hasEffectsWhenCalledAtPath ( path, callOptions, options ) { - this._updateInit( options ); - return super.hasEffectsWhenCalledAtPath( path, callOptions, options ); - } - - someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { - this._updateInit( options ); - return super.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ); - } - - _updateInit ( options ) { - this.boundExpressions.setInit( options.getReplacedVariableInit( this ) || UNKNOWN_ASSIGNMENT ); - } -} diff --git a/src/ast/variables/ReplaceableInitializationVariable.js b/src/ast/variables/ReplaceableInitializationVariable.js new file mode 100644 index 00000000000..66e802e563e --- /dev/null +++ b/src/ast/variables/ReplaceableInitializationVariable.js @@ -0,0 +1,36 @@ +import LocalVariable from './LocalVariable'; +import { UNKNOWN_ASSIGNMENT } from '../values'; + +export default class ReplaceableInitializationVariable extends LocalVariable { + constructor ( name, declarator ) { + super( name, declarator, null ); + } + + getName () { + return this.name; + } + + hasEffectsWhenAccessedAtPath ( path, options ) { + return this._getInit( options ).hasEffectsWhenAccessedAtPath( path, options ) + || super.hasEffectsWhenAccessedAtPath( path, options ); + } + + hasEffectsWhenAssignedAtPath ( path, options ) { + return this._getInit( options ).hasEffectsWhenAssignedAtPath( path, options ) + || super.hasEffectsWhenAssignedAtPath( path, options ); + } + + hasEffectsWhenCalledAtPath ( path, callOptions, options ) { + return this._getInit( options ).hasEffectsWhenCalledAtPath( path, callOptions, options ) + || super.hasEffectsWhenCalledAtPath( path, callOptions, options ); + } + + someReturnExpressionWhenCalledAtPath ( path, callOptions, predicateFunction, options ) { + return this._getInit( options ).someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ) + || super.someReturnExpressionWhenCalledAtPath( path, callOptions, predicateFunction, options ); + } + + _getInit ( options ) { + return options.getReplacedVariableInit( this ) || UNKNOWN_ASSIGNMENT; + } +} diff --git a/src/ast/variables/ThisVariable.js b/src/ast/variables/ThisVariable.js index 6ce262901b1..8724503188f 100644 --- a/src/ast/variables/ThisVariable.js +++ b/src/ast/variables/ThisVariable.js @@ -1,6 +1,6 @@ -import ReplaceableInitVariable from './ReplaceableInitVariable'; +import ReplaceableInitializationVariable from './ReplaceableInitializationVariable'; -export default class ThisVariable extends ReplaceableInitVariable { +export default class ThisVariable extends ReplaceableInitializationVariable { constructor () { super( 'this', null ); } diff --git a/src/ast/variables/StructuredAssignmentTracker.js b/src/ast/variables/VariableShapeTracker.js similarity index 96% rename from src/ast/variables/StructuredAssignmentTracker.js rename to src/ast/variables/VariableShapeTracker.js index f84fefb10e0..07650a5262a 100644 --- a/src/ast/variables/StructuredAssignmentTracker.js +++ b/src/ast/variables/VariableShapeTracker.js @@ -5,7 +5,7 @@ export const UNKNOWN_KEY = { type: 'UNKNOWN_KEY' }; const UNKNOWN_ASSIGNMENTS = new Map( [ [ SET_KEY, new Set( [ UNKNOWN_ASSIGNMENT ] ) ] ] ); -export default class StructuredAssignmentTracker { +export default class VariableShapeTracker { constructor () { this._assignments = new Map( [ [ SET_KEY, new Set() ] ] ); } @@ -21,7 +21,7 @@ export default class StructuredAssignmentTracker { } else { const [ nextPath, ...remainingPath ] = path; if ( !this._assignments.has( nextPath ) ) { - this._assignments.set( nextPath, new StructuredAssignmentTracker() ); + this._assignments.set( nextPath, new VariableShapeTracker() ); } this._assignments.get( nextPath ).addAtPath( remainingPath, assignment ); } From bc70d8d75de957884b2a35b25f13a49c2bd36e56 Mon Sep 17 00:00:00 2001 From: Lukas Taegert Date: Wed, 8 Nov 2017 07:23:12 +0100 Subject: [PATCH 76/76] Resolve #1263 --- .../samples/tree-shake-curried-functions/_config.js | 5 +++++ .../tree-shake-curried-functions/_expected/amd.js | 5 +++++ .../tree-shake-curried-functions/_expected/cjs.js | 2 ++ .../tree-shake-curried-functions/_expected/es.js | 1 + .../tree-shake-curried-functions/_expected/iife.js | 6 ++++++ .../tree-shake-curried-functions/_expected/umd.js | 9 +++++++++ .../samples/tree-shake-curried-functions/main.js | 12 ++++++++++++ 7 files changed, 40 insertions(+) create mode 100644 test/form/samples/tree-shake-curried-functions/_config.js create mode 100644 test/form/samples/tree-shake-curried-functions/_expected/amd.js create mode 100644 test/form/samples/tree-shake-curried-functions/_expected/cjs.js create mode 100644 test/form/samples/tree-shake-curried-functions/_expected/es.js create mode 100644 test/form/samples/tree-shake-curried-functions/_expected/iife.js create mode 100644 test/form/samples/tree-shake-curried-functions/_expected/umd.js create mode 100644 test/form/samples/tree-shake-curried-functions/main.js diff --git a/test/form/samples/tree-shake-curried-functions/_config.js b/test/form/samples/tree-shake-curried-functions/_config.js new file mode 100644 index 00000000000..7927d483dec --- /dev/null +++ b/test/form/samples/tree-shake-curried-functions/_config.js @@ -0,0 +1,5 @@ +var path = require('path'); + +module.exports = { + description: 'Remove side-effect-free curried functions (#1263)' +}; diff --git a/test/form/samples/tree-shake-curried-functions/_expected/amd.js b/test/form/samples/tree-shake-curried-functions/_expected/amd.js new file mode 100644 index 00000000000..f9f8229aa40 --- /dev/null +++ b/test/form/samples/tree-shake-curried-functions/_expected/amd.js @@ -0,0 +1,5 @@ +define(function () { 'use strict'; + + + +}); diff --git a/test/form/samples/tree-shake-curried-functions/_expected/cjs.js b/test/form/samples/tree-shake-curried-functions/_expected/cjs.js new file mode 100644 index 00000000000..eb109abbed0 --- /dev/null +++ b/test/form/samples/tree-shake-curried-functions/_expected/cjs.js @@ -0,0 +1,2 @@ +'use strict'; + diff --git a/test/form/samples/tree-shake-curried-functions/_expected/es.js b/test/form/samples/tree-shake-curried-functions/_expected/es.js new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/test/form/samples/tree-shake-curried-functions/_expected/es.js @@ -0,0 +1 @@ + diff --git a/test/form/samples/tree-shake-curried-functions/_expected/iife.js b/test/form/samples/tree-shake-curried-functions/_expected/iife.js new file mode 100644 index 00000000000..43ef5426880 --- /dev/null +++ b/test/form/samples/tree-shake-curried-functions/_expected/iife.js @@ -0,0 +1,6 @@ +(function () { + 'use strict'; + + + +}()); diff --git a/test/form/samples/tree-shake-curried-functions/_expected/umd.js b/test/form/samples/tree-shake-curried-functions/_expected/umd.js new file mode 100644 index 00000000000..07ce27e42f1 --- /dev/null +++ b/test/form/samples/tree-shake-curried-functions/_expected/umd.js @@ -0,0 +1,9 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + + +}))); diff --git a/test/form/samples/tree-shake-curried-functions/main.js b/test/form/samples/tree-shake-curried-functions/main.js new file mode 100644 index 00000000000..e22329a7a2a --- /dev/null +++ b/test/form/samples/tree-shake-curried-functions/main.js @@ -0,0 +1,12 @@ +function foo(a) { + return function(b) { + return a+b; + } +} + +function bar(a, b) { + return a+b; +} + +foo(1)(2); +bar(1,2);