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

6
src/ExternalModule.js

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

36
src/Module.js

@ -4,7 +4,7 @@ import MagicString from 'magic-string';
import Statement from './Statement';
import walk from './ast/walk';
import analyse from './ast/analyse';
import { has, keys } from './utils/object';
import { blank, keys } from './utils/object';
import { sequence } from './utils/promise';
import { isImportDeclaration, isExportDeclaration } from './utils/map-helpers';
import getLocation from './utils/getLocation';
@ -23,7 +23,7 @@ export default class Module {
filename: path
});
this.suggestedNames = {};
this.suggestedNames = blank();
this.comments = [];
// Try to extract a list of top-level statements/declarations. If
@ -60,8 +60,8 @@ export default class Module {
analyse () {
// imports and exports, indexed by ID
this.imports = {};
this.exports = {};
this.imports = blank();
this.exports = blank();
this.importDeclarations.forEach( statement => {
const node = statement.node;
@ -74,7 +74,7 @@ export default class Module {
const localName = specifier.local.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}'` );
err.file = this.path;
err.loc = getLocation( this.source, specifier.start );
@ -157,11 +157,11 @@ export default class Module {
analyse( this.magicString, this );
this.canonicalNames = {};
this.canonicalNames = blank();
this.definitions = {};
this.definitionPromises = {};
this.modifications = {};
this.definitions = blank();
this.definitionPromises = blank();
this.modifications = blank();
this.statements.forEach( statement => {
keys( statement.defines ).forEach( name => {
@ -169,7 +169,7 @@ export default class Module {
});
keys( statement.modifies ).forEach( name => {
if ( !has( this.modifications, name ) ) {
if ( !this.modifications[ name ] ) {
this.modifications[ name ] = [];
}
@ -179,14 +179,14 @@ export default class Module {
}
getCanonicalName ( localName ) {
if ( has( this.suggestedNames, localName ) ) {
if ( this.suggestedNames[ localName ] ) {
localName = this.suggestedNames[ localName ];
}
if ( !has( this.canonicalNames, localName ) ) {
if ( !this.canonicalNames[ localName ] ) {
let canonicalName;
if ( has( this.imports, localName ) ) {
if ( this.imports[ localName ] ) {
const importDeclaration = this.imports[ localName ];
const module = importDeclaration.module;
@ -218,14 +218,14 @@ export default class Module {
define ( name ) {
// shortcut cycles. TODO this won't work everywhere...
if ( has( this.definitionPromises, name ) ) {
if ( this.definitionPromises[ name ] ) {
return emptyArrayPromise;
}
let promise;
// The definition for this name is in a different module
if ( has( this.imports, name ) ) {
if ( this.imports[ name ] ) {
const importDeclaration = this.imports[ name ];
promise = this.bundle.fetchModule( importDeclaration.source, this.path )
@ -236,17 +236,17 @@ export default class Module {
if ( importDeclaration.name === 'default' ) {
// TODO this seems ropey
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
while ( !module.isExternal && has( module.imports, suggestion ) ) {
while ( !module.isExternal && module.imports[ suggestion ] ) {
suggestion = `_${suggestion}`;
}
module.suggestName( 'default', suggestion );
} else if ( importDeclaration.name === '*' ) {
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( '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 { getName } from './utils/map-helpers';
import getLocation from './utils/getLocation';
@ -14,9 +14,9 @@ export default class Statement {
this.magicString = magicString;
this.scope = new Scope();
this.defines = {};
this.modifies = {};
this.dependsOn = {};
this.defines = blank();
this.modifies = blank();
this.dependsOn = blank();
this.isIncluded = false;
@ -226,7 +226,7 @@ export default class Statement {
// thing(s) this statement defines
.then( () => {
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 ) {
return sequence( modifications, statement => {
@ -264,7 +264,7 @@ export default class Statement {
const scope = node._scope;
if ( scope ) {
let newNames = {};
let newNames = blank();
let hasReplacements;
keys( names ).forEach( key => {
@ -295,7 +295,7 @@ export default class Statement {
if ( parent.type === 'Property' && node !== parent.value ) return;
// TODO others...?
const name = has( names, node.name ) && names[ node.name ];
const name = names[ node.name ];
if ( name && name !== node.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 shouldAbort;
@ -11,7 +13,7 @@ let context = {
abort: () => shouldAbort = true
};
let childKeys = {};
let childKeys = blank();
let toString = Object.prototype.toString;
@ -54,4 +56,4 @@ function visit ( node, parent, enter, leave ) {
if ( leave && !shouldAbort ) {
leave( node, parent );
}
}
}

3
src/finalisers/amd.js

@ -1,4 +1,3 @@
import { has } from '../utils/object';
import { getName, quoteId } from '../utils/map-helpers';
export default function amd ( bundle, magicString, exportMode, options ) {
@ -11,7 +10,7 @@ export default function amd ( bundle, magicString, exportMode, options ) {
}
const params =
( has( options, 'moduleId' ) ? `['${options.moduleId}'], ` : `` ) +
( options.moduleId ? `['${options.moduleId}'], ` : `` ) +
( deps.length ? `[${deps.join( ', ' )}], ` : `` );
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';
export default function iife ( bundle, magicString, exportMode, options ) {
const globalNames = options.globals || {};
const globalNames = options.globals || blank();
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 );

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';
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 globalNames = options.globals || {};
const globalNames = options.globals || blank();
let amdDeps = bundle.externalModules.map( quoteId );
let cjsDeps = bundle.externalModules.map( req );
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 );
@ -27,7 +27,7 @@ export default function umd ( bundle, magicString, exportMode, options ) {
}
const amdParams =
( has( options, 'moduleId' ) ? `['${options.moduleId}'], ` : `` ) +
( options.moduleId ? `['${options.moduleId}'], ` : `` ) +
( amdDeps.length ? `[${amdDeps.join( ', ' )}], ` : `` );
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 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 );

6
src/utils/object.js

@ -1,11 +1,5 @@
export const keys = Object.keys;
export const hasOwnProp = Object.prototype.hasOwnProperty;
export function has ( obj, prop ) {
return hasOwnProp.call( obj, prop );
}
export function blank () {
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