Browse Source

use Object.create(null) to avoid conflicts with Object.prototype. fixes #12

contingency-plan
Rich-Harris 10 years ago
parent
commit
315510f767
  1. 20
      src/Bundle.js
  2. 6
      src/ExternalModule.js
  3. 36
      src/Module.js
  4. 14
      src/Statement.js
  5. 6
      src/ast/walk.js
  6. 3
      src/finalisers/amd.js
  7. 6
      src/finalisers/iife.js
  8. 8
      src/finalisers/umd.js
  9. 4
      src/utils/makeLegalIdentifier.js
  10. 6
      src/utils/object.js
  11. 3
      test/function/object-prototype-properties/_config.js
  12. 7
      test/function/object-prototype-properties/foo.js
  13. 3
      test/function/object-prototype-properties/main.js

20
src/Bundle.js

@ -1,7 +1,7 @@
import { basename, dirname, extname, relative, resolve } from 'path'; import { basename, dirname, extname, relative, resolve } from 'path';
import { readFile, Promise } from 'sander'; import { readFile, Promise } from 'sander';
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import { keys, has } from './utils/object'; import { blank, keys } from './utils/object';
import Module from './Module'; import Module from './Module';
import ExternalModule from './ExternalModule'; import ExternalModule from './ExternalModule';
import finalisers from './finalisers/index'; import finalisers from './finalisers/index';
@ -32,7 +32,7 @@ export default class Bundle {
}; };
this.entryModule = null; this.entryModule = null;
this.modulePromises = {}; this.modulePromises = blank();
this.statements = []; this.statements = [];
this.externalModules = []; this.externalModules = [];
this.internalNamespaceModules = []; this.internalNamespaceModules = [];
@ -43,7 +43,7 @@ export default class Bundle {
.then( path => { .then( path => {
if ( !path ) { if ( !path ) {
// external module // external module
if ( !has( this.modulePromises, importee ) ) { if ( !this.modulePromises[ importee ] ) {
const module = new ExternalModule( importee ); const module = new ExternalModule( importee );
this.externalModules.push( module ); this.externalModules.push( module );
this.modulePromises[ importee ] = Promise.resolve( module ); this.modulePromises[ importee ] = Promise.resolve( module );
@ -52,7 +52,7 @@ export default class Bundle {
return this.modulePromises[ importee ]; return this.modulePromises[ importee ];
} }
if ( !has( this.modulePromises, path ) ) { if ( !this.modulePromises[ path ] ) {
this.modulePromises[ path ] = Promise.resolve( this.load( path, this.loadOptions ) ) this.modulePromises[ path ] = Promise.resolve( this.load( path, this.loadOptions ) )
.then( source => { .then( source => {
const module = new Module({ const module = new Module({
@ -99,8 +99,8 @@ export default class Bundle {
} }
deconflict () { deconflict () {
let definers = {}; let definers = blank();
let conflicts = {}; let conflicts = blank();
// Discover conflicts (i.e. two statements in separate modules both define `foo`) // Discover conflicts (i.e. two statements in separate modules both define `foo`)
this.statements.forEach( statement => { this.statements.forEach( statement => {
@ -122,7 +122,7 @@ export default class Bundle {
} }
names.forEach( name => { names.forEach( name => {
if ( has( definers, name ) ) { if ( definers[ name ] ) {
conflicts[ name ] = true; conflicts[ name ] = true;
} else { } else {
definers[ name ] = []; definers[ name ] = [];
@ -139,7 +139,7 @@ export default class Bundle {
// TODO is this right? // TODO is this right?
let name = makeLegalIdentifier( module.suggestedNames['*'] || module.suggestedNames.default || module.id ); let name = makeLegalIdentifier( module.suggestedNames['*'] || module.suggestedNames.default || module.id );
if ( has( definers, name ) ) { if ( definers[ name ] ) {
conflicts[ name ] = true; conflicts[ name ] = true;
} else { } else {
definers[ name ] = []; definers[ name ] = [];
@ -162,7 +162,7 @@ export default class Bundle {
}); });
function getSafeName ( name ) { function getSafeName ( name ) {
while ( has( conflicts, name ) ) { while ( conflicts[ name ] ) {
name = `_${name}`; name = `_${name}`;
} }
@ -181,7 +181,7 @@ export default class Bundle {
// Apply new names and add to the output bundle // Apply new names and add to the output bundle
this.statements.forEach( statement => { this.statements.forEach( statement => {
let replacements = {}; let replacements = blank();
keys( statement.dependsOn ) keys( statement.dependsOn )
.concat( keys( statement.defines ) ) .concat( keys( statement.defines ) )

6
src/ExternalModule.js

@ -1,3 +1,5 @@
import { blank } from './utils/object';
export default class ExternalModule { export default class ExternalModule {
constructor ( id ) { constructor ( id ) {
this.id = id; this.id = id;
@ -6,8 +8,8 @@ export default class ExternalModule {
this.isExternal = true; this.isExternal = true;
this.importedByBundle = []; this.importedByBundle = [];
this.canonicalNames = {}; this.canonicalNames = blank();
this.suggestedNames = {}; this.suggestedNames = blank();
this.needsDefault = false; this.needsDefault = false;
this.needsNamed = false; this.needsNamed = false;

36
src/Module.js

@ -4,7 +4,7 @@ import MagicString from 'magic-string';
import Statement from './Statement'; import Statement from './Statement';
import walk from './ast/walk'; import walk from './ast/walk';
import analyse from './ast/analyse'; import analyse from './ast/analyse';
import { has, keys } from './utils/object'; import { blank, keys } from './utils/object';
import { sequence } from './utils/promise'; import { sequence } from './utils/promise';
import { isImportDeclaration, isExportDeclaration } from './utils/map-helpers'; import { isImportDeclaration, isExportDeclaration } from './utils/map-helpers';
import getLocation from './utils/getLocation'; import getLocation from './utils/getLocation';
@ -23,7 +23,7 @@ export default class Module {
filename: path filename: path
}); });
this.suggestedNames = {}; this.suggestedNames = blank();
this.comments = []; this.comments = [];
// Try to extract a list of top-level statements/declarations. If // Try to extract a list of top-level statements/declarations. If
@ -60,8 +60,8 @@ export default class Module {
analyse () { analyse () {
// imports and exports, indexed by ID // imports and exports, indexed by ID
this.imports = {}; this.imports = blank();
this.exports = {}; this.exports = blank();
this.importDeclarations.forEach( statement => { this.importDeclarations.forEach( statement => {
const node = statement.node; const node = statement.node;
@ -74,7 +74,7 @@ export default class Module {
const localName = specifier.local.name; const localName = specifier.local.name;
const name = isDefault ? 'default' : isNamespace ? '*' : specifier.imported.name; const name = isDefault ? 'default' : isNamespace ? '*' : specifier.imported.name;
if ( has( this.imports, localName ) ) { if ( this.imports[ localName ] ) {
const err = new Error( `Duplicated import '${localName}'` ); const err = new Error( `Duplicated import '${localName}'` );
err.file = this.path; err.file = this.path;
err.loc = getLocation( this.source, specifier.start ); err.loc = getLocation( this.source, specifier.start );
@ -157,11 +157,11 @@ export default class Module {
analyse( this.magicString, this ); analyse( this.magicString, this );
this.canonicalNames = {}; this.canonicalNames = blank();
this.definitions = {}; this.definitions = blank();
this.definitionPromises = {}; this.definitionPromises = blank();
this.modifications = {}; this.modifications = blank();
this.statements.forEach( statement => { this.statements.forEach( statement => {
keys( statement.defines ).forEach( name => { keys( statement.defines ).forEach( name => {
@ -169,7 +169,7 @@ export default class Module {
}); });
keys( statement.modifies ).forEach( name => { keys( statement.modifies ).forEach( name => {
if ( !has( this.modifications, name ) ) { if ( !this.modifications[ name ] ) {
this.modifications[ name ] = []; this.modifications[ name ] = [];
} }
@ -179,14 +179,14 @@ export default class Module {
} }
getCanonicalName ( localName ) { getCanonicalName ( localName ) {
if ( has( this.suggestedNames, localName ) ) { if ( this.suggestedNames[ localName ] ) {
localName = this.suggestedNames[ localName ]; localName = this.suggestedNames[ localName ];
} }
if ( !has( this.canonicalNames, localName ) ) { if ( !this.canonicalNames[ localName ] ) {
let canonicalName; let canonicalName;
if ( has( this.imports, localName ) ) { if ( this.imports[ localName ] ) {
const importDeclaration = this.imports[ localName ]; const importDeclaration = this.imports[ localName ];
const module = importDeclaration.module; const module = importDeclaration.module;
@ -218,14 +218,14 @@ export default class Module {
define ( name ) { define ( name ) {
// shortcut cycles. TODO this won't work everywhere... // shortcut cycles. TODO this won't work everywhere...
if ( has( this.definitionPromises, name ) ) { if ( this.definitionPromises[ name ] ) {
return emptyArrayPromise; return emptyArrayPromise;
} }
let promise; let promise;
// The definition for this name is in a different module // The definition for this name is in a different module
if ( has( this.imports, name ) ) { if ( this.imports[ name ] ) {
const importDeclaration = this.imports[ name ]; const importDeclaration = this.imports[ name ];
promise = this.bundle.fetchModule( importDeclaration.source, this.path ) promise = this.bundle.fetchModule( importDeclaration.source, this.path )
@ -236,17 +236,17 @@ export default class Module {
if ( importDeclaration.name === 'default' ) { if ( importDeclaration.name === 'default' ) {
// TODO this seems ropey // TODO this seems ropey
const localName = importDeclaration.localName; const localName = importDeclaration.localName;
let suggestion = has( this.suggestedNames, localName ) ? this.suggestedNames[ localName ] : localName; let suggestion = this.suggestedNames[ localName ] || localName;
// special case - the module has its own import by this name // special case - the module has its own import by this name
while ( !module.isExternal && has( module.imports, suggestion ) ) { while ( !module.isExternal && module.imports[ suggestion ] ) {
suggestion = `_${suggestion}`; suggestion = `_${suggestion}`;
} }
module.suggestName( 'default', suggestion ); module.suggestName( 'default', suggestion );
} else if ( importDeclaration.name === '*' ) { } else if ( importDeclaration.name === '*' ) {
const localName = importDeclaration.localName; const localName = importDeclaration.localName;
const suggestion = has( this.suggestedNames, localName ) ? this.suggestedNames[ localName ] : localName; const suggestion = this.suggestedNames[ localName ] || localName;
module.suggestName( '*', suggestion ); module.suggestName( '*', suggestion );
module.suggestName( 'default', `${suggestion}__default` ); module.suggestName( 'default', `${suggestion}__default` );
} }

14
src/Statement.js

@ -1,4 +1,4 @@
import { has, keys } from './utils/object'; import { blank, keys } from './utils/object';
import { sequence } from './utils/promise'; import { sequence } from './utils/promise';
import { getName } from './utils/map-helpers'; import { getName } from './utils/map-helpers';
import getLocation from './utils/getLocation'; import getLocation from './utils/getLocation';
@ -14,9 +14,9 @@ export default class Statement {
this.magicString = magicString; this.magicString = magicString;
this.scope = new Scope(); this.scope = new Scope();
this.defines = {}; this.defines = blank();
this.modifies = {}; this.modifies = blank();
this.dependsOn = {}; this.dependsOn = blank();
this.isIncluded = false; this.isIncluded = false;
@ -226,7 +226,7 @@ export default class Statement {
// thing(s) this statement defines // thing(s) this statement defines
.then( () => { .then( () => {
return sequence( keys( this.defines ), name => { return sequence( keys( this.defines ), name => {
const modifications = has( this.module.modifications, name ) && this.module.modifications[ name ]; const modifications = this.module.modifications[ name ];
if ( modifications ) { if ( modifications ) {
return sequence( modifications, statement => { return sequence( modifications, statement => {
@ -264,7 +264,7 @@ export default class Statement {
const scope = node._scope; const scope = node._scope;
if ( scope ) { if ( scope ) {
let newNames = {}; let newNames = blank();
let hasReplacements; let hasReplacements;
keys( names ).forEach( key => { keys( names ).forEach( key => {
@ -295,7 +295,7 @@ export default class Statement {
if ( parent.type === 'Property' && node !== parent.value ) return; if ( parent.type === 'Property' && node !== parent.value ) return;
// TODO others...? // TODO others...?
const name = has( names, node.name ) && names[ node.name ]; const name = names[ node.name ];
if ( name && name !== node.name ) { if ( name && name !== node.name ) {
magicString.overwrite( node.start, node.end, name ); magicString.overwrite( node.start, node.end, name );

6
src/ast/walk.js

@ -1,3 +1,5 @@
import { blank } from '../utils/object';
let shouldSkip; let shouldSkip;
let shouldAbort; let shouldAbort;
@ -11,7 +13,7 @@ let context = {
abort: () => shouldAbort = true abort: () => shouldAbort = true
}; };
let childKeys = {}; let childKeys = blank();
let toString = Object.prototype.toString; let toString = Object.prototype.toString;
@ -54,4 +56,4 @@ function visit ( node, parent, enter, leave ) {
if ( leave && !shouldAbort ) { if ( leave && !shouldAbort ) {
leave( node, parent ); leave( node, parent );
} }
} }

3
src/finalisers/amd.js

@ -1,4 +1,3 @@
import { has } from '../utils/object';
import { getName, quoteId } from '../utils/map-helpers'; import { getName, quoteId } from '../utils/map-helpers';
export default function amd ( bundle, magicString, exportMode, options ) { export default function amd ( bundle, magicString, exportMode, options ) {
@ -11,7 +10,7 @@ export default function amd ( bundle, magicString, exportMode, options ) {
} }
const params = const params =
( has( options, 'moduleId' ) ? `['${options.moduleId}'], ` : `` ) + ( options.moduleId ? `['${options.moduleId}'], ` : `` ) +
( deps.length ? `[${deps.join( ', ' )}], ` : `` ); ( deps.length ? `[${deps.join( ', ' )}], ` : `` );
const intro = `define(${params}function (${args.join( ', ' )}) { 'use strict';\n\n`; const intro = `define(${params}function (${args.join( ', ' )}) { 'use strict';\n\n`;

6
src/finalisers/iife.js

@ -1,11 +1,11 @@
import { has } from '../utils/object'; import { blank } from '../utils/object';
import { getName } from '../utils/map-helpers'; import { getName } from '../utils/map-helpers';
export default function iife ( bundle, magicString, exportMode, options ) { export default function iife ( bundle, magicString, exportMode, options ) {
const globalNames = options.globals || {}; const globalNames = options.globals || blank();
let dependencies = bundle.externalModules.map( module => { let dependencies = bundle.externalModules.map( module => {
return has( globalNames, module.id ) ? globalNames[ module.id ] : module.name; return globalNames[ module.id ] || module.name;
}); });
let args = bundle.externalModules.map( getName ); let args = bundle.externalModules.map( getName );

8
src/finalisers/umd.js

@ -1,4 +1,4 @@
import { has } from '../utils/object'; import { blank } from '../utils/object';
import { getName, quoteId, req } from '../utils/map-helpers'; import { getName, quoteId, req } from '../utils/map-helpers';
export default function umd ( bundle, magicString, exportMode, options ) { export default function umd ( bundle, magicString, exportMode, options ) {
@ -8,12 +8,12 @@ export default function umd ( bundle, magicString, exportMode, options ) {
const indentStr = magicString.getIndentString(); const indentStr = magicString.getIndentString();
const globalNames = options.globals || {}; const globalNames = options.globals || blank();
let amdDeps = bundle.externalModules.map( quoteId ); let amdDeps = bundle.externalModules.map( quoteId );
let cjsDeps = bundle.externalModules.map( req ); let cjsDeps = bundle.externalModules.map( req );
let globalDeps = bundle.externalModules.map( module => { let globalDeps = bundle.externalModules.map( module => {
return has( globalNames, module.id ) ? globalNames[ module.id ] : module.name; return globalNames[ module.id ] || module.name;
}); });
let args = bundle.externalModules.map( getName ); let args = bundle.externalModules.map( getName );
@ -27,7 +27,7 @@ export default function umd ( bundle, magicString, exportMode, options ) {
} }
const amdParams = const amdParams =
( has( options, 'moduleId' ) ? `['${options.moduleId}'], ` : `` ) + ( options.moduleId ? `['${options.moduleId}'], ` : `` ) +
( amdDeps.length ? `[${amdDeps.join( ', ' )}], ` : `` ); ( amdDeps.length ? `[${amdDeps.join( ', ' )}], ` : `` );
const cjsExport = exportMode === 'default' ? `module.exports = ` : ``; const cjsExport = exportMode === 'default' ? `module.exports = ` : ``;

4
src/utils/makeLegalIdentifier.js

@ -1,7 +1,9 @@
import { blank } from './object';
const reservedWords = 'break case class catch const continue debugger default delete do else export extends finally for function if import in instanceof let new return super switch this throw try typeof var void while with yield enum await implements package protected static interface private public'.split( ' ' ); const reservedWords = 'break case class catch const continue debugger default delete do else export extends finally for function if import in instanceof let new return super switch this throw try typeof var void while with yield enum await implements package protected static interface private public'.split( ' ' );
const builtins = 'Infinity NaN undefined null true false eval uneval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Symbol Error EvalError InternalError RangeError ReferenceError SyntaxError TypeError URIError Number Math Date String RegExp Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array Float32Array Float64Array Map Set WeakMap WeakSet SIMD ArrayBuffer DataView JSON Promise Generator GeneratorFunction Reflect Proxy Intl'.split( ' ' ); const builtins = 'Infinity NaN undefined null true false eval uneval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Symbol Error EvalError InternalError RangeError ReferenceError SyntaxError TypeError URIError Number Math Date String RegExp Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array Float32Array Float64Array Map Set WeakMap WeakSet SIMD ArrayBuffer DataView JSON Promise Generator GeneratorFunction Reflect Proxy Intl'.split( ' ' );
let blacklisted = {}; let blacklisted = blank();
reservedWords.concat( builtins ).forEach( word => blacklisted[ word ] = true ); reservedWords.concat( builtins ).forEach( word => blacklisted[ word ] = true );

6
src/utils/object.js

@ -1,11 +1,5 @@
export const keys = Object.keys; export const keys = Object.keys;
export const hasOwnProp = Object.prototype.hasOwnProperty;
export function has ( obj, prop ) {
return hasOwnProp.call( obj, prop );
}
export function blank () { export function blank () {
return Object.create( null ); return Object.create( null );
} }

3
test/function/object-prototype-properties/_config.js

@ -0,0 +1,3 @@
module.exports = {
description: 'handles names conflicting with Object.prototype properties'
};

7
test/function/object-prototype-properties/foo.js

@ -0,0 +1,7 @@
function valueOf() {
return 42;
}
export default function() {
return valueOf();
};

3
test/function/object-prototype-properties/main.js

@ -0,0 +1,3 @@
import foo from './foo';
assert.equal( foo(), 42 );
Loading…
Cancel
Save