rewriter.ts
6.02 KB
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
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {SourceMapGenerator} from 'source-map';
import * as ts from 'typescript';
/**
* A Rewriter manages iterating through a ts.SourceFile, copying input
* to output while letting the subclass potentially alter some nodes
* along the way by implementing maybeProcess().
*/
export abstract class Rewriter {
private output: string[] = [];
/** Errors found while examining the code. */
protected diagnostics: ts.Diagnostic[] = [];
/** The source map that's generated while rewriting this file. */
private sourceMap: SourceMapGenerator;
/** Current position in the output. */
private position = {line: 1, column: 1};
/**
* The current level of recursion through TypeScript Nodes. Used in formatting internal debug
* print statements.
*/
private indent: number = 0;
constructor(protected file: ts.SourceFile) {
this.sourceMap = new SourceMapGenerator({file: file.fileName});
this.sourceMap.addMapping({
original: this.position,
generated: this.position,
source: file.fileName,
});
}
getOutput(): {output: string, diagnostics: ts.Diagnostic[], sourceMap: SourceMapGenerator} {
if (this.indent !== 0) {
throw new Error('visit() failed to track nesting');
}
return {
output: this.output.join(''),
diagnostics: this.diagnostics,
sourceMap: this.sourceMap,
};
}
/**
* visit traverses a Node, recursively writing all nodes not handled by this.maybeProcess.
*/
visit(node: ts.Node) {
// this.logWithIndent('node: ' + ts.SyntaxKind[node.kind]);
this.indent++;
if (!this.maybeProcess(node)) {
this.writeNode(node);
}
this.indent--;
}
/**
* maybeProcess lets subclasses optionally processes a node.
*
* @return True if the node has been handled and doesn't need to be traversed;
* false to have the node written and its children recursively visited.
*/
protected maybeProcess(node: ts.Node): boolean {
return false;
}
/** writeNode writes a ts.Node, calling this.visit() on its children. */
writeNode(node: ts.Node, skipComments = false) {
let pos = node.getFullStart();
if (skipComments) {
// To skip comments, we skip all whitespace/comments preceding
// the node. But if there was anything skipped we should emit
// a newline in its place so that the node remains separated
// from the previous node. TODO: don't skip anything here if
// there wasn't any comment.
if (node.getFullStart() < node.getStart()) {
this.emit('\n');
}
pos = node.getStart();
}
ts.forEachChild(node, child => {
this.writeRange(pos, child.getFullStart());
this.visit(child);
pos = child.getEnd();
});
this.writeRange(pos, node.getEnd());
}
// Write a span of the input file as expressed by absolute offsets.
// These offsets are found in attributes like node.getFullStart() and
// node.getEnd().
writeRange(from: number, to: number) {
// getSourceFile().getText() is wrong here because it has the text of
// the SourceFile node of the AST, which doesn't contain the comments
// preceding that node. Semantically these ranges are just offsets
// into the original source file text, so slice from that.
let text = this.file.text.slice(from, to);
if (text) {
// Add a source mapping. writeRange(from, to) always corresponds to
// original source code, so add a mapping at the current location that
// points back to the location at `from`. The additional code generated
// by tsickle will then be considered part of the last mapped code
// section preceding it. That's arguably incorrect (e.g. for the fake
// methods defining properties), but is good enough for stack traces.
const pos = this.file.getLineAndCharacterOfPosition(from);
this.sourceMap.addMapping({
original: {line: pos.line + 1, column: pos.character + 1},
generated: this.position,
source: this.file.fileName,
});
this.emit(text);
}
}
emit(str: string) {
this.output.push(str);
for (const c of str) {
this.position.column++;
if (c === '\n') {
this.position.line++;
this.position.column = 1;
}
}
}
/** Removes comment metacharacters from a string, to make it safe to embed in a comment. */
escapeForComment(str: string): string {
return str.replace(/\/\*/g, '__').replace(/\*\//g, '__');
}
/* tslint:disable: no-unused-variable */
logWithIndent(message: string) {
/* tslint:enable: no-unused-variable */
let prefix = new Array(this.indent + 1).join('| ');
console.log(prefix + message);
}
/**
* Produces a compiler error that references the Node's kind. This is useful for the "else"
* branch of code that is attempting to handle all possible input Node types, to ensure all cases
* covered.
*/
errorUnimplementedKind(node: ts.Node, where: string) {
this.error(node, `${ts.SyntaxKind[node.kind]} not implemented in ${where}`);
}
error(node: ts.Node, messageText: string) {
this.diagnostics.push({
file: this.file,
start: node.getStart(),
length: node.getEnd() - node.getStart(),
messageText: messageText,
category: ts.DiagnosticCategory.Error,
code: 0,
});
}
}
/** Returns the string contents of a ts.Identifier. */
export function getIdentifierText(identifier: ts.Identifier): string {
// NOTE: the 'text' property on an Identifier may be escaped if it starts
// with '__', so just use getText().
return identifier.getText();
}
/**
* Converts an escaped TypeScript name into the original source name.
* Prefer getIdentifierText() instead if possible.
*/
export function unescapeName(name: string): string {
// See the private function unescapeIdentifier in TypeScript's utilities.ts.
if (name.match(/^___/)) return name.substr(1);
return name;
}