Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
81146 views
1
/**
2
* Copyright 2013 Facebook, Inc.
3
*
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
* you may not use this file except in compliance with the License.
6
* You may obtain a copy of the License at
7
*
8
* http://www.apache.org/licenses/LICENSE-2.0
9
*
10
* Unless required by applicable law or agreed to in writing, software
11
* distributed under the License is distributed on an "AS IS" BASIS,
12
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
* See the License for the specific language governing permissions and
14
* limitations under the License.
15
*/
16
17
18
/*jslint node: true*/
19
var Syntax = require('esprima-fb').Syntax;
20
var leadingIndentRegexp = /(^|\n)( {2}|\t)/g;
21
var nonWhiteRegexp = /(\S)/g;
22
23
/**
24
* A `state` object represents the state of the parser. It has "local" and
25
* "global" parts. Global contains parser position, source, etc. Local contains
26
* scope based properties like current class name. State should contain all the
27
* info required for transformation. It's the only mandatory object that is
28
* being passed to every function in transform chain.
29
*
30
* @param {string} source
31
* @param {object} transformOptions
32
* @return {object}
33
*/
34
function createState(source, rootNode, transformOptions) {
35
return {
36
/**
37
* A tree representing the current local scope (and its lexical scope chain)
38
* Useful for tracking identifiers from parent scopes, etc.
39
* @type {Object}
40
*/
41
localScope: {
42
parentNode: rootNode,
43
parentScope: null,
44
identifiers: {},
45
tempVarIndex: 0
46
},
47
/**
48
* The name (and, if applicable, expression) of the super class
49
* @type {Object}
50
*/
51
superClass: null,
52
/**
53
* The namespace to use when munging identifiers
54
* @type {String}
55
*/
56
mungeNamespace: '',
57
/**
58
* Ref to the node for the current MethodDefinition
59
* @type {Object}
60
*/
61
methodNode: null,
62
/**
63
* Ref to the node for the FunctionExpression of the enclosing
64
* MethodDefinition
65
* @type {Object}
66
*/
67
methodFuncNode: null,
68
/**
69
* Name of the enclosing class
70
* @type {String}
71
*/
72
className: null,
73
/**
74
* Whether we're currently within a `strict` scope
75
* @type {Bool}
76
*/
77
scopeIsStrict: null,
78
/**
79
* Indentation offset
80
* @type {Number}
81
*/
82
indentBy: 0,
83
/**
84
* Global state (not affected by updateState)
85
* @type {Object}
86
*/
87
g: {
88
/**
89
* A set of general options that transformations can consider while doing
90
* a transformation:
91
*
92
* - minify
93
* Specifies that transformation steps should do their best to minify
94
* the output source when possible. This is useful for places where
95
* minification optimizations are possible with higher-level context
96
* info than what jsxmin can provide.
97
*
98
* For example, the ES6 class transform will minify munged private
99
* variables if this flag is set.
100
*/
101
opts: transformOptions,
102
/**
103
* Current position in the source code
104
* @type {Number}
105
*/
106
position: 0,
107
/**
108
* Auxiliary data to be returned by transforms
109
* @type {Object}
110
*/
111
extra: {},
112
/**
113
* Buffer containing the result
114
* @type {String}
115
*/
116
buffer: '',
117
/**
118
* Source that is being transformed
119
* @type {String}
120
*/
121
source: source,
122
123
/**
124
* Cached parsed docblock (see getDocblock)
125
* @type {object}
126
*/
127
docblock: null,
128
129
/**
130
* Whether the thing was used
131
* @type {Boolean}
132
*/
133
tagNamespaceUsed: false,
134
135
/**
136
* If using bolt xjs transformation
137
* @type {Boolean}
138
*/
139
isBolt: undefined,
140
141
/**
142
* Whether to record source map (expensive) or not
143
* @type {SourceMapGenerator|null}
144
*/
145
sourceMap: null,
146
147
/**
148
* Filename of the file being processed. Will be returned as a source
149
* attribute in the source map
150
*/
151
sourceMapFilename: 'source.js',
152
153
/**
154
* Only when source map is used: last line in the source for which
155
* source map was generated
156
* @type {Number}
157
*/
158
sourceLine: 1,
159
160
/**
161
* Only when source map is used: last line in the buffer for which
162
* source map was generated
163
* @type {Number}
164
*/
165
bufferLine: 1,
166
167
/**
168
* The top-level Program AST for the original file.
169
*/
170
originalProgramAST: null,
171
172
sourceColumn: 0,
173
bufferColumn: 0
174
}
175
};
176
}
177
178
/**
179
* Updates a copy of a given state with "update" and returns an updated state.
180
*
181
* @param {object} state
182
* @param {object} update
183
* @return {object}
184
*/
185
function updateState(state, update) {
186
var ret = Object.create(state);
187
Object.keys(update).forEach(function(updatedKey) {
188
ret[updatedKey] = update[updatedKey];
189
});
190
return ret;
191
}
192
193
/**
194
* Given a state fill the resulting buffer from the original source up to
195
* the end
196
*
197
* @param {number} end
198
* @param {object} state
199
* @param {?function} contentTransformer Optional callback to transform newly
200
* added content.
201
*/
202
function catchup(end, state, contentTransformer) {
203
if (end < state.g.position) {
204
// cannot move backwards
205
return;
206
}
207
var source = state.g.source.substring(state.g.position, end);
208
var transformed = updateIndent(source, state);
209
if (state.g.sourceMap && transformed) {
210
// record where we are
211
state.g.sourceMap.addMapping({
212
generated: { line: state.g.bufferLine, column: state.g.bufferColumn },
213
original: { line: state.g.sourceLine, column: state.g.sourceColumn },
214
source: state.g.sourceMapFilename
215
});
216
217
// record line breaks in transformed source
218
var sourceLines = source.split('\n');
219
var transformedLines = transformed.split('\n');
220
// Add line break mappings between last known mapping and the end of the
221
// added piece. So for the code piece
222
// (foo, bar);
223
// > var x = 2;
224
// > var b = 3;
225
// var c =
226
// only add lines marked with ">": 2, 3.
227
for (var i = 1; i < sourceLines.length - 1; i++) {
228
state.g.sourceMap.addMapping({
229
generated: { line: state.g.bufferLine, column: 0 },
230
original: { line: state.g.sourceLine, column: 0 },
231
source: state.g.sourceMapFilename
232
});
233
state.g.sourceLine++;
234
state.g.bufferLine++;
235
}
236
// offset for the last piece
237
if (sourceLines.length > 1) {
238
state.g.sourceLine++;
239
state.g.bufferLine++;
240
state.g.sourceColumn = 0;
241
state.g.bufferColumn = 0;
242
}
243
state.g.sourceColumn += sourceLines[sourceLines.length - 1].length;
244
state.g.bufferColumn +=
245
transformedLines[transformedLines.length - 1].length;
246
}
247
state.g.buffer +=
248
contentTransformer ? contentTransformer(transformed) : transformed;
249
state.g.position = end;
250
}
251
252
/**
253
* Returns original source for an AST node.
254
* @param {object} node
255
* @param {object} state
256
* @return {string}
257
*/
258
function getNodeSourceText(node, state) {
259
return state.g.source.substring(node.range[0], node.range[1]);
260
}
261
262
function replaceNonWhite(value) {
263
return value.replace(nonWhiteRegexp, ' ');
264
}
265
266
/**
267
* Removes all non-whitespace characters
268
*/
269
function stripNonWhite(value) {
270
return value.replace(nonWhiteRegexp, '');
271
}
272
273
/**
274
* Catches up as `catchup` but replaces non-whitespace chars with spaces.
275
*/
276
function catchupWhiteOut(end, state) {
277
catchup(end, state, replaceNonWhite);
278
}
279
280
/**
281
* Catches up as `catchup` but removes all non-whitespace characters.
282
*/
283
function catchupWhiteSpace(end, state) {
284
catchup(end, state, stripNonWhite);
285
}
286
287
/**
288
* Removes all non-newline characters
289
*/
290
var reNonNewline = /[^\n]/g;
291
function stripNonNewline(value) {
292
return value.replace(reNonNewline, function() {
293
return '';
294
});
295
}
296
297
/**
298
* Catches up as `catchup` but removes all non-newline characters.
299
*
300
* Equivalent to appending as many newlines as there are in the original source
301
* between the current position and `end`.
302
*/
303
function catchupNewlines(end, state) {
304
catchup(end, state, stripNonNewline);
305
}
306
307
308
/**
309
* Same as catchup but does not touch the buffer
310
*
311
* @param {number} end
312
* @param {object} state
313
*/
314
function move(end, state) {
315
// move the internal cursors
316
if (state.g.sourceMap) {
317
if (end < state.g.position) {
318
state.g.position = 0;
319
state.g.sourceLine = 1;
320
state.g.sourceColumn = 0;
321
}
322
323
var source = state.g.source.substring(state.g.position, end);
324
var sourceLines = source.split('\n');
325
if (sourceLines.length > 1) {
326
state.g.sourceLine += sourceLines.length - 1;
327
state.g.sourceColumn = 0;
328
}
329
state.g.sourceColumn += sourceLines[sourceLines.length - 1].length;
330
}
331
state.g.position = end;
332
}
333
334
/**
335
* Appends a string of text to the buffer
336
*
337
* @param {string} str
338
* @param {object} state
339
*/
340
function append(str, state) {
341
if (state.g.sourceMap && str) {
342
state.g.sourceMap.addMapping({
343
generated: { line: state.g.bufferLine, column: state.g.bufferColumn },
344
original: { line: state.g.sourceLine, column: state.g.sourceColumn },
345
source: state.g.sourceMapFilename
346
});
347
var transformedLines = str.split('\n');
348
if (transformedLines.length > 1) {
349
state.g.bufferLine += transformedLines.length - 1;
350
state.g.bufferColumn = 0;
351
}
352
state.g.bufferColumn +=
353
transformedLines[transformedLines.length - 1].length;
354
}
355
state.g.buffer += str;
356
}
357
358
/**
359
* Update indent using state.indentBy property. Indent is measured in
360
* double spaces. Updates a single line only.
361
*
362
* @param {string} str
363
* @param {object} state
364
* @return {string}
365
*/
366
function updateIndent(str, state) {
367
var indentBy = state.indentBy;
368
if (indentBy < 0) {
369
for (var i = 0; i < -indentBy; i++) {
370
str = str.replace(leadingIndentRegexp, '$1');
371
}
372
} else {
373
for (var i = 0; i < indentBy; i++) {
374
str = str.replace(leadingIndentRegexp, '$1$2$2');
375
}
376
}
377
return str;
378
}
379
380
/**
381
* Calculates indent from the beginning of the line until "start" or the first
382
* character before start.
383
* @example
384
* " foo.bar()"
385
* ^
386
* start
387
* indent will be " "
388
*
389
* @param {number} start
390
* @param {object} state
391
* @return {string}
392
*/
393
function indentBefore(start, state) {
394
var end = start;
395
start = start - 1;
396
397
while (start > 0 && state.g.source[start] != '\n') {
398
if (!state.g.source[start].match(/[ \t]/)) {
399
end = start;
400
}
401
start--;
402
}
403
return state.g.source.substring(start + 1, end);
404
}
405
406
function getDocblock(state) {
407
if (!state.g.docblock) {
408
var docblock = require('./docblock');
409
state.g.docblock =
410
docblock.parseAsObject(docblock.extract(state.g.source));
411
}
412
return state.g.docblock;
413
}
414
415
function identWithinLexicalScope(identName, state, stopBeforeNode) {
416
var currScope = state.localScope;
417
while (currScope) {
418
if (currScope.identifiers[identName] !== undefined) {
419
return true;
420
}
421
422
if (stopBeforeNode && currScope.parentNode === stopBeforeNode) {
423
break;
424
}
425
426
currScope = currScope.parentScope;
427
}
428
return false;
429
}
430
431
function identInLocalScope(identName, state) {
432
return state.localScope.identifiers[identName] !== undefined;
433
}
434
435
/**
436
* @param {object} boundaryNode
437
* @param {?array} path
438
* @return {?object} node
439
*/
440
function initScopeMetadata(boundaryNode, path, node) {
441
return {
442
boundaryNode: boundaryNode,
443
bindingPath: path,
444
bindingNode: node
445
};
446
}
447
448
function declareIdentInLocalScope(identName, metaData, state) {
449
state.localScope.identifiers[identName] = {
450
boundaryNode: metaData.boundaryNode,
451
path: metaData.bindingPath,
452
node: metaData.bindingNode,
453
state: Object.create(state)
454
};
455
}
456
457
function getLexicalBindingMetadata(identName, state) {
458
return state.localScope.identifiers[identName];
459
}
460
461
/**
462
* Apply the given analyzer function to the current node. If the analyzer
463
* doesn't return false, traverse each child of the current node using the given
464
* traverser function.
465
*
466
* @param {function} analyzer
467
* @param {function} traverser
468
* @param {object} node
469
* @param {array} path
470
* @param {object} state
471
*/
472
function analyzeAndTraverse(analyzer, traverser, node, path, state) {
473
if (node.type) {
474
if (analyzer(node, path, state) === false) {
475
return;
476
}
477
path.unshift(node);
478
}
479
480
getOrderedChildren(node).forEach(function(child) {
481
traverser(child, path, state);
482
});
483
484
node.type && path.shift();
485
}
486
487
/**
488
* It is crucial that we traverse in order, or else catchup() on a later
489
* node that is processed out of order can move the buffer past a node
490
* that we haven't handled yet, preventing us from modifying that node.
491
*
492
* This can happen when a node has multiple properties containing children.
493
* For example, XJSElement nodes have `openingElement`, `closingElement` and
494
* `children`. If we traverse `openingElement`, then `closingElement`, then
495
* when we get to `children`, the buffer has already caught up to the end of
496
* the closing element, after the children.
497
*
498
* This is basically a Schwartzian transform. Collects an array of children,
499
* each one represented as [child, startIndex]; sorts the array by start
500
* index; then traverses the children in that order.
501
*/
502
function getOrderedChildren(node) {
503
var queue = [];
504
for (var key in node) {
505
if (node.hasOwnProperty(key)) {
506
enqueueNodeWithStartIndex(queue, node[key]);
507
}
508
}
509
queue.sort(function(a, b) { return a[1] - b[1]; });
510
return queue.map(function(pair) { return pair[0]; });
511
}
512
513
/**
514
* Helper function for analyzeAndTraverse which queues up all of the children
515
* of the given node.
516
*
517
* Children can also be found in arrays, so we basically want to merge all of
518
* those arrays together so we can sort them and then traverse the children
519
* in order.
520
*
521
* One example is the Program node. It contains `body` and `comments`, both
522
* arrays. Lexographically, comments are interspersed throughout the body
523
* nodes, but esprima's AST groups them together.
524
*/
525
function enqueueNodeWithStartIndex(queue, node) {
526
if (typeof node !== 'object' || node === null) {
527
return;
528
}
529
if (node.range) {
530
queue.push([node, node.range[0]]);
531
} else if (Array.isArray(node)) {
532
for (var ii = 0; ii < node.length; ii++) {
533
enqueueNodeWithStartIndex(queue, node[ii]);
534
}
535
}
536
}
537
538
/**
539
* Checks whether a node or any of its sub-nodes contains
540
* a syntactic construct of the passed type.
541
* @param {object} node - AST node to test.
542
* @param {string} type - node type to lookup.
543
*/
544
function containsChildOfType(node, type) {
545
return containsChildMatching(node, function(node) {
546
return node.type === type;
547
});
548
}
549
550
function containsChildMatching(node, matcher) {
551
var foundMatchingChild = false;
552
function nodeTypeAnalyzer(node) {
553
if (matcher(node) === true) {
554
foundMatchingChild = true;
555
return false;
556
}
557
}
558
function nodeTypeTraverser(child, path, state) {
559
if (!foundMatchingChild) {
560
foundMatchingChild = containsChildMatching(child, matcher);
561
}
562
}
563
analyzeAndTraverse(
564
nodeTypeAnalyzer,
565
nodeTypeTraverser,
566
node,
567
[]
568
);
569
return foundMatchingChild;
570
}
571
572
var scopeTypes = {};
573
scopeTypes[Syntax.FunctionExpression] = true;
574
scopeTypes[Syntax.FunctionDeclaration] = true;
575
scopeTypes[Syntax.Program] = true;
576
577
function getBoundaryNode(path) {
578
for (var ii = 0; ii < path.length; ++ii) {
579
if (scopeTypes[path[ii].type]) {
580
return path[ii];
581
}
582
}
583
throw new Error(
584
'Expected to find a node with one of the following types in path:\n' +
585
JSON.stringify(Object.keys(scopeTypes))
586
);
587
}
588
589
function getTempVar(tempVarIndex) {
590
return '$__' + tempVarIndex;
591
}
592
593
function getTempVarWithValue(tempVarIndex, tempVarValue) {
594
return getTempVar(tempVarIndex) + '=' + tempVarValue;
595
}
596
597
exports.append = append;
598
exports.catchup = catchup;
599
exports.catchupWhiteOut = catchupWhiteOut;
600
exports.catchupWhiteSpace = catchupWhiteSpace;
601
exports.catchupNewlines = catchupNewlines;
602
exports.containsChildMatching = containsChildMatching;
603
exports.containsChildOfType = containsChildOfType;
604
exports.createState = createState;
605
exports.declareIdentInLocalScope = declareIdentInLocalScope;
606
exports.getBoundaryNode = getBoundaryNode;
607
exports.getDocblock = getDocblock;
608
exports.getLexicalBindingMetadata = getLexicalBindingMetadata;
609
exports.initScopeMetadata = initScopeMetadata;
610
exports.identWithinLexicalScope = identWithinLexicalScope;
611
exports.identInLocalScope = identInLocalScope;
612
exports.indentBefore = indentBefore;
613
exports.move = move;
614
exports.scopeTypes = scopeTypes;
615
exports.updateIndent = updateIndent;
616
exports.updateState = updateState;
617
exports.analyzeAndTraverse = analyzeAndTraverse;
618
exports.getOrderedChildren = getOrderedChildren;
619
exports.getNodeSourceText = getNodeSourceText;
620
exports.getTempVar = getTempVar;
621
exports.getTempVarWithValue = getTempVarWithValue;
622
623