UNPKG

13.7 kBJavaScriptView Raw
1import { blank, keys } from './utils/object';
2import getLocation from './utils/getLocation';
3import walk from './ast/walk';
4import Scope from './ast/Scope';
5
6const blockDeclarations = {
7 'const': true,
8 'let': true
9};
10
11const modifierNodes = {
12 AssignmentExpression: 'left',
13 UpdateExpression: 'argument'
14};
15
16function isIife ( node, parent ) {
17 return parent && parent.type === 'CallExpression' && node === parent.callee;
18}
19
20function isFunctionDeclaration ( node, parent ) {
21 // `function foo () {}`
22 if ( node.type === 'FunctionDeclaration' ) return true;
23
24 // `var foo = function () {}` - same thing for present purposes
25 if ( node.type === 'FunctionExpression' && parent.type === 'VariableDeclarator' ) return true;
26}
27
28export default class Statement {
29 constructor ( node, module, start, end ) {
30 this.node = node;
31 this.module = module;
32 this.start = start;
33 this.end = end;
34 this.next = null; // filled in later
35
36 this.scope = new Scope();
37 this.defines = blank();
38 this.modifies = blank();
39 this.dependsOn = blank();
40 this.stronglyDependsOn = blank();
41
42 this.reassigns = blank();
43
44 this.hasSideEffects = false;
45 this.isIncluded = false;
46
47 this.isImportDeclaration = node.type === 'ImportDeclaration';
48 this.isExportDeclaration = /^Export/.test( node.type );
49 this.isReexportDeclaration = this.isExportDeclaration && !!node.source;
50 }
51
52 analyse () {
53 if ( this.isImportDeclaration ) return; // nothing to analyse
54
55 let scope = this.scope;
56
57 walk( this.node, {
58 enter ( node, parent ) {
59 let newScope;
60
61 switch ( node.type ) {
62 case 'FunctionDeclaration':
63 scope.addDeclaration( node, false, false );
64 break;
65
66 case 'BlockStatement':
67 if ( parent && /Function/.test( parent.type ) ) {
68 newScope = new Scope({
69 parent: scope,
70 block: false,
71 params: parent.params
72 });
73
74 // named function expressions - the name is considered
75 // part of the function's scope
76 if ( parent.type === 'FunctionExpression' && parent.id ) {
77 newScope.addDeclaration( parent, false, false );
78 }
79 } else {
80 newScope = new Scope({
81 parent: scope,
82 block: true
83 });
84 }
85
86 break;
87
88 case 'CatchClause':
89 newScope = new Scope({
90 parent: scope,
91 params: [ node.param ],
92 block: true
93 });
94
95 break;
96
97 case 'VariableDeclaration':
98 node.declarations.forEach( declarator => {
99 const isBlockDeclaration = node.type === 'VariableDeclaration' && blockDeclarations[ node.kind ];
100 scope.addDeclaration( declarator, isBlockDeclaration, true );
101 });
102 break;
103
104 case 'ClassDeclaration':
105 scope.addDeclaration( node, false, false );
106 break;
107 }
108
109 if ( newScope ) {
110 Object.defineProperty( node, '_scope', {
111 value: newScope,
112 configurable: true
113 });
114
115 scope = newScope;
116 }
117 },
118 leave ( node ) {
119 if ( node._scope ) {
120 scope = scope.parent;
121 }
122 }
123 });
124
125 // This allows us to track whether we're looking at code that will
126 // be executed immediately (either outside a function, or immediately
127 // inside an IIFE), for the purposes of determining whether dependencies
128 // are strong or weak. It's not bulletproof, since it wouldn't catch...
129 //
130 // var calledImmediately = function () {
131 // doSomethingWith( strongDependency );
132 // }
133 // calledImmediately();
134 //
135 // ...but it's better than nothing
136 let readDepth = 0;
137
138 // This allows us to track whether a modifying statement (i.e. assignment
139 // /update expressions) need to be captured
140 let writeDepth = 0;
141
142 if ( !this.isImportDeclaration ) {
143 walk( this.node, {
144 enter: ( node, parent ) => {
145 if ( isFunctionDeclaration( node, parent ) ) writeDepth += 1;
146 if ( /Function/.test( node.type ) && !isIife( node, parent ) ) readDepth += 1;
147
148 if ( node._scope ) scope = node._scope;
149
150 this.checkForReads( scope, node, parent, !readDepth );
151 this.checkForWrites( scope, node, writeDepth );
152 },
153 leave: ( node, parent ) => {
154 if ( isFunctionDeclaration( node, parent ) ) writeDepth -= 1;
155 if ( /Function/.test( node.type ) && !isIife( node, parent ) ) readDepth -= 1;
156
157 if ( node._scope ) scope = scope.parent;
158 }
159 });
160 }
161
162 keys( scope.declarations ).forEach( name => {
163 this.defines[ name ] = true;
164 });
165 }
166
167 checkForReads ( scope, node, parent, strong ) {
168 if ( node.type === 'Identifier' ) {
169 // disregard the `bar` in `foo.bar` - these appear as Identifier nodes
170 if ( parent.type === 'MemberExpression' && !parent.computed && node !== parent.object ) {
171 return;
172 }
173
174 // disregard the `bar` in { bar: foo }
175 if ( parent.type === 'Property' && node !== parent.value ) {
176 return;
177 }
178
179 // disregard the `bar` in `class Foo { bar () {...} }`
180 if ( parent.type === 'MethodDefinition' ) return;
181
182 // disregard the `bar` in `export { foo as bar }`
183 if ( parent.type === 'ExportSpecifier' && node !== parent.local ) return;
184
185 const definingScope = scope.findDefiningScope( node.name );
186
187 if ( !definingScope || definingScope.depth === 0 ) {
188 this.dependsOn[ node.name ] = true;
189 if ( strong ) this.stronglyDependsOn[ node.name ] = true;
190 }
191 }
192 }
193
194 checkForWrites ( scope, node, writeDepth ) {
195 const addNode = ( node, isAssignment ) => {
196 let depth = 0; // determine whether we're illegally modifying a binding or namespace
197
198 while ( node.type === 'MemberExpression' ) {
199 node = node.object;
200 depth += 1;
201 }
202
203 // disallow assignments/updates to imported bindings and namespaces
204 if ( isAssignment ) {
205 const importSpecifier = this.module.imports[ node.name ];
206
207 if ( importSpecifier && !scope.contains( node.name ) ) {
208 const minDepth = importSpecifier.name === '*' ?
209 2 : // cannot do e.g. `namespace.foo = bar`
210 1; // cannot do e.g. `foo = bar`, but `foo.bar = bar` is fine
211
212 if ( depth < minDepth ) {
213 const err = new Error( `Illegal reassignment to import '${node.name}'` );
214 err.file = this.module.id;
215 err.loc = getLocation( this.module.magicString.toString(), node.start );
216 throw err;
217 }
218 }
219
220 // special case = `export default foo; foo += 1;` - we'll
221 // need to assign a new variable so that the exported
222 // value is not updated by the second statement
223 if ( this.module.exports.default && depth === 0 && this.module.exports.default.identifier === node.name ) {
224 // but only if this is a) inside a function body or
225 // b) after the export declaration
226 if ( !!scope.parent || node.start > this.module.exports.default.statement.node.start ) {
227 this.module.exports.default.isModified = true;
228 }
229 }
230
231 // we track updates/reassignments to variables, to know whether we
232 // need to rewrite it later from `foo` to `exports.foo` to keep
233 // bindings live
234 if (
235 depth === 0 &&
236 writeDepth > 0 &&
237 !scope.contains( node.name )
238 ) {
239 this.reassigns[ node.name ] = true;
240 }
241 }
242
243 // we only care about writes that happen a) at the top level,
244 // or b) inside a function that could be immediately invoked.
245 // Writes inside named functions are only relevant if the
246 // function is called, in which case we don't need to do
247 // anything (but we still need to call checkForWrites to
248 // catch illegal reassignments to imported bindings)
249 if ( writeDepth === 0 && node.type === 'Identifier' ) {
250 this.modifies[ node.name ] = true;
251 }
252 };
253
254 if ( node.type === 'AssignmentExpression' ) {
255 addNode( node.left, true );
256 }
257
258 else if ( node.type === 'UpdateExpression' ) {
259 addNode( node.argument, true );
260 }
261
262 else if ( node.type === 'CallExpression' ) {
263 node.arguments.forEach( arg => addNode( arg, false ) );
264
265 // `foo.bar()` is assumed to mutate foo
266 if ( node.callee.type === 'MemberExpression' ) {
267 addNode( node.callee );
268 }
269 }
270 }
271
272 mark () {
273 if ( this.isIncluded ) return; // prevent infinite loops
274 this.isIncluded = true;
275
276 // `export { name } from './other'` is a special case
277 if ( this.isReexportDeclaration ) {
278 const id = this.module.resolvedIds[ this.node.source.value ];
279 const otherModule = this.module.bundle.moduleById[ id ];
280
281 if ( this.node.specifiers ) {
282 this.node.specifiers.forEach( specifier => {
283 const reexport = this.module.reexports[ specifier.exported.name ];
284
285 reexport.isUsed = true;
286 reexport.module = otherModule; // TODO still necessary?
287
288 if ( !otherModule.isExternal ) otherModule.markExport( specifier.local.name, specifier.exported.name, this.module );
289 });
290 } else {
291 otherModule.needsAll = true;
292
293 otherModule.getExports().forEach( name => {
294 if ( name !== 'default' ) otherModule.markExport( name, name, this.module );
295 });
296 }
297
298 return;
299 }
300
301 Object.keys( this.dependsOn ).forEach( name => {
302 if ( this.defines[ name ] ) return; // TODO maybe exclude from `this.dependsOn` in the first place?
303 this.module.mark( name );
304 });
305 }
306
307 markSideEffect () {
308 const statement = this;
309
310 walk( this.node, {
311 enter ( node, parent ) {
312 if ( /Function/.test( node.type ) && !isIife( node, parent ) ) return this.skip();
313
314 // If this is a top-level call expression, or an assignment to a global,
315 // this statement will need to be marked
316 if ( node.type === 'CallExpression' ) {
317 statement.mark();
318 }
319
320 else if ( node.type in modifierNodes ) {
321 let subject = node[ modifierNodes[ node.type ] ];
322 while ( subject.type === 'MemberExpression' ) subject = subject.object;
323
324 const bundle = statement.module.bundle;
325 const canonicalName = bundle.trace( statement.module, subject.name );
326
327 if ( bundle.assumedGlobals[ canonicalName ] ) statement.mark();
328 }
329 }
330 });
331 }
332
333 replaceIdentifiers ( magicString, names, bundleExports ) {
334 const replacementStack = [ names ];
335 const nameList = keys( names );
336
337 let deshadowList = [];
338 nameList.forEach( name => {
339 const replacement = names[ name ];
340 deshadowList.push( replacement.split( '.' )[0] );
341 });
342
343 let topLevel = true;
344 let depth = 0;
345
346 walk( this.node, {
347 enter ( node, parent ) {
348 if ( node._skip ) return this.skip();
349
350 if ( /^Function/.test( node.type ) ) depth += 1;
351
352 // `this` is undefined at the top level of ES6 modules
353 if ( node.type === 'ThisExpression' && depth === 0 ) {
354 magicString.overwrite( node.start, node.end, 'undefined', true );
355 }
356
357 // special case - variable declarations that need to be rewritten
358 // as bundle exports
359 if ( topLevel ) {
360 if ( node.type === 'VariableDeclaration' ) {
361 // if this contains a single declarator, and it's one that
362 // needs to be rewritten, we replace the whole lot
363 const name = node.declarations[0].id.name;
364 if ( node.declarations.length === 1 && bundleExports[ name ] ) {
365 magicString.overwrite( node.start, node.declarations[0].id.end, bundleExports[ name ], true );
366 node.declarations[0].id._skip = true;
367 }
368
369 // otherwise, we insert the `exports.foo = foo` after the declaration
370 else {
371 const exportInitialisers = node.declarations
372 .map( declarator => declarator.id.name )
373 .filter( name => !!bundleExports[ name ] )
374 .map( name => `\n${bundleExports[name]} = ${name};` )
375 .join( '' );
376
377 if ( exportInitialisers ) {
378 // TODO clean this up
379 try {
380 magicString.insert( node.end, exportInitialisers );
381 } catch ( err ) {
382 magicString.append( exportInitialisers );
383 }
384 }
385 }
386 }
387 }
388
389 const scope = node._scope;
390
391 if ( scope ) {
392 topLevel = false;
393
394 let newNames = blank();
395 let hasReplacements;
396
397 keys( names ).forEach( name => {
398 if ( !scope.declarations[ name ] ) {
399 newNames[ name ] = names[ name ];
400 hasReplacements = true;
401 }
402 });
403
404 deshadowList.forEach( name => {
405 if ( scope.declarations[ name ] ) {
406 newNames[ name ] = name + '$$'; // TODO better mechanism
407 hasReplacements = true;
408 }
409 });
410
411 if ( !hasReplacements && depth > 0 ) {
412 return this.skip();
413 }
414
415 names = newNames;
416 replacementStack.push( newNames );
417 }
418
419 if ( node.type !== 'Identifier' ) return;
420
421 // if there's no replacement, or it's the same, there's nothing more to do
422 const name = names[ node.name ];
423 if ( !name || name === node.name ) return;
424
425 // shorthand properties (`obj = { foo }`) need to be expanded
426 if ( parent.type === 'Property' && parent.shorthand ) {
427 magicString.insert( node.end, `: ${name}` );
428 parent.key._skip = true;
429 parent.value._skip = true; // redundant, but defensive
430 return;
431 }
432
433 // property names etc can be disregarded
434 if ( parent.type === 'MemberExpression' && !parent.computed && node !== parent.object ) return;
435 if ( parent.type === 'Property' && node !== parent.value ) return;
436 if ( parent.type === 'MethodDefinition' && node === parent.key ) return;
437 if ( parent.type === 'FunctionExpression' ) return;
438 if ( /Function/.test( parent.type ) && ~parent.params.indexOf( node ) ) return;
439 // TODO others...?
440
441 // all other identifiers should be overwritten
442 magicString.overwrite( node.start, node.end, name, true );
443 },
444
445 leave ( node ) {
446 if ( /^Function/.test( node.type ) ) depth -= 1;
447
448 if ( node._scope ) {
449 replacementStack.pop();
450 names = replacementStack[ replacementStack.length - 1 ];
451 }
452 }
453 });
454
455 return magicString;
456 }
457
458 source () {
459 return this.module.source.slice( this.start, this.end );
460 }
461
462 toString () {
463 return this.module.magicString.slice( this.start, this.end );
464 }
465}