UNPKG

7.87 kBJavaScriptView Raw
1import { has, 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 ) {
12 this.node = node;
13 this.module = module;
14 this.magicString = magicString;
15
16 this.scope = new Scope();
17 this.defines = {};
18 this.modifies = {};
19 this.dependsOn = {};
20
21 this.isIncluded = false;
22
23 this.leadingComments = [];
24 this.trailingComment = null;
25 this.margin = [ 0, 0 ];
26
27 // some facts about this statement...
28 this.isImportDeclaration = node.type === 'ImportDeclaration';
29 this.isExportDeclaration = /^Export/.test( node.type );
30 }
31
32 analyse () {
33 if ( this.isImportDeclaration ) return; // nothing to analyse
34
35 const statement = this; // TODO use arrow functions instead
36 const magicString = this.magicString;
37
38 let scope = this.scope;
39
40 function addToScope ( declarator ) {
41 var name = declarator.id.name;
42 scope.add( name, false );
43
44 if ( !scope.parent ) {
45 statement.defines[ name ] = true;
46 }
47 }
48
49 function addToBlockScope ( declarator ) {
50 var name = declarator.id.name;
51 scope.add( name, true );
52
53 if ( !scope.parent ) {
54 statement.defines[ name ] = true;
55 }
56 }
57
58 walk( this.node, {
59 enter ( node ) {
60 let newScope;
61
62 magicString.addSourcemapLocation( node.start );
63
64 switch ( node.type ) {
65 case 'FunctionExpression':
66 case 'FunctionDeclaration':
67 case 'ArrowFunctionExpression':
68 let names = node.params.map( getName );
69
70 if ( node.type === 'FunctionDeclaration' ) {
71 addToScope( node );
72 } else if ( node.type === 'FunctionExpression' && node.id ) {
73 names.push( node.id.name );
74 }
75
76 newScope = new Scope({
77 parent: scope,
78 params: names, // TODO rest params?
79 block: false
80 });
81
82 break;
83
84 case 'BlockStatement':
85 newScope = new Scope({
86 parent: scope,
87 block: true
88 });
89
90 break;
91
92 case 'CatchClause':
93 newScope = new Scope({
94 parent: scope,
95 params: [ node.param.name ],
96 block: true
97 });
98
99 break;
100
101 case 'VariableDeclaration':
102 node.declarations.forEach( node.kind === 'let' ? addToBlockScope : addToScope ); // TODO const?
103 break;
104
105 case 'ClassDeclaration':
106 addToScope( node );
107 break;
108 }
109
110 if ( newScope ) {
111 Object.defineProperty( node, '_scope', { value: newScope });
112 scope = newScope;
113 }
114 },
115 leave ( node ) {
116 if ( node._scope ) {
117 scope = scope.parent;
118 }
119 }
120 });
121
122 if ( !this.isImportDeclaration ) {
123 walk( this.node, {
124 enter: ( node, parent ) => {
125 if ( node._scope ) scope = node._scope;
126
127 this.checkForReads( scope, node, parent );
128 this.checkForWrites( scope, node );
129 },
130 leave: ( node ) => {
131 if ( node._scope ) scope = scope.parent;
132 }
133 });
134 }
135 }
136
137 checkForReads ( scope, node, parent ) {
138 if ( node.type === 'Identifier' ) {
139 // disregard the `bar` in `foo.bar` - these appear as Identifier nodes
140 if ( parent.type === 'MemberExpression' && node !== parent.object ) {
141 return;
142 }
143
144 // disregard the `bar` in { bar: foo }
145 if ( parent.type === 'Property' && node !== parent.value ) {
146 return;
147 }
148
149 const definingScope = scope.findDefiningScope( node.name );
150
151 if ( ( !definingScope || definingScope.depth === 0 ) && !this.defines[ node.name ] ) {
152 this.dependsOn[ node.name ] = true;
153 }
154 }
155 }
156
157 checkForWrites ( scope, node ) {
158 const addNode = ( node, disallowImportReassignments ) => {
159 let depth = 0; // determine whether we're illegally modifying a binding or namespace
160
161 while ( node.type === 'MemberExpression' ) {
162 node = node.object;
163 depth += 1;
164 }
165
166 // disallow assignments/updates to imported bindings and namespaces
167 if ( disallowImportReassignments ) {
168 const importSpecifier = this.module.imports[ node.name ];
169
170 if ( importSpecifier && !scope.contains( node.name ) ) {
171 const minDepth = importSpecifier.name === '*' ?
172 2 : // cannot do e.g. `namespace.foo = bar`
173 1; // cannot do e.g. `foo = bar`, but `foo.bar = bar` is fine
174
175 if ( depth < minDepth ) {
176 const err = new Error( `Illegal reassignment to import '${node.name}'` );
177 err.file = this.module.path;
178 err.loc = getLocation( this.module.magicString.toString(), node.start );
179 throw err;
180 }
181 }
182 }
183
184 if ( node.type !== 'Identifier' ) {
185 return;
186 }
187
188 this.modifies[ node.name ] = true;
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 }
203
204 expand () {
205 if ( this.isIncluded ) return emptyArrayPromise;
206 this.isIncluded = true;
207
208 let result = [];
209
210 // We have a statement, and it hasn't been included yet. First, include
211 // the statements it depends on
212 const dependencies = Object.keys( this.dependsOn );
213
214 return sequence( dependencies, name => {
215 return this.module.define( name ).then( definition => {
216 result.push.apply( result, definition );
217 });
218 })
219
220 // then include the statement itself
221 .then( () => {
222 result.push( this );
223 })
224
225 // then include any statements that could modify the
226 // thing(s) this statement defines
227 .then( () => {
228 return sequence( keys( this.defines ), name => {
229 const modifications = has( this.module.modifications, name ) && this.module.modifications[ name ];
230
231 if ( modifications ) {
232 return sequence( modifications, statement => {
233 if ( !statement.isIncluded ) {
234 return statement.expand()
235 .then( statements => {
236 result.push.apply( result, statements );
237 });
238 }
239 });
240 }
241 });
242 })
243
244 // the `result` is an array of statements needed to define `name`
245 .then( () => {
246 return result;
247 });
248 }
249
250 replaceIdentifiers ( names ) {
251 const magicString = this.magicString.clone().trim();
252 const replacementStack = [ names ];
253 const nameList = keys( names );
254
255 let deshadowList = [];
256 nameList.forEach( name => {
257 const replacement = names[ name ];
258 deshadowList.push( replacement.split( '.' )[0] );
259 });
260
261 if ( nameList.length > 0 ) {
262 walk( this.node, {
263 enter ( node, parent ) {
264 const scope = node._scope;
265
266 if ( scope ) {
267 let newNames = {};
268 let hasReplacements;
269
270 keys( names ).forEach( key => {
271 if ( !~scope.names.indexOf( key ) ) {
272 newNames[ key ] = names[ key ];
273 hasReplacements = true;
274 }
275 });
276
277 deshadowList.forEach( name => {
278 if ( ~scope.names.indexOf( name ) ) {
279 newNames[ name ] = name + '$$'; // TODO better mechanism
280 hasReplacements = true;
281 }
282 });
283
284 if ( !hasReplacements ) {
285 return this.skip();
286 }
287
288 names = newNames;
289 replacementStack.push( newNames );
290 }
291
292 // We want to rewrite identifiers (that aren't property names)
293 if ( node.type !== 'Identifier' ) return;
294 if ( parent.type === 'MemberExpression' && !parent.computed && node !== parent.object ) return;
295 if ( parent.type === 'Property' && node !== parent.value ) return;
296 // TODO others...?
297
298 const name = has( names, node.name ) && names[ node.name ];
299
300 if ( name && name !== node.name ) {
301 magicString.overwrite( node.start, node.end, name );
302 }
303 },
304
305 leave ( node ) {
306 if ( node._scope ) {
307 replacementStack.pop();
308 names = replacementStack[ replacementStack.length - 1 ];
309 }
310 }
311 });
312 }
313
314 return magicString;
315 }
316}