mirror of https://github.com/lukechilds/rollup.git
Rich Harris
8 years ago
committed by
GitHub
225 changed files with 2795 additions and 1300 deletions
@ -1,30 +0,0 @@ |
|||
export class Reference { |
|||
constructor ( node, scope, statement ) { |
|||
this.node = node; |
|||
this.scope = scope; |
|||
this.statement = statement; |
|||
|
|||
this.declaration = null; // bound later
|
|||
|
|||
this.parts = []; |
|||
|
|||
let root = node; |
|||
while ( root.type === 'MemberExpression' ) { |
|||
this.parts.unshift( root.property ); |
|||
root = root.object; |
|||
} |
|||
|
|||
this.name = root.name; |
|||
|
|||
this.start = node.start; |
|||
this.end = node.start + this.name.length; // can be overridden in the case of namespace members
|
|||
this.rewritten = false; |
|||
} |
|||
} |
|||
|
|||
export class SyntheticReference { |
|||
constructor ( name ) { |
|||
this.name = name; |
|||
this.parts = []; |
|||
} |
|||
} |
@ -1,160 +0,0 @@ |
|||
import { walk } from 'estree-walker'; |
|||
import Scope from './ast/Scope.js'; |
|||
import attachScopes from './ast/attachScopes.js'; |
|||
import modifierNodes, { isModifierNode } from './ast/modifierNodes.js'; |
|||
import isFunctionDeclaration from './ast/isFunctionDeclaration.js'; |
|||
import isReference from './ast/isReference.js'; |
|||
import getLocation from './utils/getLocation.js'; |
|||
import run from './utils/run.js'; |
|||
import { Reference } from './Reference.js'; |
|||
|
|||
export default class Statement { |
|||
constructor ( node, module, start, end ) { |
|||
this.node = node; |
|||
this.module = module; |
|||
this.start = start; |
|||
this.end = end; |
|||
this.next = null; // filled in later
|
|||
|
|||
this.scope = new Scope({ statement: this }); |
|||
|
|||
this.references = []; |
|||
this.stringLiteralRanges = []; |
|||
|
|||
this.isIncluded = false; |
|||
this.ran = false; |
|||
|
|||
this.isImportDeclaration = node.type === 'ImportDeclaration'; |
|||
this.isExportDeclaration = /^Export/.test( node.type ); |
|||
this.isReexportDeclaration = this.isExportDeclaration && !!node.source; |
|||
|
|||
this.isFunctionDeclaration = isFunctionDeclaration( node ) || |
|||
this.isExportDeclaration && isFunctionDeclaration( node.declaration ); |
|||
} |
|||
|
|||
firstPass () { |
|||
if ( this.isImportDeclaration ) return; // nothing to analyse
|
|||
|
|||
// attach scopes
|
|||
attachScopes( this ); |
|||
|
|||
// find references
|
|||
const statement = this; |
|||
let { module, references, scope, stringLiteralRanges } = this; |
|||
let contextDepth = 0; |
|||
|
|||
walk( this.node, { |
|||
enter ( node, parent, prop ) { |
|||
// warn about eval
|
|||
if ( node.type === 'CallExpression' && node.callee.name === 'eval' && !scope.contains( 'eval' ) ) { |
|||
// TODO show location
|
|||
module.bundle.onwarn( `Use of \`eval\` (in ${module.id}) is strongly discouraged, as it poses security risks and may cause issues with minification. See https://github.com/rollup/rollup/wiki/Troubleshooting#avoiding-eval for more details` ); |
|||
} |
|||
|
|||
// skip re-export declarations
|
|||
if ( node.type === 'ExportNamedDeclaration' && node.source ) return this.skip(); |
|||
|
|||
if ( node.type === 'TemplateElement' ) stringLiteralRanges.push([ node.start, node.end ]); |
|||
if ( node.type === 'Literal' && typeof node.value === 'string' && /\n/.test( node.raw ) ) { |
|||
stringLiteralRanges.push([ node.start + 1, node.end - 1 ]); |
|||
} |
|||
|
|||
if ( node.type === 'ThisExpression' && contextDepth === 0 ) { |
|||
module.magicString.overwrite( node.start, node.end, module.bundle.context ); |
|||
if ( module.bundle.context === 'undefined' ) module.bundle.onwarn( 'The `this` keyword is equivalent to `undefined` at the top level of an ES module, and has been rewritten' ); |
|||
} |
|||
|
|||
if ( node._scope ) scope = node._scope; |
|||
if ( /^Function/.test( node.type ) ) contextDepth += 1; |
|||
|
|||
let isReassignment; |
|||
|
|||
if ( parent && isModifierNode( parent ) ) { |
|||
let subject = parent[ modifierNodes[ parent.type ] ]; |
|||
|
|||
if ( node === subject ) { |
|||
let depth = 0; |
|||
|
|||
while ( subject.type === 'MemberExpression' ) { |
|||
subject = subject.object; |
|||
depth += 1; |
|||
} |
|||
|
|||
const importDeclaration = module.imports[ subject.name ]; |
|||
|
|||
if ( !scope.contains( subject.name ) && importDeclaration ) { |
|||
const minDepth = importDeclaration.name === '*' ? |
|||
2 : // cannot do e.g. `namespace.foo = bar`
|
|||
1; // cannot do e.g. `foo = bar`, but `foo.bar = bar` is fine
|
|||
|
|||
if ( depth < minDepth ) { |
|||
const err = new Error( `Illegal reassignment to import '${subject.name}'` ); |
|||
err.file = module.id; |
|||
err.loc = getLocation( module.magicString.original, subject.start ); |
|||
throw err; |
|||
} |
|||
} |
|||
|
|||
isReassignment = !depth; |
|||
} |
|||
} |
|||
|
|||
if ( isReference( node, parent ) ) { |
|||
// function declaration IDs are a special case – they're associated
|
|||
// with the parent scope
|
|||
const referenceScope = parent.type === 'FunctionDeclaration' && node === parent.id ? |
|||
scope.parent : |
|||
scope; |
|||
|
|||
const isShorthandProperty = parent.type === 'Property' && parent.shorthand; |
|||
|
|||
// Since `node.key` can equal `node.value` for shorthand properties
|
|||
// we must use the `prop` argument provided by `estree-walker` to determine
|
|||
// if we're looking at the key or the value.
|
|||
// If they are equal, we'll return to not create duplicate references.
|
|||
if ( isShorthandProperty && parent.value === parent.key && prop === 'value' ) { |
|||
return; |
|||
} |
|||
|
|||
const reference = new Reference( node, referenceScope, statement ); |
|||
reference.isReassignment = isReassignment; |
|||
reference.isShorthandProperty = isShorthandProperty; |
|||
references.push( reference ); |
|||
|
|||
this.skip(); // don't descend from `foo.bar.baz` into `foo.bar`
|
|||
} |
|||
}, |
|||
leave ( node ) { |
|||
if ( node._scope ) scope = scope.parent; |
|||
if ( /^Function/.test( node.type ) ) contextDepth -= 1; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
mark () { |
|||
if ( this.isIncluded ) return; // prevent infinite loops
|
|||
this.isIncluded = true; |
|||
|
|||
this.references.forEach( reference => { |
|||
if ( reference.declaration ) reference.declaration.use(); |
|||
}); |
|||
} |
|||
|
|||
run ( strongDependencies ) { |
|||
if ( ( this.ran && this.isIncluded ) || this.isImportDeclaration || this.isFunctionDeclaration ) return; |
|||
this.ran = true; |
|||
|
|||
if ( run( this.node, this.scope, this, strongDependencies, false ) ) { |
|||
this.mark(); |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
source () { |
|||
return this.module.source.slice( this.start, this.end ); |
|||
} |
|||
|
|||
toString () { |
|||
return this.module.magicString.slice( this.start, this.end ); |
|||
} |
|||
} |
@ -0,0 +1,92 @@ |
|||
import { UNKNOWN } from './values.js'; |
|||
import getLocation from '../utils/getLocation.js'; |
|||
|
|||
export default class Node { |
|||
bind ( scope ) { |
|||
this.eachChild( child => child.bind( scope ) ); |
|||
} |
|||
|
|||
eachChild ( callback ) { |
|||
for ( const key of this.keys ) { |
|||
if ( this.shorthand && key === 'key' ) continue; // key and value are the same
|
|||
|
|||
const value = this[ key ]; |
|||
|
|||
if ( value ) { |
|||
if ( 'length' in value ) { |
|||
for ( const child of value ) { |
|||
if ( child ) callback( child ); |
|||
} |
|||
} else if ( value ) { |
|||
callback( value ); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
findParent ( selector ) { |
|||
return selector.test( this.type ) ? this : this.parent.findParent( selector ); |
|||
} |
|||
|
|||
// TODO abolish findScope. if a node needs to store scope, store it
|
|||
findScope ( functionScope ) { |
|||
return this.parent.findScope( functionScope ); |
|||
} |
|||
|
|||
gatherPossibleValues ( values ) { |
|||
//this.eachChild( child => child.gatherPossibleValues( values ) );
|
|||
values.add( UNKNOWN ); |
|||
} |
|||
|
|||
getValue () { |
|||
return UNKNOWN; |
|||
} |
|||
|
|||
hasEffects ( scope ) { |
|||
for ( const key of this.keys ) { |
|||
const value = this[ key ]; |
|||
|
|||
if ( value ) { |
|||
if ( 'length' in value ) { |
|||
for ( const child of value ) { |
|||
if ( child && child.hasEffects( scope ) ) { |
|||
return true; |
|||
} |
|||
} |
|||
} else if ( value && value.hasEffects( scope ) ) { |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
initialise ( scope ) { |
|||
this.eachChild( child => child.initialise( scope ) ); |
|||
} |
|||
|
|||
locate () { |
|||
// useful for debugging
|
|||
const location = getLocation( this.module.code, this.start ); |
|||
location.file = this.module.id; |
|||
location.toString = () => JSON.stringify( location ); |
|||
|
|||
return location; |
|||
} |
|||
|
|||
render ( code, es ) { |
|||
this.eachChild( child => child.render( code, es ) ); |
|||
} |
|||
|
|||
run ( scope ) { |
|||
if ( this.ran ) return; |
|||
this.ran = true; |
|||
|
|||
this.eachChild( child => { |
|||
child.run( scope ); |
|||
}); |
|||
} |
|||
|
|||
toString () { |
|||
return this.module.code.slice( this.start, this.end ); |
|||
} |
|||
} |
@ -1,52 +0,0 @@ |
|||
import { blank, keys } from '../utils/object.js'; |
|||
import Declaration from '../Declaration.js'; |
|||
import extractNames from './extractNames.js'; |
|||
|
|||
export default class Scope { |
|||
constructor ( options ) { |
|||
options = options || {}; |
|||
|
|||
this.parent = options.parent; |
|||
this.statement = options.statement || this.parent.statement; |
|||
this.isBlockScope = !!options.block; |
|||
this.isTopLevel = !this.parent || ( this.parent.isTopLevel && this.isBlockScope ); |
|||
|
|||
this.declarations = blank(); |
|||
|
|||
if ( options.params ) { |
|||
options.params.forEach( param => { |
|||
extractNames( param ).forEach( name => { |
|||
this.declarations[ name ] = new Declaration( param, true, this.statement ); |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
addDeclaration ( node, isBlockDeclaration, isVar ) { |
|||
if ( !isBlockDeclaration && this.isBlockScope ) { |
|||
// it's a `var` or function node, and this
|
|||
// is a block scope, so we need to go up
|
|||
this.parent.addDeclaration( node, isBlockDeclaration, isVar ); |
|||
} else { |
|||
extractNames( node.id ).forEach( name => { |
|||
this.declarations[ name ] = new Declaration( node, false, this.statement ); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
contains ( name ) { |
|||
return this.declarations[ name ] || |
|||
( this.parent ? this.parent.contains( name ) : false ); |
|||
} |
|||
|
|||
eachDeclaration ( fn ) { |
|||
keys( this.declarations ).forEach( key => { |
|||
fn( key, this.declarations[ key ] ); |
|||
}); |
|||
} |
|||
|
|||
findDeclaration ( name ) { |
|||
return this.declarations[ name ] || |
|||
( this.parent && this.parent.findDeclaration( name ) ); |
|||
} |
|||
} |
@ -1,78 +0,0 @@ |
|||
import { walk } from 'estree-walker'; |
|||
import Scope from './Scope.js'; |
|||
|
|||
const blockDeclarations = { |
|||
const: true, |
|||
let: true |
|||
}; |
|||
|
|||
export default function attachScopes ( statement ) { |
|||
let { node, scope } = statement; |
|||
|
|||
walk( node, { |
|||
enter ( node, parent ) { |
|||
// function foo () {...}
|
|||
// class Foo {...}
|
|||
if ( /(Function|Class)Declaration/.test( node.type ) ) { |
|||
scope.addDeclaration( node, false, false ); |
|||
} |
|||
|
|||
// var foo = 1, bar = 2
|
|||
if ( node.type === 'VariableDeclaration' ) { |
|||
const isBlockDeclaration = blockDeclarations[ node.kind ]; |
|||
|
|||
node.declarations.forEach( declarator => { |
|||
scope.addDeclaration( declarator, isBlockDeclaration, true ); |
|||
}); |
|||
} |
|||
|
|||
let newScope; |
|||
|
|||
// create new function scope
|
|||
if ( /(Function|Class)/.test( node.type ) ) { |
|||
newScope = new Scope({ |
|||
parent: scope, |
|||
block: false, |
|||
params: node.params |
|||
}); |
|||
|
|||
// named function expressions - the name is considered
|
|||
// part of the function's scope
|
|||
if ( /(Function|Class)Expression/.test( node.type ) && node.id ) { |
|||
newScope.addDeclaration( node, false, false ); |
|||
} |
|||
} |
|||
|
|||
// create new block scope
|
|||
if ( node.type === 'BlockStatement' && ( !parent || !/Function/.test( parent.type ) ) ) { |
|||
newScope = new Scope({ |
|||
parent: scope, |
|||
block: true |
|||
}); |
|||
} |
|||
|
|||
// catch clause has its own block scope
|
|||
if ( node.type === 'CatchClause' ) { |
|||
newScope = new Scope({ |
|||
parent: scope, |
|||
params: [ node.param ], |
|||
block: true |
|||
}); |
|||
} |
|||
|
|||
if ( newScope ) { |
|||
Object.defineProperty( node, '_scope', { |
|||
value: newScope, |
|||
configurable: true |
|||
}); |
|||
|
|||
scope = newScope; |
|||
} |
|||
}, |
|||
leave ( node ) { |
|||
if ( node._scope ) { |
|||
scope = scope.parent; |
|||
} |
|||
} |
|||
}); |
|||
} |
@ -1,38 +0,0 @@ |
|||
export function isTruthy ( node ) { |
|||
if ( node.type === 'Literal' ) return !!node.value; |
|||
if ( node.type === 'ParenthesizedExpression' ) return isTruthy( node.expression ); |
|||
if ( node.operator in operators ) return operators[ node.operator ]( node ); |
|||
} |
|||
|
|||
export function isFalsy ( node ) { |
|||
return not( isTruthy( node ) ); |
|||
} |
|||
|
|||
function not ( value ) { |
|||
return value === undefined ? value : !value; |
|||
} |
|||
|
|||
function equals ( a, b, strict ) { |
|||
if ( a.type !== b.type ) return undefined; |
|||
if ( a.type === 'Literal' ) return strict ? a.value === b.value : a.value == b.value; |
|||
} |
|||
|
|||
const operators = { |
|||
'==': x => { |
|||
return equals( x.left, x.right, false ); |
|||
}, |
|||
|
|||
'!=': x => not( operators['==']( x ) ), |
|||
|
|||
'===': x => { |
|||
return equals( x.left, x.right, true ); |
|||
}, |
|||
|
|||
'!==': x => not( operators['===']( x ) ), |
|||
|
|||
'!': x => isFalsy( x.argument ), |
|||
|
|||
'&&': x => isTruthy( x.left ) && isTruthy( x.right ), |
|||
|
|||
'||': x => isTruthy( x.left ) || isTruthy( x.right ) |
|||
}; |
@ -1,7 +0,0 @@ |
|||
export function emptyBlockStatement ( start, end ) { |
|||
return { |
|||
start, end, |
|||
type: 'BlockStatement', |
|||
body: [] |
|||
}; |
|||
} |
@ -0,0 +1,63 @@ |
|||
import nodes from './nodes/index.js'; |
|||
import Node from './Node.js'; |
|||
import keys from './keys.js'; |
|||
|
|||
const newline = /\n/; |
|||
|
|||
export default function enhance ( ast, module, comments ) { |
|||
enhanceNode( ast, module, module, module.magicString ); |
|||
|
|||
let comment = comments.shift(); |
|||
|
|||
for ( const node of ast.body ) { |
|||
if ( comment && ( comment.start < node.start ) ) { |
|||
node.leadingCommentStart = comment.start; |
|||
} |
|||
|
|||
while ( comment && comment.end < node.end ) comment = comments.shift(); |
|||
|
|||
// if the next comment is on the same line as the end of the node,
|
|||
// treat is as a trailing comment
|
|||
if ( comment && !newline.test( module.code.slice( node.end, comment.start ) ) ) { |
|||
node.trailingCommentEnd = comment.end; // TODO is node.trailingCommentEnd used anywhere?
|
|||
comment = comments.shift(); |
|||
} |
|||
|
|||
node.initialise( module.scope ); |
|||
} |
|||
} |
|||
|
|||
function enhanceNode ( raw, parent, module, code ) { |
|||
if ( !raw ) return; |
|||
|
|||
if ( 'length' in raw ) { |
|||
for ( let i = 0; i < raw.length; i += 1 ) { |
|||
enhanceNode( raw[i], parent, module, code ); |
|||
} |
|||
|
|||
return; |
|||
} |
|||
|
|||
// with e.g. shorthand properties, key and value are
|
|||
// the same node. We don't want to enhance an object twice
|
|||
if ( raw.__enhanced ) return; |
|||
raw.__enhanced = true; |
|||
|
|||
if ( !keys[ raw.type ] ) { |
|||
keys[ raw.type ] = Object.keys( raw ).filter( key => typeof raw[ key ] === 'object' ); |
|||
} |
|||
|
|||
raw.parent = parent; |
|||
raw.module = module; |
|||
raw.keys = keys[ raw.type ]; |
|||
|
|||
code.addSourcemapLocation( raw.start ); |
|||
code.addSourcemapLocation( raw.end ); |
|||
|
|||
for ( const key of keys[ raw.type ] ) { |
|||
enhanceNode( raw[ key ], raw, module, code ); |
|||
} |
|||
|
|||
const type = nodes[ raw.type ] || Node; |
|||
raw.__proto__ = type.prototype; |
|||
} |
@ -1,6 +0,0 @@ |
|||
export default function isFunctionDeclaration ( node ) { |
|||
if ( !node ) return false; |
|||
|
|||
return node.type === 'FunctionDeclaration' || |
|||
( node.type === 'VariableDeclaration' && node.init && /FunctionExpression/.test( node.init.type ) ); |
|||
} |
@ -0,0 +1,4 @@ |
|||
export default { |
|||
Program: [ 'body' ], |
|||
Literal: [] |
|||
}; |
@ -1,19 +0,0 @@ |
|||
const modifierNodes = { |
|||
AssignmentExpression: 'left', |
|||
UpdateExpression: 'argument', |
|||
UnaryExpression: 'argument' |
|||
}; |
|||
|
|||
export default modifierNodes; |
|||
|
|||
export function isModifierNode ( node ) { |
|||
if ( !( node.type in modifierNodes ) ) { |
|||
return false; |
|||
} |
|||
|
|||
if ( node.type === 'UnaryExpression' ) { |
|||
return node.operator === 'delete'; |
|||
} |
|||
|
|||
return true; |
|||
} |
@ -0,0 +1,8 @@ |
|||
import Node from '../Node.js'; |
|||
import { ARRAY } from '../values.js'; |
|||
|
|||
export default class ArrayExpression extends Node { |
|||
gatherPossibleValues ( values ) { |
|||
values.add( ARRAY ); |
|||
} |
|||
} |
@ -0,0 +1,35 @@ |
|||
import Node from '../Node.js'; |
|||
import Scope from '../scopes/Scope.js'; |
|||
import extractNames from '../utils/extractNames.js'; |
|||
|
|||
export default class ArrowFunctionExpression extends Node { |
|||
initialise ( scope ) { |
|||
if ( this.body.type !== 'BlockStatement' ) { |
|||
this.scope = new Scope({ |
|||
parent: scope, |
|||
isBlockScope: false, |
|||
isLexicalBoundary: false |
|||
}); |
|||
|
|||
for ( const param of this.params ) { |
|||
for ( const name of extractNames( param ) ) { |
|||
this.scope.addDeclaration( name, null, null, true ); // TODO ugh
|
|||
} |
|||
} |
|||
} |
|||
|
|||
super.initialise( scope ); |
|||
} |
|||
|
|||
bind ( scope ) { |
|||
super.bind( this.scope || scope ); |
|||
} |
|||
|
|||
findScope ( functionScope ) { |
|||
return this.scope || this.parent.findScope( functionScope ); |
|||
} |
|||
|
|||
hasEffects () { |
|||
return false; |
|||
} |
|||
} |
@ -0,0 +1,45 @@ |
|||
import Node from '../Node.js'; |
|||
import disallowIllegalReassignment from './shared/disallowIllegalReassignment.js'; |
|||
import isUsedByBundle from './shared/isUsedByBundle.js'; |
|||
import { NUMBER, STRING } from '../values.js'; |
|||
|
|||
export default class AssignmentExpression extends Node { |
|||
bind ( scope ) { |
|||
let subject = this.left; |
|||
while ( this.left.type === 'ParenthesizedExpression' ) subject = subject.expression; |
|||
|
|||
this.subject = subject; |
|||
disallowIllegalReassignment( scope, subject ); |
|||
|
|||
if ( subject.type === 'Identifier' ) { |
|||
const declaration = scope.findDeclaration( subject.name ); |
|||
declaration.isReassigned = true; |
|||
|
|||
if ( declaration.possibleValues ) { // TODO this feels hacky
|
|||
if ( this.operator === '=' ) { |
|||
declaration.possibleValues.add( this.right ); |
|||
} else if ( this.operator === '+=' ) { |
|||
declaration.possibleValues.add( STRING ).add( NUMBER ); |
|||
} else { |
|||
declaration.possibleValues.add( NUMBER ); |
|||
} |
|||
} |
|||
} |
|||
|
|||
super.bind( scope ); |
|||
} |
|||
|
|||
hasEffects ( scope ) { |
|||
const hasEffects = this.isUsedByBundle() || this.right.hasEffects( scope ); |
|||
return hasEffects; |
|||
} |
|||
|
|||
initialise ( scope ) { |
|||
this.module.bundle.dependentExpressions.push( this ); |
|||
super.initialise( scope ); |
|||
} |
|||
|
|||
isUsedByBundle () { |
|||
return isUsedByBundle( this.findScope(), this.subject ); |
|||
} |
|||
} |
@ -0,0 +1,38 @@ |
|||
import Node from '../Node.js'; |
|||
import { UNKNOWN } from '../values.js'; |
|||
|
|||
const operators = { |
|||
'==': ( left, right ) => left == right, |
|||
'!=': ( left, right ) => left != right, |
|||
'===': ( left, right ) => left === right, |
|||
'!==': ( left, right ) => left !== right, |
|||
'<': ( left, right ) => left < right, |
|||
'<=': ( left, right ) => left <= right, |
|||
'>': ( left, right ) => left > right, |
|||
'>=': ( left, right ) => left >= right, |
|||
'<<': ( left, right ) => left << right, |
|||
'>>': ( left, right ) => left >> right, |
|||
'>>>': ( left, right ) => left >>> right, |
|||
'+': ( left, right ) => left + right, |
|||
'-': ( left, right ) => left - right, |
|||
'*': ( left, right ) => left * right, |
|||
'/': ( left, right ) => left / right, |
|||
'%': ( left, right ) => left % right, |
|||
'|': ( left, right ) => left | right, |
|||
'^': ( left, right ) => left ^ right, |
|||
'&': ( left, right ) => left & right, |
|||
in: ( left, right ) => left in right, |
|||
instanceof: ( left, right ) => left instanceof right |
|||
}; |
|||
|
|||
export default class BinaryExpression extends Node { |
|||
getValue () { |
|||
const leftValue = this.left.getValue(); |
|||
if ( leftValue === UNKNOWN ) return UNKNOWN; |
|||
|
|||
const rightValue = this.right.getValue(); |
|||
if ( rightValue === UNKNOWN ) return UNKNOWN; |
|||
|
|||
return operators[ this.operator ]( leftValue, rightValue ); |
|||
} |
|||
} |
@ -0,0 +1,71 @@ |
|||
import Node from '../Node.js'; |
|||
import Scope from '../scopes/Scope.js'; |
|||
import extractNames from '../utils/extractNames.js'; |
|||
|
|||
export default class BlockStatement extends Node { |
|||
bind () { |
|||
for ( const node of this.body ) { |
|||
node.bind( this.scope ); |
|||
} |
|||
} |
|||
|
|||
createScope ( parent ) { |
|||
this.parentIsFunction = /Function/.test( this.parent.type ); |
|||
this.isFunctionBlock = this.parentIsFunction || this.parent.type === 'Module'; |
|||
|
|||
this.scope = new Scope({ |
|||
isBlockScope: !this.isFunctionBlock, |
|||
isLexicalBoundary: this.isFunctionBlock && this.parent.type !== 'ArrowFunctionExpression', |
|||
parent: parent || this.parent.findScope( false ), // TODO always supply parent
|
|||
owner: this // TODO is this used anywhere?
|
|||
}); |
|||
|
|||
const params = this.parent.params || ( this.parent.type === 'CatchClause' && [ this.parent.param ] ); |
|||
|
|||
if ( params && params.length ) { |
|||
params.forEach( node => { |
|||
extractNames( node ).forEach( name => { |
|||
this.scope.addDeclaration( name, node, false, true ); |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
findScope ( functionScope ) { |
|||
return functionScope && !this.isFunctionBlock ? this.parent.findScope( functionScope ) : this.scope; |
|||
} |
|||
|
|||
hasEffects () { |
|||
for ( const node of this.body ) { |
|||
if ( node.hasEffects( this.scope ) ) return true; |
|||
} |
|||
} |
|||
|
|||
initialise () { |
|||
if ( !this.scope ) this.createScope(); // scope can be created early in some cases, e.g for (let i... )
|
|||
|
|||
let lastNode; |
|||
for ( const node of this.body ) { |
|||
node.initialise( this.scope ); |
|||
|
|||
if ( lastNode ) lastNode.next = node.start; |
|||
lastNode = node; |
|||
} |
|||
} |
|||
|
|||
render ( code, es ) { |
|||
for ( const node of this.body ) { |
|||
node.render( code, es ); |
|||
} |
|||
} |
|||
|
|||
run () { |
|||
if ( this.ran ) return; |
|||
this.ran = true; |
|||
|
|||
for ( const node of this.body ) { |
|||
// TODO only include non-top-level statements if necessary
|
|||
node.run( this.scope ); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,40 @@ |
|||
import getLocation from '../../utils/getLocation.js'; |
|||
import error from '../../utils/error.js'; |
|||
import Node from '../Node.js'; |
|||
import callHasEffects from './shared/callHasEffects.js'; |
|||
|
|||
export default class CallExpression extends Node { |
|||
bind ( scope ) { |
|||
if ( this.callee.type === 'Identifier' ) { |
|||
const declaration = scope.findDeclaration( this.callee.name ); |
|||
|
|||
if ( declaration.isNamespace ) { |
|||
error({ |
|||
message: `Cannot call a namespace ('${this.callee.name}')`, |
|||
file: this.module.id, |
|||
pos: this.start, |
|||
loc: getLocation( this.module.code, this.start ) |
|||
}); |
|||
} |
|||
|
|||
if ( this.callee.name === 'eval' && declaration.isGlobal ) { |
|||
this.module.bundle.onwarn( `Use of \`eval\` (in ${this.module.id}) is strongly discouraged, as it poses security risks and may cause issues with minification. See https://github.com/rollup/rollup/wiki/Troubleshooting#avoiding-eval for more details` ); |
|||
} |
|||
} |
|||
|
|||
super.bind( scope ); |
|||
} |
|||
|
|||
hasEffects ( scope ) { |
|||
return callHasEffects( scope, this.callee ); |
|||
} |
|||
|
|||
initialise ( scope ) { |
|||
this.module.bundle.dependentExpressions.push( this ); |
|||
super.initialise( scope ); |
|||
} |
|||
|
|||
isUsedByBundle () { |
|||
return this.hasEffects( this.findScope() ); |
|||
} |
|||
} |
@ -0,0 +1,40 @@ |
|||
import Node from '../Node.js'; |
|||
|
|||
// TODO is this basically identical to FunctionDeclaration?
|
|||
export default class ClassDeclaration extends Node { |
|||
activate () { |
|||
if ( this.activated ) return; |
|||
this.activated = true; |
|||
|
|||
this.body.run(); |
|||
} |
|||
|
|||
addReference () { |
|||
/* noop? */ |
|||
} |
|||
|
|||
gatherPossibleValues ( values ) { |
|||
values.add( this ); |
|||
} |
|||
|
|||
getName () { |
|||
return this.id.name; |
|||
} |
|||
|
|||
hasEffects () { |
|||
return false; |
|||
} |
|||
|
|||
initialise ( scope ) { |
|||
scope.addDeclaration( this.id.name, this, false, false ); |
|||
super.initialise( scope ); |
|||
} |
|||
|
|||
render ( code, es ) { |
|||
if ( this.activated ) { |
|||
super.render( code, es ); |
|||
} else { |
|||
code.remove( this.leadingCommentStart || this.start, this.next || this.end ); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,26 @@ |
|||
import Node from '../Node.js'; |
|||
import Scope from '../scopes/Scope.js'; |
|||
|
|||
export default class ClassExpression extends Node { |
|||
bind () { |
|||
super.bind( this.scope ); |
|||
} |
|||
|
|||
findScope () { |
|||
return this.scope; |
|||
} |
|||
|
|||
initialise () { |
|||
this.scope = new Scope({ |
|||
isBlockScope: true, |
|||
parent: this.parent.findScope( false ) |
|||
}); |
|||
|
|||
if ( this.id ) { |
|||
// function expression IDs belong to the child scope...
|
|||
this.scope.addDeclaration( this.id.name, this, false, true ); |
|||
} |
|||
|
|||
super.initialise( this.scope ); |
|||
} |
|||
} |
@ -0,0 +1,65 @@ |
|||
import Node from '../Node.js'; |
|||
import { UNKNOWN } from '../values.js'; |
|||
|
|||
export default class ConditionalExpression extends Node { |
|||
initialise ( scope ) { |
|||
if ( this.module.bundle.treeshake ) { |
|||
this.testValue = this.test.getValue(); |
|||
|
|||
if ( this.testValue === UNKNOWN ) { |
|||
super.initialise( scope ); |
|||
} |
|||
|
|||
else if ( this.testValue ) { |
|||
this.consequent.initialise( scope ); |
|||
this.alternate = null; |
|||
} else if ( this.alternate ) { |
|||
this.alternate.initialise( scope ); |
|||
this.consequent = null; |
|||
} |
|||
} |
|||
|
|||
else { |
|||
super.initialise( scope ); |
|||
} |
|||
} |
|||
|
|||
gatherPossibleValues ( values ) { |
|||
const testValue = this.test.getValue(); |
|||
|
|||
if ( testValue === UNKNOWN ) { |
|||
values.add( this.consequent ).add( this.alternate ); |
|||
} else { |
|||
values.add( testValue ? this.consequent : this.alternate ); |
|||
} |
|||
} |
|||
|
|||
getValue () { |
|||
const testValue = this.test.getValue(); |
|||
if ( testValue === UNKNOWN ) return UNKNOWN; |
|||
|
|||
return testValue ? this.consequent.getValue() : this.alternate.getValue(); |
|||
} |
|||
|
|||
render ( code, es ) { |
|||
if ( !this.module.bundle.treeshake ) { |
|||
super.render( code, es ); |
|||
} |
|||
|
|||
else { |
|||
if ( this.testValue === UNKNOWN ) { |
|||
super.render( code, es ); |
|||
} |
|||
|
|||
else if ( this.testValue ) { |
|||
code.remove( this.start, this.consequent.start ); |
|||
code.remove( this.consequent.end, this.end ); |
|||
this.consequent.render( code, es ); |
|||
} else { |
|||
code.remove( this.start, this.alternate.start ); |
|||
code.remove( this.alternate.end, this.end ); |
|||
this.alternate.render( code, es ); |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,11 @@ |
|||
import Node from '../Node.js'; |
|||
|
|||
export default class ExportAllDeclaration extends Node { |
|||
initialise () { |
|||
this.isExportDeclaration = true; |
|||
} |
|||
|
|||
render ( code ) { |
|||
code.remove( this.leadingCommentStart || this.start, this.next || this.end ); |
|||
} |
|||
} |
@ -0,0 +1,96 @@ |
|||
import Node from '../Node.js'; |
|||
|
|||
const functionOrClassDeclaration = /^(?:Function|Class)Declaration/; |
|||
|
|||
export default class ExportDefaultDeclaration extends Node { |
|||
initialise ( scope ) { |
|||
this.isExportDeclaration = true; |
|||
this.isDefault = true; |
|||
|
|||
this.name = ( this.declaration.id && this.declaration.id.name ) || this.declaration.name || this.module.basename(); |
|||
scope.declarations.default = this; |
|||
|
|||
this.declaration.initialise( scope ); |
|||
} |
|||
|
|||
activate () { |
|||
if ( this.activated ) return; |
|||
this.activated = true; |
|||
|
|||
this.run(); |
|||
} |
|||
|
|||
addReference ( reference ) { |
|||
this.name = reference.name; |
|||
if ( this.original ) this.original.addReference( reference ); |
|||
} |
|||
|
|||
bind ( scope ) { |
|||
const name = ( this.declaration.id && this.declaration.id.name ) || this.declaration.name; |
|||
if ( name ) this.original = scope.findDeclaration( name ); |
|||
|
|||
this.declaration.bind( scope ); |
|||
} |
|||
|
|||
gatherPossibleValues ( values ) { |
|||
this.declaration.gatherPossibleValues( values ); |
|||
} |
|||
|
|||
getName ( es ) { |
|||
if ( this.original && !this.original.isReassigned ) { |
|||
return this.original.getName( es ); |
|||
} |
|||
|
|||
return this.name; |
|||
} |
|||
|
|||
// TODO this is total chaos, tidy it up
|
|||
render ( code, es ) { |
|||
const treeshake = this.module.bundle.treeshake; |
|||
const name = this.getName( es ); |
|||
|
|||
if ( this.shouldInclude ) { |
|||
if ( this.activated ) { |
|||
if ( functionOrClassDeclaration.test( this.declaration.type ) ) { |
|||
if ( this.declaration.id ) { |
|||
code.remove( this.start, this.declaration.start ); |
|||
} else { |
|||
throw new Error( 'TODO anonymous class/function declaration' ); |
|||
} |
|||
} |
|||
|
|||
else { |
|||
if ( this.original && this.original.getName( es ) === name ) { |
|||
// prevent `var foo = foo`
|
|||
code.remove( this.leadingCommentStart || this.start, this.next || this.end ); |
|||
return; // don't render children. TODO this seems like a bit of a hack
|
|||
} else { |
|||
code.overwrite( this.start, this.declaration.start, `${this.module.bundle.varOrConst} ${name} = ` ); |
|||
} |
|||
} |
|||
} else { |
|||
// remove `var foo` from `var foo = bar()`, if `foo` is unused
|
|||
code.remove( this.start, this.declaration.start ); |
|||
} |
|||
|
|||
super.render( code, es ); |
|||
} else { |
|||
if ( treeshake ) { |
|||
if ( functionOrClassDeclaration.test( this.declaration.type ) && !this.declaration.activated ) { |
|||
code.remove( this.leadingCommentStart || this.start, this.next || this.end ); |
|||
} else { |
|||
const hasEffects = this.declaration.hasEffects( this.module.scope ); |
|||
code.remove( this.start, hasEffects ? this.declaration.start : this.next || this.end ); |
|||
} |
|||
} else { |
|||
code.overwrite( this.start, this.declaration.start, `${this.module.bundle.varOrConst} ${name} = ` ); |
|||
} |
|||
// code.remove( this.start, this.next || this.end );
|
|||
} |
|||
} |
|||
|
|||
run ( scope ) { |
|||
this.shouldInclude = true; |
|||
super.run( scope ); |
|||
} |
|||
} |
@ -0,0 +1,25 @@ |
|||
import Node from '../Node.js'; |
|||
|
|||
export default class ExportNamedDeclaration extends Node { |
|||
initialise ( scope ) { |
|||
this.isExportDeclaration = true; |
|||
if ( this.declaration ) { |
|||
this.declaration.initialise( scope ); |
|||
} |
|||
} |
|||
|
|||
bind ( scope ) { |
|||
if ( this.declaration ) { |
|||
this.declaration.bind( scope ); |
|||
} |
|||
} |
|||
|
|||
render ( code, es ) { |
|||
if ( this.declaration ) { |
|||
code.remove( this.start, this.declaration.start ); |
|||
this.declaration.render( code, es ); |
|||
} else { |
|||
code.remove( this.leadingCommentStart || this.start, this.next || this.end ); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,16 @@ |
|||
import Node from '../Node.js'; |
|||
|
|||
export default class ExpressionStatement extends Node { |
|||
render ( code, es ) { |
|||
if ( !this.module.bundle.treeshake || this.shouldInclude ) { |
|||
super.render( code, es ); |
|||
} else { |
|||
code.remove( this.leadingCommentStart || this.start, this.next || this.end ); |
|||
} |
|||
} |
|||
|
|||
run ( scope ) { |
|||
this.shouldInclude = true; |
|||
super.run( scope ); |
|||
} |
|||
} |
@ -0,0 +1,53 @@ |
|||
import Node from '../Node.js'; |
|||
|
|||
export default class FunctionDeclaration extends Node { |
|||
activate () { |
|||
if ( this.activated ) return; |
|||
this.activated = true; |
|||
|
|||
const scope = this.body.scope; |
|||
this.params.forEach( param => param.run( scope ) ); // in case of assignment patterns
|
|||
this.body.run(); |
|||
} |
|||
|
|||
addReference () { |
|||
/* noop? */ |
|||
} |
|||
|
|||
bind ( scope ) { |
|||
this.id.bind( scope ); |
|||
this.params.forEach( param => param.bind( this.body.scope ) ); |
|||
this.body.bind( scope ); |
|||
} |
|||
|
|||
gatherPossibleValues ( values ) { |
|||
values.add( this ); |
|||
} |
|||
|
|||
getName () { |
|||
return this.name; |
|||
} |
|||
|
|||
hasEffects () { |
|||
return false; |
|||
} |
|||
|
|||
initialise ( scope ) { |
|||
this.name = this.id.name; // may be overridden by bundle.deconflict
|
|||
scope.addDeclaration( this.name, this, false, false ); |
|||
|
|||
this.body.createScope(); |
|||
|
|||
this.id.initialise( scope ); |
|||
this.params.forEach( param => param.initialise( this.body.scope ) ); |
|||
this.body.initialise( scope ); |
|||
} |
|||
|
|||
render ( code, es ) { |
|||
if ( !this.module.bundle.treeshake || this.activated ) { |
|||
super.render( code, es ); |
|||
} else { |
|||
code.remove( this.leadingCommentStart || this.start, this.next || this.end ); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,21 @@ |
|||
import Node from '../Node.js'; |
|||
|
|||
export default class FunctionExpression extends Node { |
|||
bind () { |
|||
if ( this.id ) this.id.bind( this.body.scope ); |
|||
this.params.forEach( param => param.bind( this.body.scope ) ); |
|||
this.body.bind(); |
|||
} |
|||
|
|||
hasEffects () { |
|||
return false; |
|||
} |
|||
|
|||
initialise () { |
|||
this.body.createScope(); // TODO we'll also need to do this for For[Of|In]Statement
|
|||
|
|||
if ( this.id ) this.id.initialise( this.body.scope ); |
|||
this.params.forEach( param => param.initialise( this.body.scope ) ); |
|||
this.body.initialise(); |
|||
} |
|||
} |
@ -0,0 +1,35 @@ |
|||
import Node from '../Node.js'; |
|||
import isReference from '../utils/isReference.js'; |
|||
|
|||
export default class Identifier extends Node { |
|||
bind ( scope ) { |
|||
if ( isReference( this, this.parent ) ) { |
|||
this.declaration = scope.findDeclaration( this.name ); |
|||
this.declaration.addReference( this ); // TODO necessary?
|
|||
} |
|||
} |
|||
|
|||
gatherPossibleValues ( values ) { |
|||
if ( isReference( this, this.parent ) ) { |
|||
values.add( this ); |
|||
} |
|||
} |
|||
|
|||
render ( code, es ) { |
|||
if ( this.declaration ) { |
|||
const name = this.declaration.getName( es ); |
|||
if ( name !== this.name ) { |
|||
code.overwrite( this.start, this.end, name, true ); |
|||
|
|||
// special case
|
|||
if ( this.parent.type === 'Property' && this.parent.shorthand ) { |
|||
code.insertLeft( this.start, `${this.name}: ` ); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
run () { |
|||
if ( this.declaration ) this.declaration.activate(); |
|||
} |
|||
} |
@ -0,0 +1,55 @@ |
|||
import Node from '../Node.js'; |
|||
import { UNKNOWN } from '../values.js'; |
|||
|
|||
// TODO DRY this out
|
|||
export default class IfStatement extends Node { |
|||
initialise ( scope ) { |
|||
this.testValue = this.test.getValue(); |
|||
|
|||
if ( this.module.bundle.treeshake ) { |
|||
if ( this.testValue === UNKNOWN ) { |
|||
super.initialise( scope ); |
|||
} |
|||
|
|||
else if ( this.testValue ) { |
|||
this.consequent.initialise( scope ); |
|||
this.alternate = null; |
|||
} else { |
|||
if ( this.alternate ) this.alternate.initialise( scope ); |
|||
this.consequent = null; |
|||
} |
|||
} |
|||
|
|||
else { |
|||
super.initialise( scope ); |
|||
} |
|||
} |
|||
|
|||
render ( code, es ) { |
|||
if ( this.module.bundle.treeshake ) { |
|||
if ( this.testValue === UNKNOWN ) { |
|||
super.render( code, es ); |
|||
} |
|||
|
|||
else { |
|||
code.overwrite( this.test.start, this.test.end, JSON.stringify( this.testValue ) ); |
|||
|
|||
// TODO if no block-scoped declarations, remove enclosing
|
|||
// curlies and dedent block (if there is a block)
|
|||
|
|||
if ( this.testValue ) { |
|||
code.remove( this.start, this.consequent.start ); |
|||
code.remove( this.consequent.end, this.end ); |
|||
this.consequent.render( code, es ); |
|||
} else { |
|||
code.remove( this.start, this.alternate ? this.alternate.start : this.next || this.end ); |
|||
if ( this.alternate ) this.alternate.render( code, es ); |
|||
} |
|||
} |
|||
} |
|||
|
|||
else { |
|||
super.render( code, es ); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,16 @@ |
|||
import Node from '../Node.js'; |
|||
|
|||
export default class ImportDeclaration extends Node { |
|||
bind () { |
|||
// noop
|
|||
// TODO do the inter-module binding setup here?
|
|||
} |
|||
|
|||
initialise () { |
|||
this.isImportDeclaration = true; |
|||
} |
|||
|
|||
render ( code ) { |
|||
code.remove( this.start, this.next || this.end ); |
|||
} |
|||
} |
@ -0,0 +1,17 @@ |
|||
import Node from '../Node.js'; |
|||
|
|||
export default class Literal extends Node { |
|||
getValue () { |
|||
return this.value; |
|||
} |
|||
|
|||
gatherPossibleValues ( values ) { |
|||
values.add( this ); |
|||
} |
|||
|
|||
render ( code ) { |
|||
if ( typeof this.value === 'string' ) { |
|||
code.indentExclusionRanges.push([ this.start + 1, this.end - 1 ]); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,74 @@ |
|||
import isReference from '../utils/isReference.js'; |
|||
import Node from '../Node.js'; |
|||
import { UNKNOWN } from '../values.js'; |
|||
|
|||
class Keypath { |
|||
constructor ( node ) { |
|||
this.parts = []; |
|||
|
|||
while ( node.type === 'MemberExpression' ) { |
|||
this.parts.unshift( node.property ); |
|||
node = node.object; |
|||
} |
|||
|
|||
this.root = node; |
|||
} |
|||
} |
|||
|
|||
export default class MemberExpression extends Node { |
|||
bind ( scope ) { |
|||
// if this resolves to a namespaced declaration, prepare
|
|||
// to replace it
|
|||
// TODO this code is a bit inefficient
|
|||
if ( isReference( this ) ) { // TODO optimise namespace access like `foo['bar']` as well
|
|||
const keypath = new Keypath( this ); |
|||
|
|||
let declaration = scope.findDeclaration( keypath.root.name ); |
|||
|
|||
while ( declaration.isNamespace && keypath.parts.length ) { |
|||
const part = keypath.parts[0]; |
|||
declaration = declaration.module.traceExport( part.name ); |
|||
|
|||
if ( !declaration ) { |
|||
this.module.bundle.onwarn( `Export '${part.name}' is not defined by '${this.module.id}'` ); |
|||
break; |
|||
} |
|||
|
|||
keypath.parts.shift(); |
|||
} |
|||
|
|||
if ( keypath.parts.length ) { |
|||
super.bind( scope ); |
|||
return; // not a namespaced declaration
|
|||
} |
|||
|
|||
this.declaration = declaration; |
|||
|
|||
if ( declaration.isExternal ) { |
|||
declaration.module.suggestName( keypath.root.name ); |
|||
} |
|||
} |
|||
|
|||
else { |
|||
super.bind( scope ); |
|||
} |
|||
} |
|||
|
|||
gatherPossibleValues ( values ) { |
|||
values.add( UNKNOWN ); // TODO
|
|||
} |
|||
|
|||
render ( code, es ) { |
|||
if ( this.declaration ) { |
|||
const name = this.declaration.getName( es ); |
|||
if ( name !== this.name ) code.overwrite( this.start, this.end, name, true ); |
|||
} |
|||
|
|||
super.render( code, es ); |
|||
} |
|||
|
|||
run ( scope ) { |
|||
if ( this.declaration ) this.declaration.activate(); |
|||
super.run( scope ); |
|||
} |
|||
} |
@ -0,0 +1,8 @@ |
|||
import Node from '../Node.js'; |
|||
import callHasEffects from './shared/callHasEffects.js'; |
|||
|
|||
export default class NewExpression extends Node { |
|||
hasEffects ( scope ) { |
|||
return callHasEffects( scope, this.callee ); |
|||
} |
|||
} |
@ -0,0 +1,8 @@ |
|||
import Node from '../Node.js'; |
|||
import { OBJECT } from '../values.js'; |
|||
|
|||
export default class ObjectExpression extends Node { |
|||
gatherPossibleValues ( values ) { |
|||
values.add( OBJECT ); |
|||
} |
|||
} |
@ -0,0 +1,11 @@ |
|||
import Node from '../Node.js'; |
|||
|
|||
export default class ParenthesizedExpression extends Node { |
|||
getPossibleValues ( values ) { |
|||
return this.expression.getPossibleValues( values ); |
|||
} |
|||
|
|||
getValue () { |
|||
return this.expression.getValue(); |
|||
} |
|||
} |
@ -0,0 +1,7 @@ |
|||
import Node from '../Node.js'; |
|||
|
|||
export default class ReturnStatement extends Node { |
|||
// hasEffects () {
|
|||
// return true;
|
|||
// }
|
|||
} |
@ -0,0 +1,7 @@ |
|||
import Node from '../Node.js'; |
|||
|
|||
export default class TemplateLiteral extends Node { |
|||
render ( code ) { |
|||
code.indentExclusionRanges.push([ this.start, this.end ]); |
|||
} |
|||
} |
@ -0,0 +1,20 @@ |
|||
import Node from '../Node.js'; |
|||
|
|||
export default class ThisExpression extends Node { |
|||
initialise ( scope ) { |
|||
const lexicalBoundary = scope.findLexicalBoundary(); |
|||
|
|||
if ( lexicalBoundary.isModuleScope ) { |
|||
this.alias = this.module.bundle.context; |
|||
if ( this.alias === 'undefined' ) { |
|||
this.module.bundle.onwarn( 'The `this` keyword is equivalent to `undefined` at the top level of an ES module, and has been rewritten' ); |
|||
} |
|||
} |
|||
} |
|||
|
|||
render ( code ) { |
|||
if ( this.alias ) { |
|||
code.overwrite( this.start, this.end, this.alias, true ); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,34 @@ |
|||
import Node from '../Node.js'; |
|||
import { UNKNOWN } from '../values.js'; |
|||
|
|||
const operators = { |
|||
"-": value => -value, |
|||
"+": value => +value, |
|||
"!": value => !value, |
|||
"~": value => ~value, |
|||
typeof: value => typeof value, |
|||
void: () => undefined, |
|||
delete: () => UNKNOWN |
|||
}; |
|||
|
|||
export default class UnaryExpression extends Node { |
|||
bind ( scope ) { |
|||
if ( this.value === UNKNOWN ) super.bind( scope ); |
|||
} |
|||
|
|||
getValue () { |
|||
const argumentValue = this.argument.getValue(); |
|||
if ( argumentValue === UNKNOWN ) return UNKNOWN; |
|||
|
|||
return operators[ this.operator ]( argumentValue ); |
|||
} |
|||
|
|||
hasEffects ( scope ) { |
|||
return this.operator === 'delete' || this.argument.hasEffects( scope ); |
|||
} |
|||
|
|||
initialise ( scope ) { |
|||
this.value = this.getValue(); |
|||
if ( this.value === UNKNOWN ) super.initialise( scope ); |
|||
} |
|||
} |
@ -0,0 +1,38 @@ |
|||
import Node from '../Node.js'; |
|||
import disallowIllegalReassignment from './shared/disallowIllegalReassignment.js'; |
|||
import isUsedByBundle from './shared/isUsedByBundle.js'; |
|||
import { NUMBER } from '../values.js'; |
|||
|
|||
export default class UpdateExpression extends Node { |
|||
bind ( scope ) { |
|||
let subject = this.argument; |
|||
while ( this.argument.type === 'ParenthesizedExpression' ) subject = subject.expression; |
|||
|
|||
this.subject = subject; |
|||
disallowIllegalReassignment( scope, this.argument ); |
|||
|
|||
if ( subject.type === 'Identifier' ) { |
|||
const declaration = scope.findDeclaration( subject.name ); |
|||
declaration.isReassigned = true; |
|||
|
|||
if ( declaration.possibleValues ) { |
|||
declaration.possibleValues.add( NUMBER ); |
|||
} |
|||
} |
|||
|
|||
super.bind( scope ); |
|||
} |
|||
|
|||
hasEffects ( scope ) { |
|||
return isUsedByBundle( scope, this.subject ); |
|||
} |
|||
|
|||
initialise ( scope ) { |
|||
this.module.bundle.dependentExpressions.push( this ); |
|||
super.initialise( scope ); |
|||
} |
|||
|
|||
isUsedByBundle () { |
|||
return isUsedByBundle( this.findScope(), this.subject ); |
|||
} |
|||
} |
@ -0,0 +1,100 @@ |
|||
import Node from '../Node.js'; |
|||
import extractNames from '../utils/extractNames.js'; |
|||
|
|||
function getSeparator ( code, start ) { |
|||
let c = start; |
|||
|
|||
while ( c > 0 && code[ c - 1 ] !== '\n' ) { |
|||
c -= 1; |
|||
if ( code[c] === ';' || code[c] === '{' ) return '; '; |
|||
} |
|||
|
|||
const lineStart = code.slice( c, start ).match( /^\s*/ )[0]; |
|||
|
|||
return `;\n${lineStart}`; |
|||
} |
|||
|
|||
const forStatement = /^For(?:Of|In)Statement/; |
|||
|
|||
export default class VariableDeclaration extends Node { |
|||
initialise ( scope ) { |
|||
this.scope = scope; |
|||
super.initialise( scope ); |
|||
} |
|||
|
|||
render ( code, es ) { |
|||
const treeshake = this.module.bundle.treeshake; |
|||
|
|||
let shouldSeparate = false; |
|||
let separator; |
|||
|
|||
if ( this.scope.isModuleScope && !forStatement.test( this.parent.type ) ) { |
|||
shouldSeparate = true; |
|||
separator = getSeparator( this.module.code, this.start ); |
|||
} |
|||
|
|||
let c = this.start; |
|||
let empty = true; |
|||
|
|||
for ( let i = 0; i < this.declarations.length; i += 1 ) { |
|||
const declarator = this.declarations[i]; |
|||
|
|||
const prefix = empty ? '' : separator; // TODO indentation
|
|||
|
|||
if ( declarator.id.type === 'Identifier' ) { |
|||
const proxy = declarator.proxies.get( declarator.id.name ); |
|||
const isExportedAndReassigned = !es && proxy.exportName && proxy.isReassigned; |
|||
|
|||
if ( isExportedAndReassigned ) { |
|||
if ( declarator.init ) { |
|||
if ( shouldSeparate ) code.overwrite( c, declarator.start, prefix ); |
|||
c = declarator.end; |
|||
empty = false; |
|||
} |
|||
} else if ( !treeshake || proxy.activated ) { |
|||
if ( shouldSeparate ) code.overwrite( c, declarator.start, `${prefix}${this.kind} ` ); // TODO indentation
|
|||
c = declarator.end; |
|||
empty = false; |
|||
} |
|||
} |
|||
|
|||
else { |
|||
const exportAssignments = []; |
|||
let activated = false; |
|||
|
|||
extractNames( declarator.id ).forEach( name => { |
|||
const proxy = declarator.proxies.get( name ); |
|||
const isExportedAndReassigned = !es && proxy.exportName && proxy.isReassigned; |
|||
|
|||
if ( isExportedAndReassigned ) { |
|||
// code.overwrite( c, declarator.start, prefix );
|
|||
// c = declarator.end;
|
|||
// empty = false;
|
|||
exportAssignments.push( 'TODO' ); |
|||
} else if ( declarator.activated ) { |
|||
activated = true; |
|||
} |
|||
}); |
|||
|
|||
if ( !treeshake || activated ) { |
|||
if ( shouldSeparate ) code.overwrite( c, declarator.start, `${prefix}${this.kind} ` ); // TODO indentation
|
|||
c = declarator.end; |
|||
empty = false; |
|||
} |
|||
|
|||
if ( exportAssignments.length ) { |
|||
throw new Error( 'TODO' ); |
|||
} |
|||
} |
|||
|
|||
declarator.render( code, es ); |
|||
} |
|||
|
|||
if ( treeshake && empty ) { |
|||
code.remove( this.leadingCommentStart || this.start, this.next || this.end ); |
|||
} else if ( this.end > c ) { |
|||
const hasSemicolon = code.original[ this.end - 1 ] === ';'; |
|||
code.overwrite( c, this.end, hasSemicolon ? ';' : '' ); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,91 @@ |
|||
import Node from '../Node.js'; |
|||
import extractNames from '../utils/extractNames.js'; |
|||
import { UNKNOWN } from '../values.js'; |
|||
|
|||
class DeclaratorProxy { |
|||
constructor ( name, declarator, isTopLevel, init ) { |
|||
this.name = name; |
|||
this.declarator = declarator; |
|||
|
|||
this.activated = false; |
|||
this.isReassigned = false; |
|||
this.exportName = null; |
|||
|
|||
this.duplicates = []; |
|||
this.possibleValues = new Set( init ? [ init ] : null ); |
|||
} |
|||
|
|||
activate () { |
|||
this.activated = true; |
|||
this.declarator.activate(); |
|||
this.duplicates.forEach( dupe => dupe.activate() ); |
|||
} |
|||
|
|||
addReference () { |
|||
/* noop? */ |
|||
} |
|||
|
|||
gatherPossibleValues ( values ) { |
|||
this.possibleValues.forEach( value => values.add( value ) ); |
|||
} |
|||
|
|||
getName ( es ) { |
|||
// TODO desctructuring...
|
|||
if ( es ) return this.name; |
|||
if ( !this.isReassigned || !this.exportName ) return this.name; |
|||
|
|||
return `exports.${this.exportName}`; |
|||
} |
|||
|
|||
toString () { |
|||
return this.name; |
|||
} |
|||
} |
|||
|
|||
export default class VariableDeclarator extends Node { |
|||
activate () { |
|||
if ( this.activated ) return; |
|||
this.activated = true; |
|||
|
|||
this.run( this.findScope() ); |
|||
} |
|||
|
|||
hasEffects ( scope ) { |
|||
return this.init && this.init.hasEffects( scope ); |
|||
} |
|||
|
|||
initialise ( scope ) { |
|||
this.proxies = new Map(); |
|||
|
|||
const lexicalBoundary = scope.findLexicalBoundary(); |
|||
|
|||
const init = this.init ? |
|||
( this.id.type === 'Identifier' ? this.init : UNKNOWN ) : // TODO maybe UNKNOWN is unnecessary
|
|||
null; |
|||
|
|||
extractNames( this.id ).forEach( name => { |
|||
const proxy = new DeclaratorProxy( name, this, lexicalBoundary.isModuleScope, init ); |
|||
|
|||
this.proxies.set( name, proxy ); |
|||
scope.addDeclaration( name, proxy, this.parent.kind === 'var' ); |
|||
}); |
|||
|
|||
super.initialise( scope ); |
|||
} |
|||
|
|||
render ( code, es ) { |
|||
extractNames( this.id ).forEach( name => { |
|||
const declaration = this.proxies.get( name ); |
|||
|
|||
if ( !es && declaration.exportName && declaration.isReassigned ) { |
|||
if ( this.init ) { |
|||
code.overwrite( this.start, this.id.end, declaration.getName( es ) ); |
|||
} else if ( this.module.bundle.treeshake ) { |
|||
code.remove( this.start, this.end ); |
|||
} |
|||
} |
|||
}); |
|||
|
|||
super.render( code, es ); |
|||
} |
|||
} |
@ -0,0 +1,63 @@ |
|||
import ArrayExpression from './ArrayExpression.js'; |
|||
import ArrowFunctionExpression from './ArrowFunctionExpression.js'; |
|||
import AssignmentExpression from './AssignmentExpression.js'; |
|||
import BinaryExpression from './BinaryExpression.js'; |
|||
import BlockStatement from './BlockStatement.js'; |
|||
import CallExpression from './CallExpression.js'; |
|||
import ClassDeclaration from './ClassDeclaration.js'; |
|||
import ClassExpression from './ClassExpression.js'; |
|||
import ConditionalExpression from './ConditionalExpression.js'; |
|||
import ExportAllDeclaration from './ExportAllDeclaration.js'; |
|||
import ExportDefaultDeclaration from './ExportDefaultDeclaration.js'; |
|||
import ExportNamedDeclaration from './ExportNamedDeclaration.js'; |
|||
import ExpressionStatement from './ExpressionStatement.js'; |
|||
import FunctionDeclaration from './FunctionDeclaration.js'; |
|||
import FunctionExpression from './FunctionExpression.js'; |
|||
import Identifier from './Identifier.js'; |
|||
import IfStatement from './IfStatement.js'; |
|||
import ImportDeclaration from './ImportDeclaration.js'; |
|||
import Literal from './Literal.js'; |
|||
import MemberExpression from './MemberExpression.js'; |
|||
import NewExpression from './NewExpression.js'; |
|||
import ObjectExpression from './ObjectExpression.js'; |
|||
import ParenthesizedExpression from './ParenthesizedExpression.js'; |
|||
import ReturnStatement from './ReturnStatement.js'; |
|||
import TemplateLiteral from './TemplateLiteral.js'; |
|||
import ThisExpression from './ThisExpression.js'; |
|||
import UnaryExpression from './UnaryExpression.js'; |
|||
import UpdateExpression from './UpdateExpression.js'; |
|||
import VariableDeclarator from './VariableDeclarator.js'; |
|||
import VariableDeclaration from './VariableDeclaration.js'; |
|||
|
|||
export default { |
|||
ArrayExpression, |
|||
ArrowFunctionExpression, |
|||
AssignmentExpression, |
|||
BinaryExpression, |
|||
BlockStatement, |
|||
CallExpression, |
|||
ClassDeclaration, |
|||
ClassExpression, |
|||
ConditionalExpression, |
|||
ExportAllDeclaration, |
|||
ExportDefaultDeclaration, |
|||
ExportNamedDeclaration, |
|||
ExpressionStatement, |
|||
FunctionDeclaration, |
|||
FunctionExpression, |
|||
Identifier, |
|||
IfStatement, |
|||
ImportDeclaration, |
|||
Literal, |
|||
MemberExpression, |
|||
NewExpression, |
|||
ObjectExpression, |
|||
ParenthesizedExpression, |
|||
ReturnStatement, |
|||
TemplateLiteral, |
|||
ThisExpression, |
|||
UnaryExpression, |
|||
UpdateExpression, |
|||
VariableDeclarator, |
|||
VariableDeclaration |
|||
}; |
@ -0,0 +1,67 @@ |
|||
import flatten from '../../utils/flatten.js'; |
|||
import isReference from '../../utils/isReference.js'; |
|||
import pureFunctions from './pureFunctions.js'; |
|||
import { UNKNOWN } from '../../values.js'; |
|||
|
|||
const currentlyCalling = new Set(); |
|||
|
|||
function fnHasEffects ( fn ) { |
|||
if ( currentlyCalling.has( fn ) ) return false; // prevent infinite loops... TODO there must be a better way
|
|||
currentlyCalling.add( fn ); |
|||
|
|||
// handle body-less arrow functions
|
|||
const scope = fn.body.scope || fn.scope; |
|||
const body = fn.body.body || [ fn.body ]; |
|||
|
|||
for ( const node of body ) { |
|||
if ( node.hasEffects( scope ) ) { |
|||
currentlyCalling.delete( fn ); |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
currentlyCalling.delete( fn ); |
|||
return false; |
|||
} |
|||
|
|||
export default function callHasEffects ( scope, callee ) { |
|||
const values = new Set([ callee ]); |
|||
|
|||
for ( const node of values ) { |
|||
if ( node === UNKNOWN ) return true; // err on side of caution
|
|||
|
|||
if ( /Function/.test( node.type ) ) { |
|||
if ( fnHasEffects( node ) ) return true; |
|||
} |
|||
|
|||
else if ( isReference( node ) ) { |
|||
const flattened = flatten( node ); |
|||
const declaration = scope.findDeclaration( flattened.name ); |
|||
|
|||
if ( declaration.isGlobal ) { |
|||
if ( !pureFunctions[ flattened.keypath ] ) return true; |
|||
} |
|||
|
|||
else if ( declaration.isExternal ) { |
|||
return true; // TODO make this configurable? e.g. `path.[whatever]`
|
|||
} |
|||
|
|||
else { |
|||
if ( node.declaration ) { |
|||
node.declaration.gatherPossibleValues( values ); |
|||
} else { |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
else { |
|||
if ( !node.gatherPossibleValues ) { |
|||
throw new Error( 'TODO' ); |
|||
} |
|||
node.gatherPossibleValues( values ); |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
@ -0,0 +1,28 @@ |
|||
import getLocation from '../../../utils/getLocation.js'; |
|||
import error from '../../../utils/error.js'; |
|||
|
|||
// TODO tidy this up a bit (e.g. they can both use node.module.imports)
|
|||
export default function disallowIllegalReassignment ( scope, node ) { |
|||
if ( node.type === 'MemberExpression' && node.object.type === 'Identifier' ) { |
|||
const declaration = scope.findDeclaration( node.object.name ); |
|||
if ( declaration.isNamespace ) { |
|||
error({ |
|||
message: `Illegal reassignment to import '${node.object.name}'`, |
|||
file: node.module.id, |
|||
pos: node.start, |
|||
loc: getLocation( node.module.code, node.start ) |
|||
}); |
|||
} |
|||
} |
|||
|
|||
else if ( node.type === 'Identifier' ) { |
|||
if ( node.module.imports[ node.name ] && !scope.contains( node.name ) ) { |
|||
error({ |
|||
message: `Illegal reassignment to import '${node.name}'`, |
|||
file: node.module.id, |
|||
pos: node.start, |
|||
loc: getLocation( node.module.code, node.start ) |
|||
}); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,40 @@ |
|||
import { UNKNOWN } from '../../values.js'; |
|||
|
|||
export default function isUsedByBundle ( scope, node ) { |
|||
while ( node.type === 'ParenthesizedExpression' ) node = node.expression; |
|||
|
|||
// const expression = node;
|
|||
while ( node.type === 'MemberExpression' ) node = node.object; |
|||
|
|||
const declaration = scope.findDeclaration( node.name ); |
|||
|
|||
if ( declaration.isParam ) { |
|||
return true; |
|||
|
|||
// TODO if we mutate a parameter, assume the worst
|
|||
// return node !== expression;
|
|||
} |
|||
|
|||
if ( declaration.activated ) return true; |
|||
|
|||
const values = new Set(); |
|||
declaration.gatherPossibleValues( values ); |
|||
for ( const value of values ) { |
|||
if ( value === UNKNOWN ) { |
|||
return true; |
|||
} |
|||
|
|||
if ( value.type === 'Identifier' ) { |
|||
if ( value.declaration.activated ) { |
|||
return true; |
|||
} |
|||
value.declaration.gatherPossibleValues( values ); |
|||
} |
|||
|
|||
else if ( value.gatherPossibleValues ) { |
|||
value.gatherPossibleValues( values ); |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
@ -0,0 +1,40 @@ |
|||
import Scope from './Scope.js'; |
|||
import { UNKNOWN } from '../values'; |
|||
|
|||
class SyntheticGlobalDeclaration { |
|||
constructor ( name ) { |
|||
this.name = name; |
|||
this.isExternal = true; |
|||
this.isGlobal = true; |
|||
this.isReassigned = false; |
|||
|
|||
this.activated = true; |
|||
} |
|||
|
|||
activate () { |
|||
/* noop */ |
|||
} |
|||
|
|||
addReference ( reference ) { |
|||
reference.declaration = this; |
|||
if ( reference.isReassignment ) this.isReassigned = true; |
|||
} |
|||
|
|||
gatherPossibleValues ( values ) { |
|||
values.add( UNKNOWN ); |
|||
} |
|||
|
|||
getName () { |
|||
return this.name; |
|||
} |
|||
} |
|||
|
|||
export default class BundleScope extends Scope { |
|||
findDeclaration ( name ) { |
|||
if ( !this.declarations[ name ] ) { |
|||
this.declarations[ name ] = new SyntheticGlobalDeclaration( name ); |
|||
} |
|||
|
|||
return this.declarations[ name ]; |
|||
} |
|||
} |
@ -0,0 +1,47 @@ |
|||
import { forOwn } from '../../utils/object.js'; |
|||
import Scope from './Scope.js'; |
|||
|
|||
export default class ModuleScope extends Scope { |
|||
constructor ( module ) { |
|||
super({ |
|||
isBlockScope: false, |
|||
isLexicalBoundary: true, |
|||
isModuleScope: true, |
|||
parent: module.bundle.scope |
|||
}); |
|||
|
|||
this.module = module; |
|||
} |
|||
|
|||
deshadow ( names ) { |
|||
names = new Map( names ); |
|||
|
|||
forOwn( this.module.imports, specifier => { |
|||
if ( specifier.module.isExternal ) return; |
|||
|
|||
if ( specifier.name === '*' ) { |
|||
specifier.module.getExports().forEach( name => { |
|||
names.set( name, true ); |
|||
}); |
|||
} else { |
|||
const declaration = specifier.module.traceExport( specifier.name ); |
|||
const name = declaration.getName( true ); |
|||
if ( name !== specifier.name ) names.set( declaration.getName( true ) ); |
|||
} |
|||
}); |
|||
|
|||
super.deshadow( names ); |
|||
} |
|||
|
|||
findDeclaration ( name ) { |
|||
if ( this.declarations[ name ] ) { |
|||
return this.declarations[ name ]; |
|||
} |
|||
|
|||
return this.module.trace( name ) || this.parent.findDeclaration( name ); |
|||
} |
|||
|
|||
findLexicalBoundary () { |
|||
return this; |
|||
} |
|||
} |
@ -0,0 +1,98 @@ |
|||
import { blank, keys } from '../../utils/object.js'; |
|||
import { UNKNOWN } from '../values.js'; |
|||
|
|||
class Parameter { |
|||
constructor ( name ) { |
|||
this.name = name; |
|||
|
|||
this.isParam = true; |
|||
this.activated = true; |
|||
} |
|||
|
|||
activate () { |
|||
// noop
|
|||
} |
|||
|
|||
addReference () { |
|||
// noop?
|
|||
} |
|||
|
|||
gatherPossibleValues ( values ) { |
|||
values.add( UNKNOWN ); // TODO populate this at call time
|
|||
} |
|||
|
|||
getName () { |
|||
return this.name; |
|||
} |
|||
} |
|||
|
|||
export default class Scope { |
|||
constructor ( options ) { |
|||
options = options || {}; |
|||
|
|||
this.parent = options.parent; |
|||
this.isBlockScope = !!options.isBlockScope; |
|||
this.isLexicalBoundary = !!options.isLexicalBoundary; |
|||
this.isModuleScope = !!options.isModuleScope; |
|||
|
|||
this.children = []; |
|||
if ( this.parent ) this.parent.children.push( this ); |
|||
|
|||
this.declarations = blank(); |
|||
|
|||
if ( this.isLexicalBoundary && !this.isModuleScope ) { |
|||
this.declarations.arguments = new Parameter( 'arguments' ); |
|||
} |
|||
} |
|||
|
|||
addDeclaration ( name, declaration, isVar, isParam ) { |
|||
if ( isVar && this.isBlockScope ) { |
|||
this.parent.addDeclaration( name, declaration, isVar, isParam ); |
|||
} else { |
|||
const existingDeclaration = this.declarations[ name ]; |
|||
|
|||
if ( existingDeclaration && existingDeclaration.duplicates ) { |
|||
// TODO warn/throw on duplicates?
|
|||
existingDeclaration.duplicates.push( declaration ); |
|||
} else { |
|||
this.declarations[ name ] = isParam ? new Parameter( name ) : declaration; |
|||
} |
|||
} |
|||
} |
|||
|
|||
contains ( name ) { |
|||
return !!this.declarations[ name ] || |
|||
( this.parent ? this.parent.contains( name ) : false ); |
|||
} |
|||
|
|||
deshadow ( names ) { |
|||
keys( this.declarations ).forEach( key => { |
|||
const declaration = this.declarations[ key ]; |
|||
|
|||
// we can disregard exports.foo etc
|
|||
if ( declaration.exportName && declaration.isReassigned ) return; |
|||
|
|||
const name = declaration.getName( true ); |
|||
let deshadowed = name; |
|||
|
|||
let i = 1; |
|||
|
|||
while ( names.has( deshadowed ) ) { |
|||
deshadowed = `${name}$$${i++}`; |
|||
} |
|||
|
|||
declaration.name = deshadowed; |
|||
}); |
|||
|
|||
this.children.forEach( scope => scope.deshadow( names ) ); |
|||
} |
|||
|
|||
findDeclaration ( name ) { |
|||
return this.declarations[ name ] || |
|||
( this.parent && this.parent.findDeclaration( name ) ); |
|||
} |
|||
|
|||
findLexicalBoundary () { |
|||
return this.isLexicalBoundary ? this : this.parent.findLexicalBoundary(); |
|||
} |
|||
} |
@ -0,0 +1,8 @@ |
|||
// properties are for debugging purposes only
|
|||
export const ARRAY = { ARRAY: true, toString: () => '[[ARRAY]]' }; |
|||
export const BOOLEAN = { BOOLEAN: true, toString: () => '[[BOOLEAN]]' }; |
|||
export const FUNCTION = { FUNCTION: true, toString: () => '[[FUNCTION]]' }; |
|||
export const NUMBER = { NUMBER: true, toString: () => '[[NUMBER]]' }; |
|||
export const OBJECT = { OBJECT: true, toString: () => '[[OBJECT]]' }; |
|||
export const STRING = { STRING: true, toString: () => '[[STRING]]' }; |
|||
export const UNKNOWN = { UNKNOWN: true, toString: () => '[[UNKNOWN]]' }; |
@ -1,119 +0,0 @@ |
|||
import { walk } from 'estree-walker'; |
|||
import modifierNodes, { isModifierNode } from '../ast/modifierNodes.js'; |
|||
import isReference from '../ast/isReference.js'; |
|||
import flatten from '../ast/flatten'; |
|||
import pureFunctions from './pureFunctions.js'; |
|||
import getLocation from './getLocation.js'; |
|||
import error from './error.js'; |
|||
|
|||
function call ( callee, scope, statement, strongDependencies ) { |
|||
while ( callee.type === 'ParenthesizedExpression' ) callee = callee.expression; |
|||
|
|||
if ( callee.type === 'Identifier' ) { |
|||
const declaration = scope.findDeclaration( callee.name ) || |
|||
statement.module.trace( callee.name ); |
|||
|
|||
if ( declaration ) { |
|||
if ( declaration.isNamespace ) { |
|||
error({ |
|||
message: `Cannot call a namespace ('${callee.name}')`, |
|||
file: statement.module.id, |
|||
pos: callee.start, |
|||
loc: getLocation( statement.module.code, callee.start ) |
|||
}); |
|||
} |
|||
|
|||
return declaration.run( strongDependencies ); |
|||
} |
|||
|
|||
return !pureFunctions[ callee.name ]; |
|||
} |
|||
|
|||
if ( /FunctionExpression/.test( callee.type ) ) { |
|||
return run( callee.body, scope, statement, strongDependencies ); |
|||
} |
|||
|
|||
if ( callee.type === 'MemberExpression' ) { |
|||
const flattened = flatten( callee ); |
|||
|
|||
if ( flattened ) { |
|||
// if we're calling e.g. Object.keys(thing), there are no side-effects
|
|||
// TODO make pureFunctions configurable
|
|||
const declaration = scope.findDeclaration( flattened.name ) || statement.module.trace( flattened.name ); |
|||
|
|||
return ( !!declaration || !pureFunctions[ flattened.keypath ] ); |
|||
} |
|||
} |
|||
|
|||
// complex case like `( a ? b : c )()` or foo[bar].baz()`
|
|||
// – err on the side of caution
|
|||
return true; |
|||
} |
|||
|
|||
export default function run ( node, scope, statement, strongDependencies, force ) { |
|||
let hasSideEffect = false; |
|||
|
|||
walk( node, { |
|||
enter ( node, parent ) { |
|||
if ( !force && /Function/.test( node.type ) ) return this.skip(); |
|||
|
|||
if ( node._scope ) scope = node._scope; |
|||
|
|||
if ( isReference( node, parent ) ) { |
|||
const flattened = flatten( node ); |
|||
|
|||
if ( flattened.name === 'arguments' ) { |
|||
hasSideEffect = true; |
|||
} |
|||
|
|||
else if ( !scope.contains( flattened.name ) ) { |
|||
const declaration = statement.module.trace( flattened.name ); |
|||
if ( declaration && !declaration.isExternal ) { |
|||
const module = declaration.module || declaration.statement.module; // TODO is this right?
|
|||
if ( !module.isExternal && !~strongDependencies.indexOf( module ) ) strongDependencies.push( module ); |
|||
} |
|||
} |
|||
} |
|||
|
|||
else if ( node.type === 'DebuggerStatement' ) { |
|||
hasSideEffect = true; |
|||
} |
|||
|
|||
else if ( node.type === 'ThrowStatement' ) { |
|||
// we only care about errors thrown at the top level, otherwise
|
|||
// any function with error checking gets included if called
|
|||
if ( scope.isTopLevel ) hasSideEffect = true; |
|||
} |
|||
|
|||
else if ( node.type === 'CallExpression' || node.type === 'NewExpression' ) { |
|||
if ( call( node.callee, scope, statement, strongDependencies ) ) { |
|||
hasSideEffect = true; |
|||
} |
|||
} |
|||
|
|||
else if ( isModifierNode( node ) ) { |
|||
let subject = node[ modifierNodes[ node.type ] ]; |
|||
while ( subject.type === 'MemberExpression' ) subject = subject.object; |
|||
|
|||
let declaration = scope.findDeclaration( subject.name ); |
|||
|
|||
if ( declaration ) { |
|||
if ( declaration.isParam ) hasSideEffect = true; |
|||
} else if ( !scope.isTopLevel ) { |
|||
hasSideEffect = true; |
|||
} else { |
|||
declaration = statement.module.trace( subject.name ); |
|||
|
|||
if ( !declaration || declaration.isExternal || declaration.isUsed || ( declaration.original && declaration.original.isUsed ) ) { |
|||
hasSideEffect = true; |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
leave ( node ) { |
|||
if ( node._scope ) scope = scope.parent; |
|||
} |
|||
}); |
|||
|
|||
return hasSideEffect; |
|||
} |
@ -0,0 +1,3 @@ |
|||
module.exports = { |
|||
description: 'does not remove duplicated var declarations (#716)' |
|||
}; |
@ -0,0 +1,17 @@ |
|||
define(function () { 'use strict'; |
|||
|
|||
var a = 1; |
|||
var b = 2; |
|||
|
|||
assert.equal( a, 1 ); |
|||
assert.equal( b, 2 ); |
|||
|
|||
var a = 3; |
|||
var b = 4; |
|||
var c = 5; |
|||
|
|||
assert.equal( a, 3 ); |
|||
assert.equal( b, 4 ); |
|||
assert.equal( c, 5 ); |
|||
|
|||
}); |
@ -0,0 +1,15 @@ |
|||
'use strict'; |
|||
|
|||
var a = 1; |
|||
var b = 2; |
|||
|
|||
assert.equal( a, 1 ); |
|||
assert.equal( b, 2 ); |
|||
|
|||
var a = 3; |
|||
var b = 4; |
|||
var c = 5; |
|||
|
|||
assert.equal( a, 3 ); |
|||
assert.equal( b, 4 ); |
|||
assert.equal( c, 5 ); |
@ -0,0 +1,13 @@ |
|||
var a = 1; |
|||
var b = 2; |
|||
|
|||
assert.equal( a, 1 ); |
|||
assert.equal( b, 2 ); |
|||
|
|||
var a = 3; |
|||
var b = 4; |
|||
var c = 5; |
|||
|
|||
assert.equal( a, 3 ); |
|||
assert.equal( b, 4 ); |
|||
assert.equal( c, 5 ); |
@ -0,0 +1,18 @@ |
|||
(function () { |
|||
'use strict'; |
|||
|
|||
var a = 1; |
|||
var b = 2; |
|||
|
|||
assert.equal( a, 1 ); |
|||
assert.equal( b, 2 ); |
|||
|
|||
var a = 3; |
|||
var b = 4; |
|||
var c = 5; |
|||
|
|||
assert.equal( a, 3 ); |
|||
assert.equal( b, 4 ); |
|||
assert.equal( c, 5 ); |
|||
|
|||
}()); |
@ -0,0 +1,21 @@ |
|||
(function (global, factory) { |
|||
typeof exports === 'object' && typeof module !== 'undefined' ? factory() : |
|||
typeof define === 'function' && define.amd ? define(factory) : |
|||
(factory()); |
|||
}(this, (function () { 'use strict'; |
|||
|
|||
var a = 1; |
|||
var b = 2; |
|||
|
|||
assert.equal( a, 1 ); |
|||
assert.equal( b, 2 ); |
|||
|
|||
var a = 3; |
|||
var b = 4; |
|||
var c = 5; |
|||
|
|||
assert.equal( a, 3 ); |
|||
assert.equal( b, 4 ); |
|||
assert.equal( c, 5 ); |
|||
|
|||
}))); |
@ -0,0 +1,10 @@ |
|||
var a = 1, b = 2; |
|||
|
|||
assert.equal( a, 1 ); |
|||
assert.equal( b, 2 ); |
|||
|
|||
var a = 3, b = 4, c = 5; |
|||
|
|||
assert.equal( a, 3 ); |
|||
assert.equal( b, 4 ); |
|||
assert.equal( c, 5 ); |
@ -1,11 +1,11 @@ |
|||
import factory from 'factory'; |
|||
import { bar, foo } from 'baz'; |
|||
import { port } from 'shipping-port'; |
|||
import { forEach, port } from 'shipping-port'; |
|||
import * as containers from 'shipping-port'; |
|||
import alphabet, { a } from 'alphabet'; |
|||
|
|||
factory( null ); |
|||
foo( bar, port ); |
|||
containers.forEach( console.log, console ); |
|||
forEach( console.log, console ); |
|||
console.log( a ); |
|||
console.log( alphabet.length ); |
|||
|
@ -1,6 +1,7 @@ |
|||
import * as foo from 'foo'; |
|||
import { bar } from 'foo'; |
|||
import foo__default from 'foo'; |
|||
import * as foo from 'foo'; |
|||
|
|||
console.log( foo.bar ); |
|||
console.log( bar ); |
|||
|
|||
console.log( foo__default ); |
|||
|
@ -0,0 +1,3 @@ |
|||
module.exports = { |
|||
description: 'includes all declarations referenced by reified namespaces' |
|||
} |
@ -0,0 +1,5 @@ |
|||
define(function () { 'use strict'; |
|||
|
|||
|
|||
|
|||
}); |
@ -0,0 +1,2 @@ |
|||
'use strict'; |
|||
|
@ -0,0 +1,6 @@ |
|||
(function () { |
|||
'use strict'; |
|||
|
|||
|
|||
|
|||
}()); |
@ -0,0 +1,9 @@ |
|||
(function (global, factory) { |
|||
typeof exports === 'object' && typeof module !== 'undefined' ? factory() : |
|||
typeof define === 'function' && define.amd ? define(factory) : |
|||
(factory()); |
|||
}(this, (function () { 'use strict'; |
|||
|
|||
|
|||
|
|||
}))); |
@ -0,0 +1,7 @@ |
|||
import * as unused from './unused.js'; |
|||
|
|||
var indirection = { |
|||
unused: unused |
|||
}; |
|||
|
|||
export { indirection }; |
@ -0,0 +1 @@ |
|||
import { indirection } from './indirection.js'; |
@ -0,0 +1,3 @@ |
|||
function foo () {} |
|||
|
|||
export { foo }; |
@ -1,3 +1,3 @@ |
|||
function a () {} |
|||
|
|||
a(); |
|||
console.log( a() ); |
|||
|
@ -1,3 +1,3 @@ |
|||
import * as foo from './foo'; |
|||
|
|||
foo.bar.quux.a(); |
|||
console.log( foo.bar.quux.a() ); |
|||
|
@ -0,0 +1,3 @@ |
|||
module.exports = { |
|||
description: 'discards a self-calling function with side-effects' |
|||
}; |
@ -0,0 +1,20 @@ |
|||
define(function () { 'use strict'; |
|||
|
|||
function foo ( x ) { |
|||
effect( x ); |
|||
if ( x > 0 ) foo( x - 1 ); |
|||
} |
|||
|
|||
function bar ( x ) { |
|||
effect( x ); |
|||
if ( x > 0 ) baz( x ); |
|||
} |
|||
|
|||
function baz ( x ) { |
|||
bar( x - 1 ); |
|||
} |
|||
|
|||
foo( 10 ); |
|||
bar( 10 ); |
|||
|
|||
}); |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue