Browse Source

merge master -> gh-425

gh-669
Rich-Harris 9 years ago
parent
commit
a29d941608
  1. 14
      CHANGELOG.md
  2. 2
      package.json
  3. 125
      src/Bundle.js
  4. 25
      src/Declaration.js
  5. 71
      src/Module.js
  6. 30
      src/Reference.js
  7. 29
      src/Statement.js
  8. 4
      src/ast/isReference.js
  9. 42
      src/utils/pureFunctions.js
  10. 102
      src/utils/run.js
  11. 6
      test/form/computed-properties/_config.js
  12. 9
      test/form/computed-properties/_expected/amd.js
  13. 7
      test/form/computed-properties/_expected/cjs.js
  14. 5
      test/form/computed-properties/_expected/es6.js
  15. 10
      test/form/computed-properties/_expected/iife.js
  16. 13
      test/form/computed-properties/_expected/umd.js
  17. 3
      test/form/computed-properties/main.js
  18. 6
      test/form/external-import-alias-shadow/_config.js
  19. 9
      test/form/external-import-alias-shadow/_expected/amd.js
  20. 9
      test/form/external-import-alias-shadow/_expected/cjs.js
  21. 7
      test/form/external-import-alias-shadow/_expected/es6.js
  22. 10
      test/form/external-import-alias-shadow/_expected/iife.js
  23. 13
      test/form/external-import-alias-shadow/_expected/umd.js
  24. 3
      test/form/external-import-alias-shadow/main.js
  25. 5
      test/form/external-import-alias-shadow/parse.js
  26. 2
      test/form/object-destructuring-default-values/_config.js
  27. 3
      test/form/side-effect-n/_config.js
  28. 13
      test/form/side-effect-n/_expected/amd.js
  29. 11
      test/form/side-effect-n/_expected/cjs.js
  30. 9
      test/form/side-effect-n/_expected/es6.js
  31. 14
      test/form/side-effect-n/_expected/iife.js
  32. 17
      test/form/side-effect-n/_expected/umd.js
  33. 9
      test/form/side-effect-n/main.js
  34. 3
      test/form/side-effect-o/_config.js
  35. 17
      test/form/side-effect-o/_expected/amd.js
  36. 15
      test/form/side-effect-o/_expected/cjs.js
  37. 13
      test/form/side-effect-o/_expected/es6.js
  38. 18
      test/form/side-effect-o/_expected/iife.js
  39. 21
      test/form/side-effect-o/_expected/umd.js
  40. 13
      test/form/side-effect-o/main.js
  41. 3
      test/function/call-non-function-default-exports/_config.js
  42. 9
      test/function/call-non-function-default-exports/foo.js
  43. 4
      test/function/call-non-function-default-exports/main.js
  44. 21
      test/function/cycles-pathological/_config.js
  45. 10
      test/function/deconstructed-exported-vars/_config.js
  46. 11
      test/function/deconstructed-exported-vars/main.js
  47. 3
      test/function/default-exports-in-parens/_config.js
  48. 3
      test/function/default-exports-in-parens/foo.js
  49. 3
      test/function/default-exports-in-parens/main.js
  50. 2
      test/function/iife-comments/_config.js
  51. 19
      test/function/iife-strong-dependencies/_config.js
  52. 3
      test/function/import-chain-as/_config.js
  53. 1
      test/function/import-chain-as/first.js
  54. 5
      test/function/import-chain-as/main.js
  55. 2
      test/function/import-chain-as/second.js
  56. 8
      test/function/removes-empty-exported-vars/_config.js
  57. 4
      test/function/removes-empty-exported-vars/main.js

14
CHANGELOG.md

