Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
80542 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
tempVars: []
47
},
48
/**
49
* The name (and, if applicable, expression) of the super class
50
* @type {Object}
51
*/
52
superClass: null,
53
/**
54
* The namespace to use when munging identifiers
55
* @type {String}
56
*/
57
mungeNamespace: '',
58
/**
59
* Ref to the node for the current MethodDefinition
60
* @type {Object}
61
*/
62
methodNode: null,
63
/**
64
* Ref to the node for the FunctionExpression of the enclosing
65
* MethodDefinition
66
* @type {Object}
67
*/
68
methodFuncNode: null,
69
/**
70
* Name of the enclosing class
71
* @type {String}
72
*/
73
className: null,
74
/**
75
* Whether we're currently within a `strict` scope
76
* @type {Bool}
77
*/
78
scopeIsStrict: null,
79
/**
80
* Indentation offset
81
* @type {Number}
82
*/
83
indentBy: 0,
84
/**
85
* Global state (not affected by updateState)
86
* @type {Object}
87
*/
88
g: {
89
/**
90
* A set of general options that transformations can consider while doing
91
* a transformation:
92
*
93
* - minify
94
* Specifies that transformation steps should do their best to minify
95
* the output source when possible. This is useful for places where
96
* minification optimizations are possible with higher-level context
97
* info than what jsxmin can provide.
98
*
99
* For example, the ES6 class transform will minify munged private
100
* variables if this flag is set.
101
*/
102
opts: transformOptions,
103
/**
104
* Current position in the source code
105
* @type {Number}
106
*/
107
position: 0,
108
/**
109
* Auxiliary data to be returned by transforms
110
* @type {Object}
111
*/
112
extra: {},
113
/**
114
* Buffer containing the result
115
* @type {String}
116
*/
117
buffer: '',
118
/**
119
* Source that is being transformed
120
* @type {String}
121
*/
122
source: source,
123
124
/**
125
* Cached parsed docblock (see getDocblock)
126
* @type {object}
127
*/
128
docblock: null,
129
130
/**
131
* Whether the thing was used
132
* @type {Boolean}
133
*/
134
tagNamespaceUsed: false,
135
136
/**
137
* If using bolt xjs transformation
138
* @type {Boolean}
139
*/
140
isBolt: undefined,
141
142
/**
143
* Whether to record source map (expensive) or not
144
* @type {SourceMapGenerator|null}
145
*/
146
sourceMap: null,
147
148
/**
149
* Filename of the file being processed. Will be returned as a source
150
* attribute in the source map
151
*/
152
sourceMapFilename: 'source.js',
153
154
/**
155
* Only when source map is used: last line in the source for which
156
* source map was generated
157
* @type {Number}
158
*/
159
sourceLine: 1,
160
161
/**
162
* Only when source map is used: last line in the buffer for which
163
* source map was generated
164
* @type {Number}
165
*/
166
bufferLine: 1,
167
168
/**
169
* The top-level Program AST for the original file.
170
*/
171
originalProgramAST: null,
172
173
sourceColumn: 0,
174
bufferColumn: 0
175
}
176
};
177
}
178
179
/**
180
* Updates a copy of a given state with "update" and returns an updated state.
181
*
182
* @param {object} state
183
* @param {object} update
184
* @return {object}
185
*/
186
function updateState(state, update) {
187
var ret = Object.create(state);
188
Object.keys(update).forEach(function(updatedKey) {
189
ret[updatedKey] = update[updatedKey];
190
});
191
return ret;
192
}
193
194
/**
195
* Given a state fill the resulting buffer from the original source up to
196
* the end
197
*
198
* @param {number} end
199
* @param {object} state
200
* @param {?function} contentTransformer Optional callback to transform newly
201
* added content.
202
*/
203
function catchup(end, state, contentTransformer) {
204
if (end < state.g.position) {
205
// cannot move backwards
206
return;
207
}
208
var source = state.g.source.substring(state.g.position, end);
209
var transformed = updateIndent(source, state);
210
if (state.g.sourceMap && transformed) {
211
// record where we are
212
state.g.sourceMap.addMapping({
213
generated: { line: state.g.bufferLine, column: state.g.bufferColumn },
214
original: { line: state.g.sourceLine, column: state.g.sourceColumn },
215
source: state.g.sourceMapFilename
216
});
217
218
// record line breaks in transformed source
219
var sourceLines = source.split('\n');
220
var transformedLines = transformed.split('\n');
221
// Add line break mappings between last known mapping and the end of the
222
// added piece. So for the code piece
223
// (foo, bar);
224
// > var x = 2;
225
// > var b = 3;
226
// var c =
227
// only add lines marked with ">": 2, 3.
228
for (var i = 1; i < sourceLines.length - 1; i++) {
229
state.g.sourceMap.addMapping({
230
generated: { line: state.g.bufferLine, column: 0 },
231
original: { line: state.g.sourceLine, column: 0 },
232
source: state.g.sourceMapFilename
233
});
234
state.g.sourceLine++;
235
state.g.bufferLine++;
236
}
237
// offset for the last piece
238
if (sourceLines.length > 1) {
239
state.g.sourceLine++;
240
state.g.bufferLine++;
241
state.g.sourceColumn = 0;
242
state.g.bufferColumn = 0;
243
}
244
state.g.sourceColumn += sourceLines[sourceLines.length - 1].length;
245
state.g.bufferColumn +=
246
transformedLines[transformedLines.length - 1].length;
247
}
248
state.g.buffer +=
249
contentTransformer ? contentTransformer(transformed) : transformed;
250
state.g.position = end;
251
}
252
253
/**
254
* Returns original source for an AST node.
255
* @param {object} node
256
* @param {object} state
257
* @return {string}
258
*/
259
function getNodeSourceText(node, state) {
260
return state.g.source.substring(node.range[0], node.range[1]);
261
}
262
263
function _replaceNonWhite(value) {
264
return value.replace(nonWhiteRegexp, ' ');
265
}
266
267
/**
268
* Removes all non-whitespace characters
269
*/
270
function _stripNonWhite(value) {
271
return value.replace(nonWhiteRegexp, '');
272
}
273
274
/**
275
* Finds the position of the next instance of the specified syntactic char in
276
* the pending source.
277
*
278
* NOTE: This will skip instances of the specified char if they sit inside a
279
* comment body.
280
*
281
* NOTE: This function also assumes that the buffer's current position is not
282
* already within a comment or a string. This is rarely the case since all
283
* of the buffer-advancement utility methods tend to be used on syntactic
284
* nodes' range values -- but it's a small gotcha that's worth mentioning.
285
*/
286
function getNextSyntacticCharOffset(char, state) {
287
var pendingSource = state.g.source.substring(state.g.position);
288
var pendingSourceLines = pendingSource.split('\n');
289
290
var charOffset = 0;
291
var line;
292
var withinBlockComment = false;
293
var withinString = false;
294
lineLoop: while ((line = pendingSourceLines.shift()) !== undefined) {
295
var lineEndPos = charOffset + line.length;
296
charLoop: for (; charOffset < lineEndPos; charOffset++) {
297
var currChar = pendingSource[charOffset];
298
if (currChar === '"' || currChar === '\'') {
299
withinString = !withinString;
300
continue charLoop;
301
} else if (withinString) {
302
continue charLoop;
303
} else if (charOffset + 1 < lineEndPos) {
304
var nextTwoChars = currChar + line[charOffset + 1];
305
if (nextTwoChars === '//') {
306
charOffset = lineEndPos + 1;
307
continue lineLoop;
308
} else if (nextTwoChars === '/*') {
309
withinBlockComment = true;
310
charOffset += 1;
311
continue charLoop;
312
} else if (nextTwoChars === '*/') {
313
withinBlockComment = false;
314
charOffset += 1;
315
continue charLoop;
316
}
317
}
318
319
if (!withinBlockComment && currChar === char) {
320
return charOffset + state.g.position;
321
}
322
}
323
324
// Account for '\n'
325
charOffset++;
326
withinString = false;
327
}
328
329
throw new Error('`' + char + '` not found!');
330
}
331
332
/**
333
* Catches up as `catchup` but replaces non-whitespace chars with spaces.
334
*/
335
function catchupWhiteOut(end, state) {
336
catchup(end, state, _replaceNonWhite);
337
}
338
339
/**
340
* Catches up as `catchup` but removes all non-whitespace characters.
341
*/
342
function catchupWhiteSpace(end, state) {
343
catchup(end, state, _stripNonWhite);
344
}
345
346
/**
347
* Removes all non-newline characters
348
*/
349
var reNonNewline = /[^\n]/g;
350
function stripNonNewline(value) {
351
return value.replace(reNonNewline, function() {
352
return '';
353
});
354
}
355
356
/**
357
* Catches up as `catchup` but removes all non-newline characters.
358
*
359
* Equivalent to appending as many newlines as there are in the original source
360
* between the current position and `end`.
361
*/
362
function catchupNewlines(end, state) {
363
catchup(end, state, stripNonNewline);
364
}
365
366
367
/**
368
* Same as catchup but does not touch the buffer
369
*
370
* @param {number} end
371
* @param {object} state
372
*/
373
function move(end, state) {
374
// move the internal cursors
375
if (state.g.sourceMap) {
376
if (end < state.g.position) {
377
state.g.position = 0;
378
state.g.sourceLine = 1;
379
state.g.sourceColumn = 0;
380
}
381
382
var source = state.g.source.substring(state.g.position, end);
383
var sourceLines = source.split('\n');
384
if (sourceLines.length > 1) {
385
state.g.sourceLine += sourceLines.length - 1;
386
state.g.sourceColumn = 0;
387
}
388
state.g.sourceColumn += sourceLines[sourceLines.length - 1].length;
389
}
390
state.g.position = end;
391
}
392
393
/**
394
* Appends a string of text to the buffer
395
*
396
* @param {string} str
397
* @param {object} state
398
*/
399
function append(str, state) {
400
if (state.g.sourceMap && str) {
401
state.g.sourceMap.addMapping({
402
generated: { line: state.g.bufferLine, column: state.g.bufferColumn },
403
original: { line: state.g.sourceLine, column: state.g.sourceColumn },
404
source: state.g.sourceMapFilename
405
});
406
var transformedLines = str.split('\n');
407
if (transformedLines.length > 1) {
408
state.g.bufferLine += transformedLines.length - 1;
409
state.g.bufferColumn = 0;
410
}
411
state.g.bufferColumn +=
412
transformedLines[transformedLines.length - 1].length;
413
}
414
state.g.buffer += str;
415
}
416
417
/**
418
* Update indent using state.indentBy property. Indent is measured in
419
* double spaces. Updates a single line only.
420
*
421
* @param {string} str
422
* @param {object} state
423
* @return {string}
424
*/
425
function updateIndent(str, state) {
426
/*jshint -W004*/
427
var indentBy = state.indentBy;
428
if (indentBy < 0) {
429
for (var i = 0; i < -indentBy; i++) {
430
str = str.replace(leadingIndentRegexp, '$1');
431
}
432
} else {
433
for (var i = 0; i < indentBy; i++) {
434
str = str.replace(leadingIndentRegexp, '$1$2$2');
435
}
436
}
437
return str;
438
}
439
440
/**
441
* Calculates indent from the beginning of the line until "start" or the first
442
* character before start.
443
* @example
444
* " foo.bar()"
445
* ^
446
* start
447
* indent will be " "
448
*
449
* @param {number} start
450
* @param {object} state
451
* @return {string}
452
*/
453
function indentBefore(start, state) {
454
var end = start;
455
start = start - 1;
456
457
while (start > 0 && state.g.source[start] != '\n') {
458
if (!state.g.source[start].match(/[ \t]/)) {
459
end = start;
460
}
461
start--;
462
}
463
return state.g.source.substring(start + 1, end);
464
}
465
466
function getDocblock(state) {
467
if (!state.g.docblock) {
468
var docblock = require('./docblock');
469
state.g.docblock =
470
docblock.parseAsObject(docblock.extract(state.g.source));
471
}
472
return state.g.docblock;
473
}
474
475
function identWithinLexicalScope(identName, state, stopBeforeNode) {
476
var currScope = state.localScope;
477
while (currScope) {
478
if (currScope.identifiers[identName] !== undefined) {
479
return true;
480
}
481
482
if (stopBeforeNode && currScope.parentNode === stopBeforeNode) {
483
break;
484
}
485
486
currScope = currScope.parentScope;
487
}
488
return false;
489
}
490
491
function identInLocalScope(identName, state) {
492
return state.localScope.identifiers[identName] !== undefined;
493
}
494
495
/**
496
* @param {object} boundaryNode
497
* @param {?array} path
498
* @return {?object} node
499
*/
500
function initScopeMetadata(boundaryNode, path, node) {
501
return {
502
boundaryNode: boundaryNode,
503
bindingPath: path,
504
bindingNode: node
505
};
506
}
507
508
function declareIdentInLocalScope(identName, metaData, state) {
509
state.localScope.identifiers[identName] = {
510
boundaryNode: metaData.boundaryNode,
511
path: metaData.bindingPath,
512
node: metaData.bindingNode,
513
state: Object.create(state)
514
};
515
}
516
517
function getLexicalBindingMetadata(identName, state) {
518
var currScope = state.localScope;
519
while (currScope) {
520
if (currScope.identifiers[identName] !== undefined) {
521
return currScope.identifiers[identName];
522
}
523
524
currScope = currScope.parentScope;
525
}
526
}
527
528
function getLocalBindingMetadata(identName, state) {
529
return state.localScope.identifiers[identName];
530
}
531
532
/**
533
* Apply the given analyzer function to the current node. If the analyzer
534
* doesn't return false, traverse each child of the current node using the given
535
* traverser function.
536
*
537
* @param {function} analyzer
538
* @param {function} traverser
539
* @param {object} node
540
* @param {array} path
541
* @param {object} state
542
*/
543
function analyzeAndTraverse(analyzer, traverser, node, path, state) {
544
if (node.type) {
545
if (analyzer(node, path, state) === false) {
546
return;
547
}
548
path.unshift(node);
549
}
550
551
getOrderedChildren(node).forEach(function(child) {
552
traverser(child, path, state);
553
});
554
555
node.type && path.shift();
556
}
557
558
/**
559
* It is crucial that we traverse in order, or else catchup() on a later
560
* node that is processed out of order can move the buffer past a node
561
* that we haven't handled yet, preventing us from modifying that node.
562
*
563
* This can happen when a node has multiple properties containing children.
564
* For example, XJSElement nodes have `openingElement`, `closingElement` and
565
* `children`. If we traverse `openingElement`, then `closingElement`, then
566
* when we get to `children`, the buffer has already caught up to the end of
567
* the closing element, after the children.
568
*
569
* This is basically a Schwartzian transform. Collects an array of children,
570
* each one represented as [child, startIndex]; sorts the array by start
571
* index; then traverses the children in that order.
572
*/
573
function getOrderedChildren(node) {
574
var queue = [];
575
for (var key in node) {
576
if (node.hasOwnProperty(key)) {
577
enqueueNodeWithStartIndex(queue, node[key]);
578
}
579
}
580
queue.sort(function(a, b) { return a[1] - b[1]; });
581
return queue.map(function(pair) { return pair[0]; });
582
}
583
584
/**
585
* Helper function for analyzeAndTraverse which queues up all of the children
586
* of the given node.
587
*
588
* Children can also be found in arrays, so we basically want to merge all of
589
* those arrays together so we can sort them and then traverse the children
590
* in order.
591
*
592
* One example is the Program node. It contains `body` and `comments`, both
593
* arrays. Lexographically, comments are interspersed throughout the body
594
* nodes, but esprima's AST groups them together.
595
*/
596
function enqueueNodeWithStartIndex(queue, node) {
597
if (typeof node !== 'object' || node === null) {
598
return;
599
}
600
if (node.range) {
601
queue.push([node, node.range[0]]);
602
} else if (Array.isArray(node)) {
603
for (var ii = 0; ii < node.length; ii++) {
604
enqueueNodeWithStartIndex(queue, node[ii]);
605
}
606
}
607
}
608
609
/**
610
* Checks whether a node or any of its sub-nodes contains
611
* a syntactic construct of the passed type.
612
* @param {object} node - AST node to test.
613
* @param {string} type - node type to lookup.
614
*/
615
function containsChildOfType(node, type) {
616
return containsChildMatching(node, function(node) {
617
return node.type === type;
618
});
619
}
620
621
function containsChildMatching(node, matcher) {
622
var foundMatchingChild = false;
623
function nodeTypeAnalyzer(node) {
624
if (matcher(node) === true) {
625
foundMatchingChild = true;
626
return false;
627
}
628
}
629
function nodeTypeTraverser(child, path, state) {
630
if (!foundMatchingChild) {
631
foundMatchingChild = containsChildMatching(child, matcher);
632
}
633
}
634
analyzeAndTraverse(
635
nodeTypeAnalyzer,
636
nodeTypeTraverser,
637
node,
638
[]
639
);
640
return foundMatchingChild;
641
}
642
643
var scopeTypes = {};
644
scopeTypes[Syntax.ArrowFunctionExpression] = true;
645
scopeTypes[Syntax.FunctionExpression] = true;
646
scopeTypes[Syntax.FunctionDeclaration] = true;
647
scopeTypes[Syntax.Program] = true;
648
649
function getBoundaryNode(path) {
650
for (var ii = 0; ii < path.length; ++ii) {
651
if (scopeTypes[path[ii].type]) {
652
return path[ii];
653
}
654
}
655
throw new Error(
656
'Expected to find a node with one of the following types in path:\n' +
657
JSON.stringify(Object.keys(scopeTypes))
658
);
659
}
660
661
function getTempVar(tempVarIndex) {
662
return '$__' + tempVarIndex;
663
}
664
665
function injectTempVar(state) {
666
var tempVar = '$__' + (state.localScope.tempVarIndex++);
667
state.localScope.tempVars.push(tempVar);
668
return tempVar;
669
}
670
671
function injectTempVarDeclarations(state, index) {
672
if (state.localScope.tempVars.length) {
673
state.g.buffer =
674
state.g.buffer.slice(0, index) +
675
'var ' + state.localScope.tempVars.join(', ') + ';' +
676
state.g.buffer.slice(index);
677
state.localScope.tempVars = [];
678
}
679
}
680
681
exports.analyzeAndTraverse = analyzeAndTraverse;
682
exports.append = append;
683
exports.catchup = catchup;
684
exports.catchupNewlines = catchupNewlines;
685
exports.catchupWhiteOut = catchupWhiteOut;
686
exports.catchupWhiteSpace = catchupWhiteSpace;
687
exports.containsChildMatching = containsChildMatching;
688
exports.containsChildOfType = containsChildOfType;
689
exports.createState = createState;
690
exports.declareIdentInLocalScope = declareIdentInLocalScope;
691
exports.getBoundaryNode = getBoundaryNode;
692
exports.getDocblock = getDocblock;
693
exports.getLexicalBindingMetadata = getLexicalBindingMetadata;
694
exports.getLocalBindingMetadata = getLocalBindingMetadata;
695
exports.getNextSyntacticCharOffset = getNextSyntacticCharOffset;
696
exports.getNodeSourceText = getNodeSourceText;
697
exports.getOrderedChildren = getOrderedChildren;
698
exports.getTempVar = getTempVar;
699
exports.identInLocalScope = identInLocalScope;
700
exports.identWithinLexicalScope = identWithinLexicalScope;
701
exports.indentBefore = indentBefore;
702
exports.initScopeMetadata = initScopeMetadata;
703
exports.injectTempVar = injectTempVar;
704
exports.injectTempVarDeclarations = injectTempVarDeclarations;
705
exports.move = move;
706
exports.scopeTypes = scopeTypes;
707
exports.updateIndent = updateIndent;
708
exports.updateState = updateState;
709
710