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 ) ) {
this.modulePromises[ path ] = readFile( path, { encoding: 'utf-8' })
.then( code => {
.then( source => {
const module = new Module({
path,
code,
source,
bundle: this
});
@ -67,7 +67,13 @@ export default class Bundle {
if ( entryModule.exports.default ) {
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}`;
}
@ -161,7 +167,7 @@ export default class Bundle {
}
});
const source = statement.source.clone().trim();
const source = statement.magicString.clone().trim();
// modify exports as necessary
if ( /^Export/.test( statement.node.type ) ) {

181
src/Module.js

@ -6,39 +6,47 @@ import Statement from './Statement';
import analyse from './ast/analyse';
import { has, keys } from './utils/object';
import { sequence } from './utils/promise';
import { isImportDeclaration, isExportDeclaration } from './utils/map-helpers';
import getLocation from './utils/getLocation';
import makeLegalIdentifier from './utils/makeLegalIdentifier';
const emptyArrayPromise = Promise.resolve([]);
export default class Module {
constructor ({ path, code, bundle }) {
constructor ({ path, source, bundle }) {
this.source = source;
this.bundle = bundle;
this.path = path;
this.relativePath = relative( bundle.base, path ).slice( 0, -3 ); // remove .js
this.code = new MagicString( code, {
this.magicString = new MagicString( source, {
filename: path
});
this.suggestedNames = {};
this.comments = [];
// Try to extract a list of top-level statements/declarations. If
// the parse fails, attach file info and abort
try {
this.ast = parse( code, {
const ast = parse( source, {
ecmaVersion: 6,
sourceType: 'module',
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 ) {
err.file = path;
throw err;
}
this.statements = this.ast.body.map( node => {
const source = this.code.snip( node.start, node.end );
return new Statement( node, source, this );
});
this.importDeclarations = this.statements.filter( isImportDeclaration );
this.exportDeclarations = this.statements.filter( isExportDeclaration );
this.analyse();
}
@ -48,110 +56,99 @@ export default class Module {
this.imports = {};
this.exports = {};
this.statements.forEach( statement => {
this.importDeclarations.forEach( statement => {
const node = statement.node;
let source;
const source = node.source.value;
// import foo from './foo';
// import { bar } from './bar';
if ( node.type === 'ImportDeclaration' ) {
source = node.source.value;
node.specifiers.forEach( specifier => {
const isDefault = specifier.type === 'ImportDefaultSpecifier';
const isNamespace = specifier.type === 'ImportNamespaceSpecifier';
node.specifiers.forEach( specifier => {
const isDefault = specifier.type === 'ImportDefaultSpecifier';
const isNamespace = specifier.type === 'ImportNamespaceSpecifier';
const localName = specifier.local.name;
const name = isDefault ? 'default' : isNamespace ? '*' : specifier.imported.name;
const localName = specifier.local.name;
const name = isDefault ? 'default' : isNamespace ? '*' : specifier.imported.name;
if ( has( this.imports, localName ) ) {
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 ) ) {
const err = new Error( `Duplicated import '${localName}'` );
err.file = this.path;
err.loc = getLocation( this.code.original, specifier.start );
throw err;
}
this.imports[ localName ] = {
source,
name,
localName
};
});
});
this.imports[ localName ] = {
source,
name,
localName
};
});
this.exportDeclarations.forEach( statement => {
const node = statement.node;
const source = node.source && node.source.value;
// 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 default function foo () {}
// export default foo;
// export default 42;
if ( node.type === 'ExportDefaultDeclaration' ) {
const isDeclaration = /Declaration$/.test( node.declaration.type );
this.exports.default = {
node, // TODO remove this
statement,
name: 'default',
localName: isDeclaration ? node.declaration.id.name : 'default',
isDeclaration
};
}
// 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;
// export { foo, bar, baz }
// export var foo = 42;
// export function foo () {}
else if ( node.type === 'ExportNamedDeclaration' ) {
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 ] = {
localName,
exportedName
};
this.exports[ exportedName ] = {
// export { foo } from './foo';
if ( source ) {
this.imports[ localName ] = {
source,
localName,
exportedName
name: exportedName
};
}
});
}
if ( source ) {
this.imports[ localName ] = {
source,
localName,
name: exportedName
};
}
});
}
else {
let declaration = node.declaration;
let name;
else {
let declaration = node.declaration;
if ( declaration.type === 'VariableDeclaration' ) {
// export var foo = 42
name = declaration.declarations[0].id.name;
} else {
// export function foo () {}
name = declaration.id.name;
}
let name;
this.exports[ name ] = {
node, // TODO remove
statement,
localName: name,
expression: declaration
};
if ( declaration.type === 'VariableDeclaration' ) {
// export var foo = 42
name = declaration.declarations[0].id.name;
} else {
// export function foo () {}
name = declaration.id.name;
}
this.exports[ name ] = {
statement,
localName: name,
expression: declaration
};
}
}
});
analyse( this.ast, this.code, this );
this.definedNames = this.scope.names.slice(); // TODO is this used?
analyse( this.magicString, this );
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 {
constructor ( node, source, module ) {
constructor ( node, magicString, module ) {
this.node = node;
this.module = module;
this.source = source;
this.magicString = magicString;
this.scope = new Scope();
this.defines = {};
this.modifies = {};
this.dependsOn = {};
@ -15,5 +20,168 @@ export default class Statement {
this.leadingComments = []
this.trailingComment = null;
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 getLocation from '../utils/getLocation';
export default function analyse ( ast, magicString, module ) {
export default function analyse ( magicString, module ) {
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
let previousStatement = null;
let commentIndex = 0;
module.statements.forEach( statement => {
currentTopLevelStatement = statement; // so we can attach scoping info
const node = statement.node;
let trailing = !!previousStatement;
@ -54,7 +33,7 @@ export default function analyse ( ast, magicString, module ) {
if ( !comment || ( comment.end > node.start ) ) break;
// 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;
}
@ -77,155 +56,8 @@ export default function analyse ( ast, magicString, module ) {
if ( previousStatement ) previousStatement.margin[1] = margin;
statement.margin[0] = margin;
walk( statement.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 === currentTopLevelStatement ) {
currentTopLevelStatement = null;
}
if ( node._scope ) {
scope = scope.parent;
}
}
});
statement.analyse();
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 ) {
return `require('${x.id}')`;
}
export function isImportDeclaration ( statement ) {
return statement.isImportDeclaration;
}
export function isExportDeclaration ( statement ) {
return statement.isExportDeclaration;
}

Loading…
Cancel
Save