From 708272b93580690d2d5d2664eb68c3aa91e3aef3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 21 May 2015 16:58:55 -0400 Subject: [PATCH] push analyse logic into Statement class --- src/Bundle.js | 14 ++- src/Module.js | 181 +++++++++++++++++++-------------------- src/Statement.js | 174 ++++++++++++++++++++++++++++++++++++- src/ast/analyse.js | 174 +------------------------------------ src/utils/map-helpers.js | 8 ++ 5 files changed, 281 insertions(+), 270 deletions(-) diff --git a/src/Bundle.js b/src/Bundle.js index fd8b003..edf60f2 100644 --- a/src/Bundle.js +++ b/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 ) ) { diff --git a/src/Module.js b/src/Module.js index 689e5ad..804d286 100644 --- a/src/Module.js +++ b/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 = {}; diff --git a/src/Statement.js b/src/Statement.js index 1932166..90bde0d 100644 --- a/src/Statement.js +++ b/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 ) ); + } } } diff --git a/src/ast/analyse.js b/src/ast/analyse.js index adae223..fe0c6dd 100644 --- a/src/ast/analyse.js +++ b/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; } diff --git a/src/utils/map-helpers.js b/src/utils/map-helpers.js index 49e0942..9576535 100644 --- a/src/utils/map-helpers.js +++ b/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; +}