Browse Source

push analyse logic into Statement class

contingency-plan
Rich Harris 10 years ago
parent
commit
708272b935
  1. 14
      src/Bundle.js
  2. 181
      src/Module.js
  3. 174
      src/Statement.js
  4. 174
      src/ast/analyse.js
  5. 8
      src/utils/map-helpers.js

14
src/Bundle.js

@ -44,10 +44,10 @@ export default class Bundle {
if ( !has( this.modulePromises, path ) ) { if ( !has( this.modulePromises, path ) ) {
this.modulePromises[ path ] = readFile( path, { encoding: 'utf-8' }) this.modulePromises[ path ] = readFile( path, { encoding: 'utf-8' })
.then( code => { .then( source => {
const module = new Module({ const module = new Module({
path, path,
code, source,
bundle: this bundle: this
}); });
@ -67,7 +67,13 @@ export default class Bundle {
if ( entryModule.exports.default ) { if ( entryModule.exports.default ) {
let defaultExportName = makeLegalIdentifier( basename( this.entryPath ).slice( 0, -extname( this.entryPath ).length ) ); let defaultExportName = makeLegalIdentifier( basename( this.entryPath ).slice( 0, -extname( this.entryPath ).length ) );
while ( entryModule.scope.contains( defaultExportName ) ) {
let topLevelNames = [];
entryModule.statements.forEach( statement => {
keys( statement.defines ).forEach( name => topLevelNames.push( name ) );
});
while ( ~topLevelNames.indexOf( defaultExportName ) ) {
defaultExportName = `_${defaultExportName}`; defaultExportName = `_${defaultExportName}`;
} }
@ -161,7 +167,7 @@ export default class Bundle {
} }
}); });
const source = statement.source.clone().trim(); const source = statement.magicString.clone().trim();
// modify exports as necessary // modify exports as necessary
if ( /^Export/.test( statement.node.type ) ) { if ( /^Export/.test( statement.node.type ) ) {

181
src/Module.js

@ -6,39 +6,47 @@ import Statement from './Statement';
import analyse from './ast/analyse'; import analyse from './ast/analyse';
import { has, keys } from './utils/object'; import { has, keys } from './utils/object';
import { sequence } from './utils/promise'; import { sequence } from './utils/promise';
import { isImportDeclaration, isExportDeclaration } from './utils/map-helpers';
import getLocation from './utils/getLocation'; import getLocation from './utils/getLocation';
import makeLegalIdentifier from './utils/makeLegalIdentifier'; import makeLegalIdentifier from './utils/makeLegalIdentifier';
const emptyArrayPromise = Promise.resolve([]); const emptyArrayPromise = Promise.resolve([]);
export default class Module { export default class Module {
constructor ({ path, code, bundle }) { constructor ({ path, source, bundle }) {
this.source = source;
this.bundle = bundle; this.bundle = bundle;
this.path = path; this.path = path;
this.relativePath = relative( bundle.base, path ).slice( 0, -3 ); // remove .js this.relativePath = relative( bundle.base, path ).slice( 0, -3 ); // remove .js
this.code = new MagicString( code, { this.magicString = new MagicString( source, {
filename: path filename: path
}); });
this.suggestedNames = {}; this.suggestedNames = {};
this.comments = []; this.comments = [];
// Try to extract a list of top-level statements/declarations. If
// the parse fails, attach file info and abort
try { try {
this.ast = parse( code, { const ast = parse( source, {
ecmaVersion: 6, ecmaVersion: 6,
sourceType: 'module', sourceType: 'module',
onComment: ( block, text, start, end ) => this.comments.push({ block, text, start, end }) onComment: ( block, text, start, end ) => this.comments.push({ block, text, start, end })
}); });
this.statements = ast.body.map( node => {
const magicString = this.magicString.snip( node.start, node.end );
return new Statement( node, magicString, this );
});
} catch ( err ) { } catch ( err ) {
err.file = path; err.file = path;
throw err; throw err;
} }
this.statements = this.ast.body.map( node => { this.importDeclarations = this.statements.filter( isImportDeclaration );
const source = this.code.snip( node.start, node.end ); this.exportDeclarations = this.statements.filter( isExportDeclaration );
return new Statement( node, source, this );
});
this.analyse(); this.analyse();
} }
@ -48,110 +56,99 @@ export default class Module {
this.imports = {}; this.imports = {};
this.exports = {}; this.exports = {};
this.statements.forEach( statement => { this.importDeclarations.forEach( statement => {
const node = statement.node; const node = statement.node;
let source; const source = node.source.value;
// import foo from './foo'; node.specifiers.forEach( specifier => {
// import { bar } from './bar'; const isDefault = specifier.type === 'ImportDefaultSpecifier';
if ( node.type === 'ImportDeclaration' ) { const isNamespace = specifier.type === 'ImportNamespaceSpecifier';
source = node.source.value;
node.specifiers.forEach( specifier => { const localName = specifier.local.name;
const isDefault = specifier.type === 'ImportDefaultSpecifier'; const name = isDefault ? 'default' : isNamespace ? '*' : specifier.imported.name;
const isNamespace = specifier.type === 'ImportNamespaceSpecifier';
const localName = specifier.local.name; if ( has( this.imports, localName ) ) {
const name = isDefault ? 'default' : isNamespace ? '*' : specifier.imported.name; const err = new Error( `Duplicated import '${localName}'` );
err.file = this.path;
err.loc = getLocation( this.source, specifier.start );
throw err;
}
if ( has( this.imports, localName ) ) { this.imports[ localName ] = {
const err = new Error( `Duplicated import '${localName}'` ); source,
err.file = this.path; name,
err.loc = getLocation( this.code.original, specifier.start ); localName
throw err; };
} });
});
this.imports[ localName ] = { this.exportDeclarations.forEach( statement => {
source, const node = statement.node;
name, const source = node.source && node.source.value;
localName
}; // export default function foo () {}
}); // export default foo;
// export default 42;
if ( node.type === 'ExportDefaultDeclaration' ) {
const isDeclaration = /Declaration$/.test( node.declaration.type );
this.exports.default = {
statement,
name: 'default',
localName: isDeclaration ? node.declaration.id.name : 'default',
isDeclaration
};
} }
else if ( /^Export/.test( node.type ) ) { // export { foo, bar, baz }
// export default function foo () {} // export var foo = 42;
// export default foo; // export function foo () {}
// export default 42; else if ( node.type === 'ExportNamedDeclaration' ) {
if ( node.type === 'ExportDefaultDeclaration' ) { if ( node.specifiers.length ) {
const isDeclaration = /Declaration$/.test( node.declaration.type ); // export { foo, bar, baz }
node.specifiers.forEach( specifier => {
this.exports.default = { const localName = specifier.local.name;
node, // TODO remove this const exportedName = specifier.exported.name;
statement,
name: 'default', this.exports[ exportedName ] = {
localName: isDeclaration ? node.declaration.id.name : 'default', localName,
isDeclaration exportedName
}; };
}
// export { foo, bar, baz }
// export var foo = 42;
// export function foo () {}
else if ( node.type === 'ExportNamedDeclaration' ) {
// export { foo } from './foo';
source = node.source && node.source.value;
if ( node.specifiers.length ) {
// export { foo, bar, baz }
node.specifiers.forEach( specifier => {
const localName = specifier.local.name;
const exportedName = specifier.exported.name;
this.exports[ exportedName ] = { // export { foo } from './foo';
if ( source ) {
this.imports[ localName ] = {
source,
localName, localName,
exportedName name: exportedName
}; };
}
});
}
if ( source ) { else {
this.imports[ localName ] = { let declaration = node.declaration;
source,
localName,
name: exportedName
};
}
});
}
else {
let declaration = node.declaration;
let name;
if ( declaration.type === 'VariableDeclaration' ) { let name;
// export var foo = 42
name = declaration.declarations[0].id.name;
} else {
// export function foo () {}
name = declaration.id.name;
}
this.exports[ name ] = { if ( declaration.type === 'VariableDeclaration' ) {
node, // TODO remove // export var foo = 42
statement, name = declaration.declarations[0].id.name;
localName: name, } else {
expression: declaration // export function foo () {}
}; name = declaration.id.name;
} }
this.exports[ name ] = {
statement,
localName: name,
expression: declaration
};
} }
} }
}); });
analyse( this.magicString, this );
analyse( this.ast, this.code, this );
this.definedNames = this.scope.names.slice(); // TODO is this used?
this.canonicalNames = {}; this.canonicalNames = {};

174
src/Statement.js

@ -1,11 +1,16 @@
import { keys } from './utils/object'; import { has, keys } from './utils/object';
import { getName } from './utils/map-helpers';
import getLocation from './utils/getLocation';
import walk from './ast/walk';
import Scope from './ast/Scope';
export default class Statement { export default class Statement {
constructor ( node, source, module ) { constructor ( node, magicString, module ) {
this.node = node; this.node = node;
this.module = module; this.module = module;
this.source = source; this.magicString = magicString;
this.scope = new Scope();
this.defines = {}; this.defines = {};
this.modifies = {}; this.modifies = {};
this.dependsOn = {}; this.dependsOn = {};
@ -15,5 +20,168 @@ export default class Statement {
this.leadingComments = [] this.leadingComments = []
this.trailingComment = null; this.trailingComment = null;
this.margin = [ 0, 0 ]; this.margin = [ 0, 0 ];
// some facts about this statement...
this.isImportDeclaration = node.type === 'ImportDeclaration';
this.isExportDeclaration = /^Export/.test( node.type );
}
analyse () {
if ( this.isImportDeclaration ) return; // nothing to analyse
const statement = this; // TODO use arrow functions instead
const magicString = this.magicString;
let scope = this.scope;
function addToScope ( declarator ) {
var name = declarator.id.name;
scope.add( name, false );
if ( !scope.parent ) {
statement.defines[ name ] = true;
}
}
function addToBlockScope ( declarator ) {
var name = declarator.id.name;
scope.add( name, true );
if ( !scope.parent ) {
statement.defines[ name ] = true;
}
}
walk( this.node, {
enter ( node ) {
let newScope;
magicString.addSourcemapLocation( node.start );
switch ( node.type ) {
case 'FunctionExpression':
case 'FunctionDeclaration':
case 'ArrowFunctionExpression':
let names = node.params.map( getName );
if ( node.type === 'FunctionDeclaration' ) {
addToScope( node );
} else if ( node.type === 'FunctionExpression' && node.id ) {
names.push( node.id.name );
}
newScope = new Scope({
parent: scope,
params: names, // TODO rest params?
block: false
});
break;
case 'BlockStatement':
newScope = new Scope({
parent: scope,
block: true
});
break;
case 'CatchClause':
newScope = new Scope({
parent: scope,
params: [ node.param.name ],
block: true
});
break;
case 'VariableDeclaration':
node.declarations.forEach( node.kind === 'let' ? addToBlockScope : addToScope ); // TODO const?
break;
case 'ClassDeclaration':
addToScope( node );
break;
}
if ( newScope ) {
Object.defineProperty( node, '_scope', { value: newScope });
scope = newScope;
}
},
leave ( node ) {
if ( node._scope ) {
scope = scope.parent;
}
}
});
if ( !this.isImportDeclaration ) {
walk( this.node, {
enter: ( node, parent ) => {
if ( node._scope ) scope = node._scope;
this.checkForReads( scope, node, parent );
this.checkForWrites( scope, node );
},
leave: ( node ) => {
if ( node._scope ) scope = scope.parent;
}
});
}
}
checkForReads ( scope, node, parent ) {
if ( node.type === 'Identifier' ) {
// disregard the `bar` in `foo.bar` - these appear as Identifier nodes
if ( parent.type === 'MemberExpression' && node !== parent.object ) {
return;
}
// disregard the `bar` in { bar: foo }
if ( parent.type === 'Property' && node !== parent.value ) {
return;
}
const definingScope = scope.findDefiningScope( node.name );
if ( ( !definingScope || definingScope.depth === 0 ) && !this.defines[ node.name ] ) {
this.dependsOn[ node.name ] = true;
}
}
}
checkForWrites ( scope, node ) {
const addNode = ( node, disallowImportReassignments ) => {
while ( node.type === 'MemberExpression' ) {
node = node.object;
}
// disallow assignments/updates to imported bindings and namespaces
if ( disallowImportReassignments && has( this.module.imports, node.name ) && !scope.contains( node.name ) ) {
const err = new Error( `Illegal reassignment to import '${node.name}'` );
err.file = this.module.path;
err.loc = getLocation( this.module.magicString.toString(), node.start );
throw err;
}
if ( node.type !== 'Identifier' ) {
return;
}
this.modifies[ node.name ] = true;
};
if ( node.type === 'AssignmentExpression' ) {
addNode( node.left, true );
}
else if ( node.type === 'UpdateExpression' ) {
addNode( node.argument, true );
}
else if ( node.type === 'CallExpression' ) {
node.arguments.forEach( arg => addNode( arg, false ) );
}
} }
} }

174
src/ast/analyse.js

@ -4,35 +4,14 @@ import { getName } from '../utils/map-helpers';
import { has } from '../utils/object'; import { has } from '../utils/object';
import getLocation from '../utils/getLocation'; import getLocation from '../utils/getLocation';
export default function analyse ( ast, magicString, module ) { export default function analyse ( magicString, module ) {
let scope = new Scope(); let scope = new Scope();
let currentTopLevelStatement;
function addToScope ( declarator ) {
var name = declarator.id.name;
scope.add( name, false );
if ( !scope.parent ) {
currentTopLevelStatement.defines[ name ] = true;
}
}
function addToBlockScope ( declarator ) {
var name = declarator.id.name;
scope.add( name, true );
if ( !scope.parent ) {
currentTopLevelStatement.defines[ name ] = true;
}
}
// first we need to generate comprehensive scope info // first we need to generate comprehensive scope info
let previousStatement = null; let previousStatement = null;
let commentIndex = 0; let commentIndex = 0;
module.statements.forEach( statement => { module.statements.forEach( statement => {
currentTopLevelStatement = statement; // so we can attach scoping info
const node = statement.node; const node = statement.node;
let trailing = !!previousStatement; let trailing = !!previousStatement;
@ -54,7 +33,7 @@ export default function analyse ( ast, magicString, module ) {
if ( !comment || ( comment.end > node.start ) ) break; if ( !comment || ( comment.end > node.start ) ) break;
// attach any trailing comment to the previous statement // attach any trailing comment to the previous statement
if ( trailing && !/\n/.test( magicString.slice( previousStatement.node.end, comment.start ) ) ) { if ( trailing && !/\n/.test( module.source.slice( previousStatement.node.end, comment.start ) ) ) {
previousStatement.trailingComment = comment; previousStatement.trailingComment = comment;
} }
@ -77,155 +56,8 @@ export default function analyse ( ast, magicString, module ) {
if ( previousStatement ) previousStatement.margin[1] = margin; if ( previousStatement ) previousStatement.margin[1] = margin;
statement.margin[0] = margin; statement.margin[0] = margin;
walk( statement.node, { statement.analyse();
enter ( node ) {
let newScope;
magicString.addSourcemapLocation( node.start );
switch ( node.type ) {
case 'FunctionExpression':
case 'FunctionDeclaration':
case 'ArrowFunctionExpression':
let names = node.params.map( getName );
if ( node.type === 'FunctionDeclaration' ) {
addToScope( node );
} else if ( node.type === 'FunctionExpression' && node.id ) {
names.push( node.id.name );
}
newScope = new Scope({
parent: scope,
params: names, // TODO rest params?
block: false
});
break;
case 'BlockStatement':
newScope = new Scope({
parent: scope,
block: true
});
break;
case 'CatchClause':
newScope = new Scope({
parent: scope,
params: [ node.param.name ],
block: true
});
break;
case 'VariableDeclaration':
node.declarations.forEach( node.kind === 'let' ? addToBlockScope : addToScope ); // TODO const?
break;
case 'ClassDeclaration':
addToScope( node );
break;
}
if ( newScope ) {
Object.defineProperty( node, '_scope', { value: newScope });
scope = newScope;
}
},
leave ( node ) {
if ( node === currentTopLevelStatement ) {
currentTopLevelStatement = null;
}
if ( node._scope ) {
scope = scope.parent;
}
}
});
previousStatement = statement; previousStatement = statement;
}); });
// then, we need to find which top-level dependencies this statement has,
// and which it potentially modifies
module.statements.forEach( statement => {
function checkForReads ( node, parent ) {
if ( node.type === 'Identifier' ) {
// disregard the `bar` in `foo.bar` - these appear as Identifier nodes
if ( parent.type === 'MemberExpression' && node !== parent.object ) {
return;
}
// disregard the `bar` in { bar: foo }
if ( parent.type === 'Property' && node !== parent.value ) {
return;
}
const definingScope = scope.findDefiningScope( node.name );
if ( ( !definingScope || definingScope.depth === 0 ) && !statement.defines[ node.name ] ) {
statement.dependsOn[ node.name ] = true;
}
}
}
function checkForWrites ( node ) {
function addNode ( node, disallowImportReassignments ) {
while ( node.type === 'MemberExpression' ) {
node = node.object;
}
// disallow assignments/updates to imported bindings and namespaces
if ( disallowImportReassignments && has( module.imports, node.name ) && !scope.contains( node.name ) ) {
const err = new Error( `Illegal reassignment to import '${node.name}'` );
err.file = module.path;
err.loc = getLocation( module.code.toString(), node.start );
throw err;
}
if ( node.type !== 'Identifier' ) {
return;
}
statement.modifies[ node.name ] = true;
}
if ( node.type === 'AssignmentExpression' ) {
addNode( node.left, true );
}
else if ( node.type === 'UpdateExpression' ) {
addNode( node.argument, true );
}
else if ( node.type === 'CallExpression' ) {
node.arguments.forEach( arg => addNode( arg, false ) );
}
// TODO UpdateExpressions, method calls?
}
walk( statement.node, {
enter ( node, parent ) {
// skip imports
if ( /^Import/.test( node.type ) ) return this.skip();
if ( node._scope ) scope = node._scope;
checkForReads( node, parent );
checkForWrites( node, parent );
//if ( node.type === 'ReturnStatement')
},
leave ( node ) {
if ( node._scope ) scope = scope.parent;
}
});
});
module.scope = scope;
} }

8
src/utils/map-helpers.js

@ -9,3 +9,11 @@ export function quoteId ( x ) {
export function req ( x ) { export function req ( x ) {
return `require('${x.id}')`; return `require('${x.id}')`;
} }
export function isImportDeclaration ( statement ) {
return statement.isImportDeclaration;
}
export function isExportDeclaration ( statement ) {
return statement.isExportDeclaration;
}

Loading…
Cancel
Save