-
Notifications
You must be signed in to change notification settings - Fork 350
/
patcher.js
480 lines (388 loc) · 14.9 KB
/
patcher.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
var assert = require("assert");
var linesModule = require("./lines");
var types = require("./types");
var getFieldValue = types.getFieldValue;
var Printable = types.namedTypes.Printable;
var Expression = types.namedTypes.Expression;
var SourceLocation = types.namedTypes.SourceLocation;
var util = require("./util");
var comparePos = util.comparePos;
var FastPath = require("./fast-path");
var isObject = types.builtInTypes.object;
var isArray = types.builtInTypes.array;
var isString = types.builtInTypes.string;
var riskyAdjoiningCharExp = /[0-9a-z_$]/i;
function Patcher(lines) {
assert.ok(this instanceof Patcher);
assert.ok(lines instanceof linesModule.Lines);
var self = this,
replacements = [];
self.replace = function(loc, lines) {
if (isString.check(lines))
lines = linesModule.fromString(lines);
replacements.push({
lines: lines,
start: loc.start,
end: loc.end
});
};
self.get = function(loc) {
// If no location is provided, return the complete Lines object.
loc = loc || {
start: { line: 1, column: 0 },
end: { line: lines.length,
column: lines.getLineLength(lines.length) }
};
var sliceFrom = loc.start,
toConcat = [];
function pushSlice(from, to) {
assert.ok(comparePos(from, to) <= 0);
toConcat.push(lines.slice(from, to));
}
replacements.sort(function(a, b) {
return comparePos(a.start, b.start);
}).forEach(function(rep) {
if (comparePos(sliceFrom, rep.start) > 0) {
// Ignore nested replacement ranges.
} else {
pushSlice(sliceFrom, rep.start);
toConcat.push(rep.lines);
sliceFrom = rep.end;
}
});
pushSlice(sliceFrom, loc.end);
return linesModule.concat(toConcat);
};
}
exports.Patcher = Patcher;
var Pp = Patcher.prototype;
Pp.tryToReprintComments = function(newNode, oldNode, print) {
var patcher = this;
if (!newNode.comments &&
!oldNode.comments) {
// We were (vacuously) able to reprint all the comments!
return true;
}
var newPath = FastPath.from(newNode);
var oldPath = FastPath.from(oldNode);
newPath.stack.push("comments", getSurroundingComments(newNode));
oldPath.stack.push("comments", getSurroundingComments(oldNode));
var reprints = [];
var ableToReprintComments =
findArrayReprints(newPath, oldPath, reprints);
// No need to pop anything from newPath.stack or oldPath.stack, since
// newPath and oldPath are fresh local variables.
if (ableToReprintComments && reprints.length > 0) {
reprints.forEach(function(reprint) {
var oldComment = reprint.oldPath.getValue();
assert.ok(oldComment.leading || oldComment.trailing);
patcher.replace(
oldComment.loc,
// Comments can't have .comments, so it doesn't matter
// whether we print with comments or without.
print(reprint.newPath).indentTail(oldComment.loc.indent)
);
});
}
return ableToReprintComments;
};
// Get all comments that are either leading or trailing, ignoring any
// comments that occur inside node.loc. Returns an empty array for nodes
// with no leading or trailing comments.
function getSurroundingComments(node) {
var result = [];
if (node.comments &&
node.comments.length > 0) {
node.comments.forEach(function(comment) {
if (comment.leading || comment.trailing) {
result.push(comment);
}
});
}
return result;
}
Pp.deleteComments = function(node) {
if (!node.comments) {
return;
}
var patcher = this;
node.comments.forEach(function(comment) {
if (comment.leading) {
// Delete leading comments along with any trailing whitespace
// they might have.
patcher.replace({
start: comment.loc.start,
end: node.loc.lines.skipSpaces(
comment.loc.end, false, false)
}, "");
} else if (comment.trailing) {
// Delete trailing comments along with any leading whitespace
// they might have.
patcher.replace({
start: node.loc.lines.skipSpaces(
comment.loc.start, true, false),
end: comment.loc.end
}, "");
}
});
};
exports.getReprinter = function(path) {
assert.ok(path instanceof FastPath);
// Make sure that this path refers specifically to a Node, rather than
// some non-Node subproperty of a Node.
var node = path.getValue();
if (!Printable.check(node))
return;
var orig = node.original;
var origLoc = orig && orig.loc;
var lines = origLoc && origLoc.lines;
var reprints = [];
if (!lines || !findReprints(path, reprints))
return;
return function(print) {
var patcher = new Patcher(lines);
reprints.forEach(function(reprint) {
var newNode = reprint.newPath.getValue();
var oldNode = reprint.oldPath.getValue();
SourceLocation.assert(oldNode.loc, true);
var needToPrintNewPathWithComments =
!patcher.tryToReprintComments(newNode, oldNode, print)
if (needToPrintNewPathWithComments) {
// Since we were not able to preserve all leading/trailing
// comments, we delete oldNode's comments, print newPath
// with comments, and then patch the resulting lines where
// oldNode used to be.
patcher.deleteComments(oldNode);
}
var pos = util.copyPos(oldNode.loc.start);
var needsLeadingSpace = lines.prevPos(pos) &&
riskyAdjoiningCharExp.test(lines.charAt(pos));
var newLines = print(
reprint.newPath,
needToPrintNewPathWithComments
).indentTail(oldNode.loc.indent);
var needsTrailingSpace =
riskyAdjoiningCharExp.test(lines.charAt(oldNode.loc.end));
// If we try to replace the argument of a ReturnStatement like
// return"asdf" with e.g. a literal null expression, we run
// the risk of ending up with returnnull, so we need to add an
// extra leading space in situations where that might
// happen. Likewise for "asdf"in obj. See #170.
if (needsLeadingSpace || needsTrailingSpace) {
var newParts = [];
needsLeadingSpace && newParts.push(" ");
newParts.push(newLines);
needsTrailingSpace && newParts.push(" ");
newLines = linesModule.concat(newParts);
}
patcher.replace(oldNode.loc, newLines);
});
// Recall that origLoc is the .loc of an ancestor node that is
// guaranteed to contain all the reprinted nodes and comments.
return patcher.get(origLoc).indentTail(-orig.loc.indent);
};
};
function findReprints(newPath, reprints) {
var newNode = newPath.getValue();
Printable.assert(newNode);
var oldNode = newNode.original;
Printable.assert(oldNode);
assert.deepEqual(reprints, []);
if (newNode.type !== oldNode.type) {
return false;
}
var oldPath = new FastPath(oldNode);
var canReprint = findChildReprints(newPath, oldPath, reprints);
if (!canReprint) {
// Make absolutely sure the calling code does not attempt to reprint
// any nodes.
reprints.length = 0;
}
return canReprint;
}
function findAnyReprints(newPath, oldPath, reprints) {
var newNode = newPath.getValue();
var oldNode = oldPath.getValue();
if (newNode === oldNode)
return true;
if (isArray.check(newNode))
return findArrayReprints(newPath, oldPath, reprints);
if (isObject.check(newNode))
return findObjectReprints(newPath, oldPath, reprints);
return false;
}
function findArrayReprints(newPath, oldPath, reprints) {
var newNode = newPath.getValue();
var oldNode = oldPath.getValue();
isArray.assert(newNode);
var len = newNode.length;
if (!(isArray.check(oldNode) &&
oldNode.length === len))
return false;
for (var i = 0; i < len; ++i) {
newPath.stack.push(i, newNode[i]);
oldPath.stack.push(i, oldNode[i]);
var canReprint = findAnyReprints(newPath, oldPath, reprints);
newPath.stack.length -= 2;
oldPath.stack.length -= 2;
if (!canReprint) {
return false;
}
}
return true;
}
function findObjectReprints(newPath, oldPath, reprints) {
var newNode = newPath.getValue();
isObject.assert(newNode);
if (newNode.original === null) {
// If newNode.original node was set to null, reprint the node.
return false;
}
var oldNode = oldPath.getValue();
if (!isObject.check(oldNode))
return false;
if (Printable.check(newNode)) {
if (!Printable.check(oldNode)) {
return false;
}
// Here we need to decide whether the reprinted code for newNode
// is appropriate for patching into the location of oldNode.
if (newNode.type === oldNode.type) {
var childReprints = [];
if (findChildReprints(newPath, oldPath, childReprints)) {
reprints.push.apply(reprints, childReprints);
} else if (oldNode.loc) {
// If we have no .loc information for oldNode, then we
// won't be able to reprint it.
reprints.push({
oldPath: oldPath.copy(),
newPath: newPath.copy()
});
} else {
return false;
}
return true;
}
if (Expression.check(newNode) &&
Expression.check(oldNode) &&
// If we have no .loc information for oldNode, then we won't
// be able to reprint it.
oldNode.loc) {
// If both nodes are subtypes of Expression, then we should be
// able to fill the location occupied by the old node with
// code printed for the new node with no ill consequences.
reprints.push({
oldPath: oldPath.copy(),
newPath: newPath.copy()
});
return true;
}
// The nodes have different types, and at least one of the types
// is not a subtype of the Expression type, so we cannot safely
// assume the nodes are syntactically interchangeable.
return false;
}
return findChildReprints(newPath, oldPath, reprints);
}
// This object is reused in hasOpeningParen and hasClosingParen to avoid
// having to allocate a temporary object.
var reusablePos = { line: 1, column: 0 };
var nonSpaceExp = /\S/;
function hasOpeningParen(oldPath) {
var oldNode = oldPath.getValue();
var loc = oldNode.loc;
var lines = loc && loc.lines;
if (lines) {
var pos = reusablePos;
pos.line = loc.start.line;
pos.column = loc.start.column;
while (lines.prevPos(pos)) {
var ch = lines.charAt(pos);
if (ch === "(") {
// If we found an opening parenthesis but it occurred before
// the start of the original subtree for this reprinting, then
// we must not return true for hasOpeningParen(oldPath).
return comparePos(oldPath.getRootValue().loc.start, pos) <= 0;
}
if (nonSpaceExp.test(ch)) {
return false;
}
}
}
return false;
}
function hasClosingParen(oldPath) {
var oldNode = oldPath.getValue();
var loc = oldNode.loc;
var lines = loc && loc.lines;
if (lines) {
var pos = reusablePos;
pos.line = loc.end.line;
pos.column = loc.end.column;
do {
var ch = lines.charAt(pos);
if (ch === ")") {
// If we found a closing parenthesis but it occurred after the
// end of the original subtree for this reprinting, then we
// must not return true for hasClosingParen(oldPath).
return comparePos(pos, oldPath.getRootValue().loc.end) <= 0;
}
if (nonSpaceExp.test(ch)) {
return false;
}
} while (lines.nextPos(pos));
}
return false;
}
function hasParens(oldPath) {
// This logic can technically be fooled if the node has parentheses
// but there are comments intervening between the parentheses and the
// node. In such cases the node will be harmlessly wrapped in an
// additional layer of parentheses.
return hasOpeningParen(oldPath) && hasClosingParen(oldPath);
}
function findChildReprints(newPath, oldPath, reprints) {
var newNode = newPath.getValue();
var oldNode = oldPath.getValue();
isObject.assert(newNode);
isObject.assert(oldNode);
if (newNode.original === null) {
// If newNode.original node was set to null, reprint the node.
return false;
}
// If this type of node cannot come lexically first in its enclosing
// statement (e.g. a function expression or object literal), and it
// seems to be doing so, then the only way we can ignore this problem
// and save ourselves from falling back to the pretty printer is if an
// opening parenthesis happens to precede the node. For example,
// (function(){ ... }()); does not need to be reprinted, even though
// the FunctionExpression comes lexically first in the enclosing
// ExpressionStatement and fails the hasParens test, because the
// parent CallExpression passes the hasParens test. If we relied on
// the path.needsParens() && !hasParens(oldNode) check below, the
// absence of a closing parenthesis after the FunctionExpression would
// trigger pretty-printing unnecessarily.
if (!newPath.canBeFirstInStatement() &&
newPath.firstInStatement() &&
!hasOpeningParen(oldPath))
return false;
// If this node needs parentheses and will not be wrapped with
// parentheses when reprinted, then return false to skip reprinting
// and let it be printed generically.
if (newPath.needsParens(true) && !hasParens(oldPath)) {
return false;
}
for (var k in util.getUnionOfKeys(newNode, oldNode)) {
if (k === "loc")
continue;
newPath.stack.push(k, types.getFieldValue(newNode, k));
oldPath.stack.push(k, types.getFieldValue(oldNode, k));
var canReprint = findAnyReprints(newPath, oldPath, reprints);
newPath.stack.length -= 2;
oldPath.stack.length -= 2;
if (!canReprint) {
return false;
}
}
return true;
}