tsickle
Version:
Transpile TypeScript code to JavaScript with Closure annotations.
181 lines (165 loc) • 6.16 kB
text/typescript
/**
* @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;
}