From e6ab027313bc5bc5e8eba59d5b4bc3b790339f4e Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Sat, 9 Jan 2016 17:44:49 -0500 Subject: [PATCH] handle complex call expressions (#440) --- src/utils/pureFunctions.js | 42 ++++++++++++ src/utils/run.js | 83 ++++------------------- test/form/side-effect-n/_expected/amd.js | 13 ++++ test/form/side-effect-n/_expected/cjs.js | 11 +++ test/form/side-effect-n/_expected/es6.js | 9 +++ test/form/side-effect-n/_expected/iife.js | 14 ++++ test/form/side-effect-n/_expected/umd.js | 17 +++++ test/form/side-effect-o/_expected/amd.js | 17 +++++ test/form/side-effect-o/_expected/cjs.js | 15 ++++ test/form/side-effect-o/_expected/es6.js | 13 ++++ test/form/side-effect-o/_expected/iife.js | 18 +++++ test/form/side-effect-o/_expected/umd.js | 21 ++++++ 12 files changed, 202 insertions(+), 71 deletions(-) create mode 100644 src/utils/pureFunctions.js create mode 100644 test/form/side-effect-n/_expected/amd.js create mode 100644 test/form/side-effect-n/_expected/cjs.js create mode 100644 test/form/side-effect-n/_expected/es6.js create mode 100644 test/form/side-effect-n/_expected/iife.js create mode 100644 test/form/side-effect-n/_expected/umd.js create mode 100644 test/form/side-effect-o/_expected/amd.js create mode 100644 test/form/side-effect-o/_expected/cjs.js create mode 100644 test/form/side-effect-o/_expected/es6.js create mode 100644 test/form/side-effect-o/_expected/iife.js create mode 100644 test/form/side-effect-o/_expected/umd.js diff --git a/src/utils/pureFunctions.js b/src/utils/pureFunctions.js new file mode 100644 index 0000000..3642a05 --- /dev/null +++ b/src/utils/pureFunctions.js @@ -0,0 +1,42 @@ +let pureFunctions = {}; + +const arrayTypes = 'Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array Float32Array Float64Array'.split( ' ' ); +const simdTypes = 'Int8x16 Int16x8 Int32x4 Float32x4 Float64x2'.split( ' ' ); +const simdMethods = 'abs add and bool check div equal extractLane fromFloat32x4 fromFloat32x4Bits fromFloat64x2 fromFloat64x2Bits fromInt16x8Bits fromInt32x4 fromInt32x4Bits fromInt8x16Bits greaterThan greaterThanOrEqual lessThan lessThanOrEqual load max maxNum min minNum mul neg not notEqual or reciprocalApproximation reciprocalSqrtApproximation replaceLane select selectBits shiftLeftByScalar shiftRightArithmeticByScalar shiftRightLogicalByScalar shuffle splat sqrt store sub swizzle xor'.split( ' ' ); +let allSimdMethods = []; +simdTypes.forEach( t => { + simdMethods.forEach( m => { + allSimdMethods.push( `SIMD.${t}.${m}` ); + }); +}); + +[ + 'Array.isArray', + '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', + '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', + 'Date', 'Date.UTC', 'Date.now', 'Date.parse', + 'String', 'String.fromCharCode', 'String.fromCodePoint', 'String.raw', + 'RegExp', + 'Map', 'Set', 'WeakMap', 'WeakSet', + 'ArrayBuffer', 'ArrayBuffer.isView', + 'DataView', + 'JSON.parse', 'JSON.stringify', + 'Promise', 'Promise.all', 'Promise.race', 'Promise.reject', 'Promise.resolve', + 'Intl.Collator', 'Intl.Collator.supportedLocalesOf', 'Intl.DateTimeFormat', 'Intl.DateTimeFormat.supportedLocalesOf', 'Intl.NumberFormat', 'Intl.NumberFormat.supportedLocalesOf' + + // TODO properties of e.g. window... +].concat( + arrayTypes, + arrayTypes.map( t => `${t}.from` ), + arrayTypes.map( t => `${t}.of` ), + simdTypes.map( t => `SIMD.${t}` ), + allSimdMethods +).forEach( name => pureFunctions[ name ] = true ); + // TODO add others to this list from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects + +export default pureFunctions; diff --git a/src/utils/run.js b/src/utils/run.js index 79724d3..bf35a73 100644 --- a/src/utils/run.js +++ b/src/utils/run.js @@ -2,67 +2,24 @@ import { walk } from 'estree-walker'; import modifierNodes, { isModifierNode } from '../ast/modifierNodes.js'; import isReference from '../ast/isReference.js'; import flatten from '../ast/flatten'; - -let pureFunctions = {}; - -const arrayTypes = 'Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array Float32Array Float64Array'.split( ' ' ); -const simdTypes = 'Int8x16 Int16x8 Int32x4 Float32x4 Float64x2'.split( ' ' ); -const simdMethods = 'abs add and bool check div equal extractLane fromFloat32x4 fromFloat32x4Bits fromFloat64x2 fromFloat64x2Bits fromInt16x8Bits fromInt32x4 fromInt32x4Bits fromInt8x16Bits greaterThan greaterThanOrEqual lessThan lessThanOrEqual load max maxNum min minNum mul neg not notEqual or reciprocalApproximation reciprocalSqrtApproximation replaceLane select selectBits shiftLeftByScalar shiftRightArithmeticByScalar shiftRightLogicalByScalar shuffle splat sqrt store sub swizzle xor'.split( ' ' ); -let allSimdMethods = []; -simdTypes.forEach( t => { - simdMethods.forEach( m => { - allSimdMethods.push( `SIMD.${t}.${m}` ); - }); -}); - -[ - 'Array.isArray', - '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', - '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', - 'Date', 'Date.UTC', 'Date.now', 'Date.parse', - 'String', 'String.fromCharCode', 'String.fromCodePoint', 'String.raw', - 'RegExp', - 'Map', 'Set', 'WeakMap', 'WeakSet', - 'ArrayBuffer', 'ArrayBuffer.isView', - 'DataView', - 'JSON.parse', 'JSON.stringify', - 'Promise', 'Promise.all', 'Promise.race', 'Promise.reject', 'Promise.resolve', - 'Intl.Collator', 'Intl.Collator.supportedLocalesOf', 'Intl.DateTimeFormat', 'Intl.DateTimeFormat.supportedLocalesOf', 'Intl.NumberFormat', 'Intl.NumberFormat.supportedLocalesOf' - - // TODO properties of e.g. window... -].concat( - arrayTypes, - arrayTypes.map( t => `${t}.from` ), - arrayTypes.map( t => `${t}.of` ), - simdTypes.map( t => `SIMD.${t}` ), - allSimdMethods -).forEach( name => pureFunctions[ name ] = true ); - // TODO add others to this list from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects +import pureFunctions from './pureFunctions.js'; function call ( callee, scope, statement, strongDependencies ) { - let hasSideEffect; - while ( callee.type === 'ParenthesizedExpression' ) callee = callee.expression; if ( callee.type === 'Identifier' ) { const declaration = scope.findDeclaration( callee.name ) || statement.module.trace( callee.name ); - if ( declaration ) { - if ( declaration.run( strongDependencies ) ) { - hasSideEffect = true; - } - } else if ( !pureFunctions[ callee.name ] ) { - hasSideEffect = true; - } + if ( declaration ) return declaration.run( strongDependencies ); + return !pureFunctions[ callee.name ]; + } + + if ( /FunctionExpression/.test( callee.type ) ) { + return run( callee.body, scope, statement, strongDependencies ); } - else if ( callee.type === 'MemberExpression' ) { + if ( callee.type === 'MemberExpression' ) { const flattened = flatten( callee ); if ( flattened ) { @@ -70,29 +27,13 @@ function call ( callee, scope, statement, strongDependencies ) { // TODO make pureFunctions configurable const declaration = scope.findDeclaration( flattened.name ) || statement.module.trace( flattened.name ); - if ( !!declaration || !pureFunctions[ flattened.keypath ] ) { - hasSideEffect = true; - } - } else { - // is not a keypath like `foo.bar.baz` – could be e.g. - // `foo[bar].baz()`. Err on the side of caution - hasSideEffect = true; - } - } - - else if ( /FunctionExpression/.test( callee.type ) ) { - if ( run( callee.body, scope, statement, strongDependencies ) ) { - hasSideEffect = true; + return ( !!declaration || !pureFunctions[ flattened.keypath ] ); } } - else { - // huh? - console.log( 'callee', callee ) - throw new Error( 'Cannot call a non-function' ); - } - - return hasSideEffect; + // complex case like `( a ? b : c )()` or foo[bar].baz()` + // – err on the side of caution + return true; } export default function run ( node, scope, statement, strongDependencies, force ) { diff --git a/test/form/side-effect-n/_expected/amd.js b/test/form/side-effect-n/_expected/amd.js new file mode 100644 index 0000000..22e9a23 --- /dev/null +++ b/test/form/side-effect-n/_expected/amd.js @@ -0,0 +1,13 @@ +define(function () { 'use strict'; + + function foo () { + console.log( 'foo' ); + } + + function bar () { + console.log( 'bar' ); + } + + ( Math.random() < 0.5 ? foo : bar )(); + +}); \ No newline at end of file diff --git a/test/form/side-effect-n/_expected/cjs.js b/test/form/side-effect-n/_expected/cjs.js new file mode 100644 index 0000000..5a79e81 --- /dev/null +++ b/test/form/side-effect-n/_expected/cjs.js @@ -0,0 +1,11 @@ +'use strict'; + +function foo () { + console.log( 'foo' ); +} + +function bar () { + console.log( 'bar' ); +} + +( Math.random() < 0.5 ? foo : bar )(); \ No newline at end of file diff --git a/test/form/side-effect-n/_expected/es6.js b/test/form/side-effect-n/_expected/es6.js new file mode 100644 index 0000000..47c7743 --- /dev/null +++ b/test/form/side-effect-n/_expected/es6.js @@ -0,0 +1,9 @@ +function foo () { + console.log( 'foo' ); +} + +function bar () { + console.log( 'bar' ); +} + +( Math.random() < 0.5 ? foo : bar )(); \ No newline at end of file diff --git a/test/form/side-effect-n/_expected/iife.js b/test/form/side-effect-n/_expected/iife.js new file mode 100644 index 0000000..23d5f37 --- /dev/null +++ b/test/form/side-effect-n/_expected/iife.js @@ -0,0 +1,14 @@ +(function () { + 'use strict'; + + function foo () { + console.log( 'foo' ); + } + + function bar () { + console.log( 'bar' ); + } + + ( Math.random() < 0.5 ? foo : bar )(); + +}()); \ No newline at end of file diff --git a/test/form/side-effect-n/_expected/umd.js b/test/form/side-effect-n/_expected/umd.js new file mode 100644 index 0000000..c792f48 --- /dev/null +++ b/test/form/side-effect-n/_expected/umd.js @@ -0,0 +1,17 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, function () { 'use strict'; + + function foo () { + console.log( 'foo' ); + } + + function bar () { + console.log( 'bar' ); + } + + ( Math.random() < 0.5 ? foo : bar )(); + +})); \ No newline at end of file diff --git a/test/form/side-effect-o/_expected/amd.js b/test/form/side-effect-o/_expected/amd.js new file mode 100644 index 0000000..7989486 --- /dev/null +++ b/test/form/side-effect-o/_expected/amd.js @@ -0,0 +1,17 @@ +define(function () { 'use strict'; + + function fn () { + return Math.random() < 0.5 ? foo : bar; + } + + function foo () { + console.log( 'foo' ); + } + + function bar () { + console.log( 'bar' ); + } + + fn()(); + +}); \ No newline at end of file diff --git a/test/form/side-effect-o/_expected/cjs.js b/test/form/side-effect-o/_expected/cjs.js new file mode 100644 index 0000000..3565359 --- /dev/null +++ b/test/form/side-effect-o/_expected/cjs.js @@ -0,0 +1,15 @@ +'use strict'; + +function fn () { + return Math.random() < 0.5 ? foo : bar; +} + +function foo () { + console.log( 'foo' ); +} + +function bar () { + console.log( 'bar' ); +} + +fn()(); \ No newline at end of file diff --git a/test/form/side-effect-o/_expected/es6.js b/test/form/side-effect-o/_expected/es6.js new file mode 100644 index 0000000..93ed395 --- /dev/null +++ b/test/form/side-effect-o/_expected/es6.js @@ -0,0 +1,13 @@ +function fn () { + return Math.random() < 0.5 ? foo : bar; +} + +function foo () { + console.log( 'foo' ); +} + +function bar () { + console.log( 'bar' ); +} + +fn()(); \ No newline at end of file diff --git a/test/form/side-effect-o/_expected/iife.js b/test/form/side-effect-o/_expected/iife.js new file mode 100644 index 0000000..d3b3b18 --- /dev/null +++ b/test/form/side-effect-o/_expected/iife.js @@ -0,0 +1,18 @@ +(function () { + 'use strict'; + + function fn () { + return Math.random() < 0.5 ? foo : bar; + } + + function foo () { + console.log( 'foo' ); + } + + function bar () { + console.log( 'bar' ); + } + + fn()(); + +}()); \ No newline at end of file diff --git a/test/form/side-effect-o/_expected/umd.js b/test/form/side-effect-o/_expected/umd.js new file mode 100644 index 0000000..80d69a3 --- /dev/null +++ b/test/form/side-effect-o/_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 fn () { + return Math.random() < 0.5 ? foo : bar; + } + + function foo () { + console.log( 'foo' ); + } + + function bar () { + console.log( 'bar' ); + } + + fn()(); + +})); \ No newline at end of file