@ -1,5 +1,19 @@
# rollup changelog # rollup changelog
## 0.25.0
* **breaking** Module order is determined according to spec, rather than in a way designed to prevent runtime errors. Rollup will warn if it detects runtime errors are likely ([#435](https://github.com/rollup/rollup/issues/435))
* Prevent overly aggressive tree-shaking with complex call expressions ([#440](https://github.com/rollup/rollup/issues/440))
## 0.24.1
* Handle calls to default exports other that are not function expressions or references to function declarations ([#421](https://github.com/rollup/rollup/issues/421))
* Ensure namespace blocks are created for chained imports ([#430](https://github.com/rollup/rollup/issues/430))
* Include references in computed property keys ([#434](https://github.com/rollup/rollup/issues/434))
* Use CLI `--external` option correctly ([#417](https://github.com/rollup/rollup/pull/417))
* Allow relative imports to be treated as external, if absolute paths are provided in `options.external` ([#410](https://github.com/rollup/rollup/issues/410))
* Make IIFE output adhere to Crockford style ([#415](https://github.com/rollup/rollup/pull/415))
## 0.24.0 ## 0.24.0
* No longer attempts to resolve IDs in `options.external` ([#407](https://github.com/rollup/rollup/issues/407)) * No longer attempts to resolve IDs in `options.external` ([#407](https://github.com/rollup/rollup/issues/407))

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "rollup", "name": "rollup",
"version": "0.24.0", "version": "0.25.0",
"description": "Next-generation ES6 module bundler", "description": "Next-generation ES6 module bundler",
"main": "dist/rollup.js", "main": "dist/rollup.js",
"jsnext:main": "src/rollup.js", "jsnext:main": "src/rollup.js",

125
src/Bundle.js

@ -127,6 +127,13 @@ export default class Bundle {
this.externalModules.forEach( module => { this.externalModules.forEach( module => {
module.name = getSafeName( module.name ); module.name = getSafeName( module.name );
// ensure we don't shadow named external imports, if
// we're creating an ES6 bundle
keys( module.declarations ).forEach( name => {
const declaration = module.declarations[ name ];
declaration.setSafeName( getSafeName( name ) );
});
}); });
this.modules.forEach( module => { this.modules.forEach( module => {
@ -169,7 +176,7 @@ export default class Bundle {
} }
fetchAllDependencies ( module ) { fetchAllDependencies ( module ) {
const promises = module.dependencies.map( source => { const promises = module.sources.map( source => {
return this.resolveId( source, module.id ) return this.resolveId( source, module.id )
.then( resolvedId => { .then( resolvedId => {
// If the `resolvedId` is supposed to be external, make it so. // If the `resolvedId` is supposed to be external, make it so.
@ -277,85 +284,79 @@ export default class Bundle {
sort () { sort () {
let seen = {}; let seen = {};
let ordered = [];
let hasCycles; let hasCycles;
let ordered = [];
let strongDeps = {}; let stronglyDependsOn = blank();
let stronglyDependsOn = {}; let dependsOn = blank();
function visit ( module ) {
if ( seen[ module.id ] ) return;
seen[ module.id ] = true;
const { strongDependencies, weakDependencies } = module.consolidateDependencies();
strongDeps[ module.id ] = [];
stronglyDependsOn[ module.id ] = {};
strongDependencies.forEach( imported => { this.modules.forEach( module => {
strongDeps[ module.id ].push( imported ); stronglyDependsOn[ module.id ] = blank();
dependsOn[ module.id ] = blank();
});
if ( seen[ imported.id ] ) { this.modules.forEach( module => {
// we need to prevent an infinite loop, and note that function processStrongDependency ( dependency ) {
// we need to check for strong/weak dependency relationships if ( dependency === module || stronglyDependsOn[ module.id ][ dependency.id ] ) return;
hasCycles = true;
return;
}
visit( imported ); stronglyDependsOn[ module.id ][ dependency.id ] = true;
}); dependency.strongDependencies.forEach( processStrongDependency );
}
weakDependencies.forEach( imported => { function processDependency ( dependency ) {
if ( seen[ imported.id ] ) { if ( dependency === module || dependsOn[ module.id ][ dependency.id ] ) return;
// we need to prevent an infinite loop, and note that
// we need to check for strong/weak dependency relationships
hasCycles = true;
return;
}
visit( imported ); dependsOn[ module.id ][ dependency.id ] = true;
}); dependency.dependencies.forEach( processDependency );
}
// add second (and third...) order dependencies module.strongDependencies.forEach( processStrongDependency );
function addStrongDependencies ( dependency ) { module.dependencies.forEach( processDependency );
if ( stronglyDependsOn[ module.id ][ dependency.id ] ) return; });
stronglyDependsOn[ module.id ][ dependency.id ] = true; const visit = module => {
strongDeps[ dependency.id ].forEach( addStrongDependencies ); if ( seen[ module.id ] ) {
hasCycles = true;
return;
} }
strongDeps[ module.id ].forEach( addStrongDependencies ); seen[ module.id ] = true;
module.dependencies.forEach( visit );
ordered.push( module ); ordered.push( module );
} };
this.modules.forEach( visit ); visit( this.entryModule );
if ( hasCycles ) { if ( hasCycles ) {
let unordered = ordered; ordered.forEach( ( a, i ) => {
ordered = []; for ( i += 1; i < ordered.length; i += 1 ) {
const b = ordered[i];
let placed = blank();
if ( stronglyDependsOn[ a.id ][ b.id ] ) {
// unordered is actually semi-ordered, as [ fewer dependencies ... more dependencies ] // somewhere, there is a module that imports b before a. Because
unordered.forEach( module => { // b imports a, a is placed before b. We need to find the module
// ensure strong dependencies of `module` that don't strongly depend on `module` go first // in question, so we can provide a useful error message
strongDeps[ module.id ].forEach( place ); let parent = '[[unknown]]';
function place ( dep ) { const findParent = module => {
if ( placed[ dep.id ] ) return; if ( dependsOn[ module.id ][ a.id ] && dependsOn[ module.id ][ b.id ] ) {
placed[ dep.id ] = true; parent = module.id;
} else {
if ( !stronglyDependsOn[ dep.id ][ module.id ] ) { for ( let i = 0; i < module.dependencies.length; i += 1 ) {
strongDeps[ dep.id ].forEach( place ); const dependency = module.dependencies[i];
ordered.push( dep ); if ( findParent( dependency ) ) return;
}
}
};
findParent( this.entryModule );
this.onwarn(
`Module ${a.id} may be unable to evaluate without ${b.id}, but is included first due to a cyclical dependency. Consider swapping the import statements in ${parent} to ensure correct ordering`
);
} }
} }
if ( !~ordered.indexOf( module ) ) {
placed[ module.id ] = true;
ordered.push( module );
}
}); });
} }

25
src/Declaration.js

@ -1,5 +1,6 @@
import { blank, keys } from './utils/object.js'; import { blank, keys } from './utils/object.js';
import run from './utils/run.js'; import run from './utils/run.js';
import { SyntheticReference } from './Reference.js';
export default class Declaration { export default class Declaration {
constructor ( node, isParam, statement ) { constructor ( node, isParam, statement ) {
@ -110,9 +111,15 @@ export class SyntheticDefaultDeclaration {
return this.original.run( strongDependencies ); return this.original.run( strongDependencies );
} }
if ( /FunctionExpression/.test( this.node.declaration.type ) ) { let declaration = this.node.declaration;
return run( this.node.declaration.body, this.statement.scope, this.statement, strongDependencies, false ); while ( declaration.type === 'ParenthesizedExpression' ) declaration = declaration.expression;
if ( /FunctionExpression/.test( declaration.type ) ) {
return run( declaration.body, this.statement.scope, this.statement, strongDependencies, false );
} }
// otherwise assume the worst
return true;
} }
use () { use () {
@ -170,6 +177,12 @@ export class SyntheticNamespaceDeclaration {
if ( !this.needsNamespaceBlock ) { if ( !this.needsNamespaceBlock ) {
this.needsNamespaceBlock = true; this.needsNamespaceBlock = true;
this.module.bundle.internalNamespaces.push( this ); this.module.bundle.internalNamespaces.push( this );
// add synthetic references, in case of chained
// namespace imports
keys( this.originals ).forEach( name => {
this.originals[ name ].addReference( new SyntheticReference( name ) );
});
} }
reference.declaration = this; reference.declaration = this;
@ -208,6 +221,8 @@ export class ExternalDeclaration {
this.module = module; this.module = module;
this.name = name; this.name = name;
this.isExternal = true; this.isExternal = true;
this.safeName = null;
} }
addAlias () { addAlias () {
@ -233,13 +248,17 @@ export class ExternalDeclaration {
this.module.name; this.module.name;
} }
return es6 ? this.name : `${this.module.name}.${this.name}`; return es6 ? this.safeName : `${this.module.name}.${this.name}`;
} }
run () { run () {
return true; return true;
} }
setSafeName ( name ) {
this.safeName = name;
}
use () { use () {
// noop? // noop?
} }

71
src/Module.js

@ -22,6 +22,7 @@ export default class Module {
this.id = id; this.id = id;
// all dependencies // all dependencies
this.sources = [];
this.dependencies = []; this.dependencies = [];
this.resolvedIds = blank(); this.resolvedIds = blank();
@ -62,7 +63,7 @@ export default class Module {
// export { name } from './other.js' // export { name } from './other.js'
if ( source ) { if ( source ) {
if ( !~this.dependencies.indexOf( source ) ) this.dependencies.push( source ); if ( !~this.sources.indexOf( source ) ) this.sources.push( source );
if ( node.type === 'ExportAllDeclaration' ) { if ( node.type === 'ExportAllDeclaration' ) {
// Store `export * from '...'` statements in an array of delegates. // Store `export * from '...'` statements in an array of delegates.
@ -136,7 +137,7 @@ export default class Module {
const node = statement.node; const node = statement.node;
const source = node.source.value; const source = node.source.value;
if ( !~this.dependencies.indexOf( source ) ) this.dependencies.push( source ); if ( !~this.sources.indexOf( source ) ) this.sources.push( source );
node.specifiers.forEach( specifier => { node.specifiers.forEach( specifier => {
const localName = specifier.local.name; const localName = specifier.local.name;
@ -212,6 +213,13 @@ export default class Module {
const id = this.resolvedIds[ source ]; const id = this.resolvedIds[ source ];
return this.bundle.moduleById[ id ]; return this.bundle.moduleById[ id ];
}); });
this.sources.forEach( source => {
const id = this.resolvedIds[ source ];
const module = this.bundle.moduleById[ id ];
if ( !module.isExternal ) this.dependencies.push( module );
});
} }
bindReferences () { bindReferences () {
@ -243,26 +251,6 @@ export default class Module {
}); });
} }
consolidateDependencies () {
let strongDependencies = [];
let weakDependencies = [];
// treat all imports as weak dependencies
this.dependencies.forEach( source => {
const id = this.resolvedIds[ source ];
const dependency = this.bundle.moduleById[ id ];
if ( !dependency.isExternal && !~weakDependencies.indexOf( dependency ) ) {
weakDependencies.push( dependency );
}
});
strongDependencies = this.strongDependencies
.filter( module => module !== this );
return { strongDependencies, weakDependencies };
}
getExports () { getExports () {
let exports = blank(); let exports = blank();
@ -445,14 +433,37 @@ export default class Module {
} }
// split up/remove var declarations as necessary // split up/remove var declarations as necessary
if ( statement.node.isSynthetic ) { if ( statement.node.type === 'VariableDeclaration' ) {
// insert `var/let/const` if necessary const declarator = statement.node.declarations[0];
const declaration = this.declarations[ statement.node.declarations[0].id.name ];
if ( !( declaration.isExported && declaration.isReassigned ) ) { // TODO encapsulate this if ( declarator.id.type === 'Identifier' ) {
magicString.insert( statement.start, `${statement.node.kind} ` ); const declaration = this.declarations[ declarator.id.name ];
if ( declaration.isExported && declaration.isReassigned ) { // `var foo = ...` becomes `exports.foo = ...`
magicString.remove( statement.start, declarator.init ? declarator.start : statement.next );
return;
}
} }
magicString.overwrite( statement.end, statement.next, ';\n' ); // TODO account for trailing newlines else {
// we handle destructuring differently, because whereas we can rewrite
// `var foo = ...` as `exports.foo = ...`, in a case like `var { a, b } = c()`
// where `a` or `b` is exported and reassigned, we have to append
// `exports.a = a;` and `exports.b = b` instead
extractNames( declarator.id ).forEach( name => {
const declaration = this.declarations[ name ];
if ( declaration.isExported && declaration.isReassigned ) {
magicString.insert( statement.end, `;\nexports.${name} = ${declaration.render( es6 )}` );
}
});
}
if ( statement.node.isSynthetic ) {
// insert `var/let/const` if necessary
magicString.insert( statement.start, `${statement.node.kind} ` );
magicString.overwrite( statement.end, statement.next, ';\n' ); // TODO account for trailing newlines
}
} }
let toDeshadow = blank(); let toDeshadow = blank();
@ -566,11 +577,11 @@ export default class Module {
return magicString.trim(); return magicString.trim();
} }
run ( safe ) { run () {
let marked = false; let marked = false;
this.statements.forEach( statement => { this.statements.forEach( statement => {
marked = statement.run( this.strongDependencies, safe ) || marked; marked = statement.run( this.strongDependencies ) || marked;
}); });
return marked; return marked;

30
src/Reference.js

@ -0,0 +1,30 @@
export class Reference {
constructor ( node, scope, statement ) {
this.node = node;
this.scope = scope;
this.statement = statement;
this.declaration = null; // bound later
this.parts = [];
let root = node;
while ( root.type === 'MemberExpression' ) {
this.parts.unshift( root.property.name );
root = root.object;
}
this.name = root.name;
this.start = node.start;
this.end = node.start + this.name.length; // can be overridden in the case of namespace members
this.rewritten = false;
}
}
export class SyntheticReference {
constructor ( name ) {
this.name = name;
this.parts = [];
}
}

29
src/Statement.js

@ -6,30 +6,7 @@ import isFunctionDeclaration from './ast/isFunctionDeclaration.js';
import isReference from './ast/isReference.js'; import isReference from './ast/isReference.js';
import getLocation from './utils/getLocation.js'; import getLocation from './utils/getLocation.js';
import run from './utils/run.js'; import run from './utils/run.js';
import { Reference } from './Reference.js';
class Reference {
constructor ( node, scope, statement ) {
this.node = node;
this.scope = scope;
this.statement = statement;
this.declaration = null; // bound later
this.parts = [];
let root = node;
while ( root.type === 'MemberExpression' ) {
this.parts.unshift( root.property.name );
root = root.object;
}
this.name = root.name;
this.start = node.start;
this.end = node.start + this.name.length; // can be overridden in the case of namespace members
this.rewritten = false;
}
}
export default class Statement { export default class Statement {
constructor ( node, module, start, end ) { constructor ( node, module, start, end ) {
@ -153,11 +130,11 @@ export default class Statement {
}); });
} }
run ( strongDependencies, safe ) { run ( strongDependencies ) {
if ( ( this.ran && this.isIncluded ) || this.isImportDeclaration || this.isFunctionDeclaration ) return; if ( ( this.ran && this.isIncluded ) || this.isImportDeclaration || this.isFunctionDeclaration ) return;
this.ran = true; this.ran = true;
if ( run( this.node, this.scope, this, strongDependencies, false, safe ) ) { if ( run( this.node, this.scope, this, strongDependencies, false ) ) {
this.mark(); this.mark();
return true; return true;
} }

4
src/ast/isReference.js

@ -12,8 +12,8 @@ export default function isReference ( node, parent ) {
// TODO is this right? // TODO is this right?
if ( parent.type === 'MemberExpression' ) return parent.computed || node === parent.object; if ( parent.type === 'MemberExpression' ) return parent.computed || node === parent.object;
// disregard the `bar` in { bar: foo } // disregard the `bar` in `{ bar: foo }`, but keep it in `{ [bar]: foo }`
if ( parent.type === 'Property' && node !== parent.value ) return false; if ( parent.type === 'Property' ) return parent.computed || node === parent.value;
// disregard the `bar` in `class Foo { bar () {...} }` // disregard the `bar` in `class Foo { bar () {...} }`
if ( parent.type === 'MethodDefinition' ) return false; if ( parent.type === 'MethodDefinition' ) return false;

42
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;

102
src/utils/run.js

@ -2,49 +2,39 @@ import { walk } from 'estree-walker';
import modifierNodes, { isModifierNode } from '../ast/modifierNodes.js'; import modifierNodes, { isModifierNode } from '../ast/modifierNodes.js';
import isReference from '../ast/isReference.js'; import isReference from '../ast/isReference.js';
import flatten from '../ast/flatten'; import flatten from '../ast/flatten';
import pureFunctions from './pureFunctions.js';
let pureFunctions = {}; function call ( callee, scope, statement, strongDependencies ) {
while ( callee.type === 'ParenthesizedExpression' ) callee = callee.expression;
const arrayTypes = 'Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array Float32Array Float64Array'.split( ' ' ); if ( callee.type === 'Identifier' ) {
const simdTypes = 'Int8x16 Int16x8 Int32x4 Float32x4 Float64x2'.split( ' ' ); const declaration = scope.findDeclaration( callee.name ) ||
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( ' ' ); statement.module.trace( callee.name );
let allSimdMethods = [];
simdTypes.forEach( t => { if ( declaration ) return declaration.run( strongDependencies );
simdMethods.forEach( m => { return !pureFunctions[ callee.name ];
allSimdMethods.push( `SIMD.${t}.${m}` ); }
});
}); if ( /FunctionExpression/.test( callee.type ) ) {
return run( callee.body, scope, statement, strongDependencies );
[ }
'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
if ( callee.type === 'MemberExpression' ) {
const flattened = flatten( callee );
if ( flattened ) {
// if we're calling e.g. Object.keys(thing), there are no side-effects
// TODO make pureFunctions configurable
const declaration = scope.findDeclaration( flattened.name ) || statement.module.trace( flattened.name );
return ( !!declaration || !pureFunctions[ flattened.keypath ] );
}
}
// 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 ) { export default function run ( node, scope, statement, strongDependencies, force ) {
let hasSideEffect = false; let hasSideEffect = false;
@ -78,39 +68,7 @@ export default function run ( node, scope, statement, strongDependencies, force
} }
else if ( node.type === 'CallExpression' || node.type === 'NewExpression' ) { else if ( node.type === 'CallExpression' || node.type === 'NewExpression' ) {
if ( node.callee.type === 'Identifier' ) { if ( call( node.callee, scope, statement, strongDependencies ) ) {
const declaration = scope.findDeclaration( node.callee.name ) ||
statement.module.trace( node.callee.name );
if ( declaration ) {
if ( declaration.run( strongDependencies ) ) {
hasSideEffect = true;
}
} else if ( !pureFunctions[ node.callee.name ] ) {
hasSideEffect = true;
}
}
else if ( node.callee.type === 'MemberExpression' ) {
const flattened = flatten( node.callee );
if ( flattened ) {
// if we're calling e.g. Object.keys(thing), there are no side-effects
// 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;
}
}
// otherwise we're probably dealing with a function expression
else if ( run( node.callee, scope, statement, strongDependencies, true ) ) {
hasSideEffect = true; hasSideEffect = true;
} }
} }

6
test/form/computed-properties/_config.js

@ -0,0 +1,6 @@
module.exports = {
description: 'computed property keys include declarations of referenced identifiers',
options: {
moduleName: 'computedProperties'
}
};

9
test/form/computed-properties/_expected/amd.js

@ -0,0 +1,9 @@
define(['exports'], function (exports) { 'use strict';
var foo = 'foo';
var x = {[foo]: 'bar'};
exports.x = x;
});

7
test/form/computed-properties/_expected/cjs.js

@ -0,0 +1,7 @@
'use strict';
var foo = 'foo';
var x = {[foo]: 'bar'};
exports.x = x;

5
test/form/computed-properties/_expected/es6.js

@ -0,0 +1,5 @@
var foo = 'foo';
var x = {[foo]: 'bar'};
export { x };

10
test/form/computed-properties/_expected/iife.js

@ -0,0 +1,10 @@
(function (exports) {
'use strict';
var foo = 'foo';
var x = {[foo]: 'bar'};
exports.x = x;
}((this.computedProperties = {})));

13
test/form/computed-properties/_expected/umd.js

@ -0,0 +1,13 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(factory((global.computedProperties = {})));
}(this, function (exports) { 'use strict';
var foo = 'foo';
var x = {[foo]: 'bar'};
exports.x = x;
}));

3
test/form/computed-properties/main.js

@ -0,0 +1,3 @@
var foo = 'foo';
export var x = {[foo]: 'bar'};

6
test/form/external-import-alias-shadow/_config.js

@ -0,0 +1,6 @@
module.exports = {
description: 'handles external aliased named imports that shadow another name',
options: {
external: [ 'acorn' ]
}
};

9
test/form/external-import-alias-shadow/_expected/amd.js

@ -0,0 +1,9 @@
define(['acorn'], function (acorn) { 'use strict';
function parse$1(source) {
return acorn.parse(source, { ecmaVersion: 6 });
}
console.log(parse$1('foo'));
});

9
test/form/external-import-alias-shadow/_expected/cjs.js

@ -0,0 +1,9 @@
'use strict';
var acorn = require('acorn');
function parse$1(source) {
return acorn.parse(source, { ecmaVersion: 6 });
}
console.log(parse$1('foo'));

7
test/form/external-import-alias-shadow/_expected/es6.js

@ -0,0 +1,7 @@
import { parse } from 'acorn';
function parse$1(source) {
return parse(source, { ecmaVersion: 6 });
}
console.log(parse$1('foo'));

10
test/form/external-import-alias-shadow/_expected/iife.js

@ -0,0 +1,10 @@
(function (acorn) {
'use strict';
function parse$1(source) {
return acorn.parse(source, { ecmaVersion: 6 });
}
console.log(parse$1('foo'));
}(acorn));

13
test/form/external-import-alias-shadow/_expected/umd.js

@ -0,0 +1,13 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('acorn')) :
typeof define === 'function' && define.amd ? define(['acorn'], factory) :
(factory(global.acorn));
}(this, function (acorn) { 'use strict';
function parse$1(source) {
return acorn.parse(source, { ecmaVersion: 6 });
}
console.log(parse$1('foo'));
}));

3
test/form/external-import-alias-shadow/main.js

@ -0,0 +1,3 @@
import parse from './parse';
console.log(parse('foo'));

5
test/form/external-import-alias-shadow/parse.js

@ -0,0 +1,5 @@
import { parse as acornParse } from 'acorn';
export default function parse(source) {
return acornParse(source, { ecmaVersion: 6 });
}

2
test/form/object-destructuring-default-values/_config.js

@ -1,3 +1,3 @@
module.exports = { module.exports = {
description: 'object destructuring default values are preserved' description: 'object destructuring default values are preserved'
}; };

3
test/form/side-effect-n/_config.js

@ -0,0 +1,3 @@
module.exports = {
description: 'detects side-effects in complex call expressions'
};

13
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 )();
});

11
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 )();

9
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 )();

14
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 )();
}());

17
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 )();
}));

9
test/form/side-effect-n/main.js

@ -0,0 +1,9 @@
function foo () {
console.log( 'foo' );
}
function bar () {
console.log( 'bar' );
}
( Math.random() < 0.5 ? foo : bar )();

3
test/form/side-effect-o/_config.js

@ -0,0 +1,3 @@
module.exports = {
description: 'detects side-effects in complex call expressions'
};

17
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()();
});

15
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()();

13
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()();

18
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()();
}());

21
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()();
}));

13
test/form/side-effect-o/main.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()();

3
test/function/call-non-function-default-exports/_config.js

@ -0,0 +1,3 @@
module.exports = {
description: 'calls non-function default exports'
};

9
test/function/call-non-function-default-exports/foo.js

@ -0,0 +1,9 @@
function x () {
global.answer = 'x';
}
function y () {
global.answer = 'y';
}
export default Math.random() < 0.5 ? x : y;

4
test/function/call-non-function-default-exports/main.js

@ -0,0 +1,4 @@
import foo from './foo.js';
foo();
assert.ok( /[xy]/.test( global.answer ) );

21
test/function/cycles-pathological/_config.js

@ -1,18 +1,17 @@
var assert = require( 'assert' ); var assert = require( 'assert' );
var warned;
module.exports = { module.exports = {
description: 'resolves pathological cyclical dependencies gracefully', description: 'resolves pathological cyclical dependencies gracefully',
babel: true, babel: true,
exports: function ( exports ) { options: {
assert.ok( exports.a.isA ); onwarn: function ( message ) {
assert.ok( exports.b1.isA ); assert.ok( /Module .+B\.js may be unable to evaluate without .+A\.js, but is included first due to a cyclical dependency. Consider swapping the import statements in .+main\.js to ensure correct ordering/.test( message ) );
assert.ok( exports.b1.isB ); warned = true;
assert.ok( exports.b2.isA ); }
assert.ok( exports.b2.isB ); },
assert.ok( exports.c1.isC ); runtimeError: function () {
assert.ok( exports.c1.isD ); assert.ok( warned );
assert.ok( exports.c2.isC );
assert.ok( exports.c2.isD );
assert.ok( exports.d.isD );
} }
}; };

10
test/function/deconstructed-exported-vars/_config.js

@ -0,0 +1,10 @@
var assert = require( 'assert' );
module.exports = {
description: 'allows destructuring in exported variable declarations, synthetic or otherwise',
babel: true,
exports: function ( exports ) {
assert.equal( exports.a, 1 );
assert.equal( exports.d, 4 );
}
};

11
test/function/deconstructed-exported-vars/main.js

@ -0,0 +1,11 @@
let { a, b } = c(), [ d, e ] = f();
function c () {
return { a: 1, b: 2 };
}
function f () {
return [ 4, 5 ];
}
export { a, d };

3
test/function/default-exports-in-parens/_config.js

@ -0,0 +1,3 @@
module.exports = {
description: 'handles default function exports wrapped in parens'
};

3
test/function/default-exports-in-parens/foo.js

@ -0,0 +1,3 @@
export default (function () {
global.answer = 42;
});

3
test/function/default-exports-in-parens/main.js

@ -0,0 +1,3 @@
import foo from './foo.js';
foo();
assert.equal( global.answer, 42 );

2
test/function/iife-comments/_config.js

@ -5,4 +5,4 @@ module.exports = {
exports: function ( exports ) { exports: function ( exports ) {
assert.equal( exports, 42 ); assert.equal( exports, 42 );
} }
} };

19
test/function/iife-strong-dependencies/_config.js

@ -1,15 +1,16 @@
var assert = require( 'assert' ); var assert = require( 'assert' );
var warned;
module.exports = { module.exports = {
description: 'does not treat references inside IIFEs as weak dependencies', // edge case encountered in THREE.js codebase description: 'does not treat references inside IIFEs as weak dependencies', // edge case encountered in THREE.js codebase
exports: function ( exports ) { options: {
assert.ok( exports.a1.isA ); onwarn: function ( message ) {
assert.ok( exports.b1.isB ); assert.ok( /Module .+D\.js may be unable to evaluate without .+C\.js, but is included first due to a cyclical dependency. Consider swapping the import statements in .+main\.js to ensure correct ordering/.test( message ) );
assert.ok( exports.c1.isC ); warned = true;
assert.ok( exports.d1.isD ); }
assert.ok( exports.a2.isA ); },
assert.ok( exports.b2.isB ); runtimeError: function () {
assert.ok( exports.c2.isC ); assert.ok( warned );
assert.ok( exports.d2.isD );
} }
}; };

3
test/function/import-chain-as/_config.js

@ -0,0 +1,3 @@
module.exports = {
description: 'imports as- chained exports'
};

1
test/function/import-chain-as/first.js

@ -0,0 +1 @@
export var value = 42;

5
test/function/import-chain-as/main.js

@ -0,0 +1,5 @@
import * as second from './second';
assert.equal( second.first.value, 42 );
console.log( 'second', second )
assert.deepEqual( second, { first: { value: 42 } });

2
test/function/import-chain-as/second.js

@ -0,0 +1,2 @@
import * as first from './first';
export { first };

8
test/function/removes-empty-exported-vars/_config.js

@ -0,0 +1,8 @@
var assert = require( 'assert' );
module.exports = {
description: 'removes empty exported var declarations',
exports: function ( exports ) {
assert.equal( exports.foo, 42 );
}
};

4
test/function/removes-empty-exported-vars/main.js

@ -0,0 +1,4 @@
var foo;
foo = 42;
export { foo };
Loading…
Cancel
Save