UNPKG

9.69 kBJavaScriptView Raw
1import { blank, keys } from './utils/object';
2import { sequence } from './utils/promise';
3import { getName } from './utils/map-helpers';
4import getLocation from './utils/getLocation';
5import walk from './ast/walk';
6import Scope from './ast/Scope';
7
8const emptyArrayPromise = Promise.resolve([]);
9
10export default class Statement {
11 constructor ( node, magicString, module, index ) {
12 this.node = node;
13 this.module = module;
14 this.magicString = magicString;
15 this.index = index;
16
17 this.scope = new Scope();
18 this.defines = blank();
19 this.modifies = blank();
20 this.dependsOn = blank();
21
22 this.isIncluded = false;
23
24 this.leadingComments = [];
25 this.trailingComment = null;
26 this.margin = [ 0, 0 ];
27
28 // some facts about this statement...
29 this.isImportDeclaration = node.type === 'ImportDeclaration';
30 this.isExportDeclaration = /^Export/.test( node.type );
31 }
32
33 analyse () {
34 if ( this.isImportDeclaration ) return; // nothing to analyse
35
36 const statement = this; // TODO use arrow functions instead
37 const magicString = this.magicString;
38
39 let scope = this.scope;
40
41 walk( this.node, {
42 enter ( node ) {
43 let newScope;
44
45 magicString.addSourcemapLocation( node.start );
46
47 switch ( node.type ) {
48 case 'FunctionExpression':
49 case 'FunctionDeclaration':
50 case 'ArrowFunctionExpression':
51 if ( node.type === 'FunctionDeclaration' ) {
52 scope.addDeclaration( node.id.name, node );
53 }
54
55 newScope = new Scope({
56 parent: scope,
57 params: node.params, // TODO rest params?
58 block: false
59 });
60
61 // named function expressions - the name is considered
62 // part of the function's scope
63 if ( node.type === 'FunctionExpression' && node.id ) {
64 newScope.addDeclaration( node.id.name, node );
65 }
66
67 break;
68
69 case 'BlockStatement':
70 newScope = new Scope({
71 parent: scope,
72 block: true
73 });
74
75 break;
76
77 case 'CatchClause':
78 newScope = new Scope({
79 parent: scope,
80 params: [ node.param ],
81 block: true
82 });
83
84 break;
85
86 case 'VariableDeclaration':
87 node.declarations.forEach( declarator => {
88 scope.addDeclaration( declarator.id.name, node );
89 });
90 break;
91
92 case 'ClassDeclaration':
93 scope.addDeclaration( node.id.name, node );
94 break;
95 }
96
97 if ( newScope ) {
98 Object.defineProperty( node, '_scope', { value: newScope });
99 scope = newScope;
100 }
101 },
102 leave ( node ) {
103 if ( node._scope ) {
104 scope = scope.parent;
105 }
106 }
107 });
108
109 if ( !this.isImportDeclaration ) {
110 walk( this.node, {
111 enter: ( node, parent ) => {
112 if ( node._scope ) scope = node._scope;
113
114 this.checkForReads( scope, node, parent );
115 this.checkForWrites( scope, node );
116 },
117 leave: ( node ) => {
118 if ( node._scope ) scope = scope.parent;
119 }
120 });
121 }
122
123 keys( scope.declarations ).forEach( name => {
124 statement.defines[ name ] = true;
125 });
126 }
127
128 checkForReads ( scope, node, parent ) {
129 if ( node.type === 'Identifier' ) {
130 // disregard the `bar` in `foo.bar` - these appear as Identifier nodes
131 if ( parent.type === 'MemberExpression' && node !== parent.object ) {
132 return;
133 }
134
135 // disregard the `bar` in { bar: foo }
136 if ( parent.type === 'Property' && node !== parent.value ) {
137 return;
138 }
139
140 const definingScope = scope.findDefiningScope( node.name );
141
142 if ( ( !definingScope || definingScope.depth === 0 ) && !this.defines[ node.name ] ) {
143 this.dependsOn[ node.name ] = true;
144 }
145 }
146 }
147
148 checkForWrites ( scope, node ) {
149 const addNode = ( node, disallowImportReassignments ) => {
150 let depth = 0; // determine whether we're illegally modifying a binding or namespace
151
152 while ( node.type === 'MemberExpression' ) {
153 node = node.object;
154 depth += 1;
155 }
156
157 // disallow assignments/updates to imported bindings and namespaces
158 if ( disallowImportReassignments ) {
159 const importSpecifier = this.module.imports[ node.name ];
160
161 if ( importSpecifier && !scope.contains( node.name ) ) {
162 const minDepth = importSpecifier.name === '*' ?
163 2 : // cannot do e.g. `namespace.foo = bar`
164 1; // cannot do e.g. `foo = bar`, but `foo.bar = bar` is fine
165
166 if ( depth < minDepth ) {
167 const err = new Error( `Illegal reassignment to import '${node.name}'` );
168 err.file = this.module.path;
169 err.loc = getLocation( this.module.magicString.toString(), node.start );
170 throw err;
171 }
172 }
173 }
174
175 if ( node.type === 'Identifier' ) {
176 this.modifies[ node.name ] = true;
177 }
178
179 // special case = `export default foo; foo += 1;` - we'll
180 // need to assign a new variable so that the exported
181 // value is not updated by the second statement
182 if ( this.module.exports.default && this.module.exports.default.identifier === node.name ) {
183 // but only if this is a) inside a function body or
184 // b) after the export declaration
185 if ( !!scope.parent || node.start > this.module.exports.default.statement.node.start ) {
186 this.module.exports.default.isModified = true;
187 }
188 }
189 };
190
191 if ( node.type === 'AssignmentExpression' ) {
192 addNode( node.left, true );
193 }
194
195 else if ( node.type === 'UpdateExpression' ) {
196 addNode( node.argument, true );
197 }
198
199 else if ( node.type === 'CallExpression' ) {
200 node.arguments.forEach( arg => addNode( arg, false ) );
201
202 // `foo.bar()` is assumed to mutate foo
203 if ( node.callee.type === 'MemberExpression' ) {
204 addNode( node.callee );
205 }
206 }
207 }
208
209 expand () {
210 if ( this.isIncluded ) return emptyArrayPromise; // TODO can this happen?
211 this.isIncluded = true;
212
213 let result = [];
214
215 // We have a statement, and it hasn't been included yet. First, include
216 // the statements it depends on
217 const dependencies = Object.keys( this.dependsOn );
218
219 return sequence( dependencies, name => {
220 return this.module.define( name ).then( definition => {
221 result.push.apply( result, definition );
222 });
223 })
224
225 // then include the statement itself
226 .then( () => {
227 result.push( this );
228 })
229
230 // then include any statements that could modify the
231 // thing(s) this statement defines
232 .then( () => {
233 return sequence( keys( this.defines ), name => {
234 const modifications = this.module.modifications[ name ];
235
236 if ( modifications ) {
237 return sequence( modifications, statement => {
238 if ( !statement.isIncluded ) {
239 return statement.expand()
240 .then( statements => {
241 result.push.apply( result, statements );
242 });
243 }
244 });
245 }
246 });
247 })
248
249 // the `result` is an array of all statements that need
250 // to be included if this one is
251 .then( () => {
252 return result;
253 });
254 }
255
256 replaceIdentifiers ( names, bundleExports ) {
257 const module = this.module;
258
259 const magicString = this.magicString.clone();
260 const replacementStack = [ names ];
261 const nameList = keys( names );
262
263 let deshadowList = [];
264 nameList.forEach( name => {
265 const replacement = names[ name ];
266 deshadowList.push( replacement.split( '.' )[0] );
267 });
268
269 if ( nameList.length > 0 || keys( bundleExports ).length ) {
270 let topLevel = true;
271
272 walk( this.node, {
273 enter ( node, parent ) {
274 if ( node._skip ) return this.skip();
275
276 // special case - variable declarations that need to be rewritten
277 // as bundle exports
278 if ( topLevel ) {
279 if ( node.type === 'VariableDeclaration' ) {
280 // if this contains a single declarator, and it's one that
281 // needs to be rewritten, we replace the whole lot
282 const name = node.declarations[0].id.name;
283 if ( node.declarations.length === 1 && bundleExports[ name ] ) {
284 magicString.overwrite( node.start, node.declarations[0].id.end, bundleExports[ name ] );
285 node.declarations[0].id._skip = true;
286 }
287
288 // otherwise, we insert the `exports.foo = foo` after the declaration
289 else {
290 const exportInitialisers = node.declarations
291 .map( declarator => declarator.id.name )
292 .filter( name => !!bundleExports[ name ] )
293 .map( name => `\n${bundleExports[name]} = ${name};` )
294 .join( '' );
295
296 // TODO clean this up
297 try {
298 magicString.insert( node.end, exportInitialisers );
299 } catch ( err ) {
300 magicString.append( exportInitialisers );
301 }
302 }
303 }
304 }
305
306 const scope = node._scope;
307
308 if ( scope ) {
309 topLevel = false;
310
311 let newNames = blank();
312 let hasReplacements;
313
314 keys( names ).forEach( key => {
315 if ( !scope.declarations[ key ] ) {
316 newNames[ key ] = names[ key ];
317 hasReplacements = true;
318 }
319 });
320
321 deshadowList.forEach( name => {
322 if ( ~scope.declarations[ name ] ) {
323 newNames[ name ] = name + '$$'; // TODO better mechanism
324 hasReplacements = true;
325 }
326 });
327
328 if ( !hasReplacements ) {
329 return this.skip();
330 }
331
332 names = newNames;
333 replacementStack.push( newNames );
334 }
335
336 // We want to rewrite identifiers (that aren't property names etc)
337 if ( node.type !== 'Identifier' ) return;
338 if ( parent.type === 'MemberExpression' && !parent.computed && node !== parent.object ) return;
339 if ( parent.type === 'Property' && node !== parent.value ) return;
340 // TODO others...?
341
342 const name = names[ node.name ];
343
344 if ( name && name !== node.name ) {
345 magicString.overwrite( node.start, node.end, name );
346 }
347 },
348
349 leave ( node ) {
350 if ( node._scope ) {
351 replacementStack.pop();
352 names = replacementStack[ replacementStack.length - 1 ];
353 }
354 }
355 });
356 }
357
358 return magicString;
359 }
360}