feat(ivy): a generic visitor which allows prefixing nodes for ngtsc (#24230)

This adds ngtsc/util/src/visitor, a utility for visiting TS ASTs that
can add synthetic nodes immediately prior to certain types of nodes (e.g.
class declarations). It's useful to lift definitions that need to be
referenced repeatedly in generated code outside of the class that defines
them.

PR Close #24230
This commit is contained in:
Alex Rickabaugh
2018-05-30 16:02:53 -07:00
committed by Miško Hevery
parent f781f741ea
commit ca79e11bfa
9 changed files with 284 additions and 18 deletions

View File

@ -0,0 +1,15 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ts_library")
load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test")
ts_library(
name = "testing",
testonly = 1,
srcs = glob([
"**/*.ts",
]),
deps = [
"//packages:types",
],
)

View File

@ -0,0 +1,130 @@
/**
* @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 * as path from 'path';
import * as ts from 'typescript';
export function makeProgram(files: {name: string, contents: string}[]):
{program: ts.Program, host: ts.CompilerHost} {
const host = new InMemoryHost();
files.forEach(file => host.writeFile(file.name, file.contents));
const rootNames = files.map(file => host.getCanonicalFileName(file.name));
const program = ts.createProgram(rootNames, {noLib: true, experimentalDecorators: true}, host);
const diags = [...program.getSyntacticDiagnostics(), ...program.getSemanticDiagnostics()];
if (diags.length > 0) {
throw new Error(
`Typescript diagnostics failed! ${diags.map(diag => diag.messageText).join(', ')}`);
}
return {program, host};
}
export class InMemoryHost implements ts.CompilerHost {
private fileSystem = new Map<string, string>();
getSourceFile(
fileName: string, languageVersion: ts.ScriptTarget,
onError?: ((message: string) => void)|undefined,
shouldCreateNewSourceFile?: boolean|undefined): ts.SourceFile|undefined {
const contents = this.fileSystem.get(this.getCanonicalFileName(fileName));
if (contents === undefined) {
onError && onError(`File does not exist: ${this.getCanonicalFileName(fileName)})`);
return undefined;
}
return ts.createSourceFile(fileName, contents, languageVersion, undefined, ts.ScriptKind.TS);
}
getDefaultLibFileName(options: ts.CompilerOptions): string { return '/lib.d.ts'; }
writeFile(
fileName: string, data: string, writeByteOrderMark?: boolean,
onError?: ((message: string) => void)|undefined,
sourceFiles?: ReadonlyArray<ts.SourceFile>): void {
this.fileSystem.set(this.getCanonicalFileName(fileName), data);
}
getCurrentDirectory(): string { return '/'; }
getDirectories(dir: string): string[] {
const fullDir = this.getCanonicalFileName(dir) + '/';
const dirSet = new Set(Array
// Look at all paths known to the host.
.from(this.fileSystem.keys())
// Filter out those that aren't under the requested directory.
.filter(candidate => candidate.startsWith(fullDir))
// Relativize the rest by the requested directory.
.map(candidate => candidate.substr(fullDir.length))
// What's left are dir/.../file.txt entries, and file.txt entries.
// Get the dirname, which
// yields '.' for the latter and dir/... for the former.
.map(candidate => path.dirname(candidate))
// Filter out the '.' entries, which were files.
.filter(candidate => candidate !== '.')
// Finally, split on / and grab the first entry.
.map(candidate => candidate.split('/', 1)[0]));
// Get the resulting values out of the Set.
return Array.from(dirSet);
}
getCanonicalFileName(fileName: string): string {
return path.posix.normalize(`${this.getCurrentDirectory()}/${fileName}`);
}
useCaseSensitiveFileNames(): boolean { return true; }
getNewLine(): string { return '\n'; }
fileExists(fileName: string): boolean { return this.fileSystem.has(fileName); }
readFile(fileName: string): string|undefined { return this.fileSystem.get(fileName); }
}
function bindingNameEquals(node: ts.BindingName, name: string): boolean {
if (ts.isIdentifier(node)) {
return node.text === name;
}
return false;
}
export function getDeclaration<T extends ts.Declaration>(
program: ts.Program, fileName: string, name: string, assert: (value: any) => value is T): T {
const sf = program.getSourceFile(fileName);
if (!sf) {
throw new Error(`No such file: ${fileName}`);
}
let chosenDecl: ts.Declaration|null = null;
sf.statements.forEach(stmt => {
if (chosenDecl !== null) {
return;
} else if (ts.isVariableStatement(stmt)) {
stmt.declarationList.declarations.forEach(decl => {
if (bindingNameEquals(decl.name, name)) {
chosenDecl = decl;
}
});
} else if (ts.isClassDeclaration(stmt) || ts.isFunctionDeclaration(stmt)) {
if (stmt.name !== undefined && stmt.name.text === name) {
chosenDecl = stmt;
}
}
});
chosenDecl = chosenDecl as ts.Declaration | null;
if (chosenDecl === null) {
throw new Error(`No such symbol: ${name} in ${fileName}`);
}
if (!assert(chosenDecl)) {
throw new Error(`Symbol ${name} from ${fileName} is a ${ts.SyntaxKind[chosenDecl.kind]}`);
}
return chosenDecl;
}