feature: Plugin model for TypeScript indexer (#3787)
* add plugin interface
* Adds tests for plugin model
* Use path to VName function instead of Map
* Remove compilation unit from plugin
* refactor: `Vistor` -> `Visitor`
* differentiate node kind/value in plugin test
diff --git a/kythe/typescript/indexer.ts b/kythe/typescript/indexer.ts
index 5e6a40e..74dc7b9 100644
--- a/kythe/typescript/indexer.ts
+++ b/kythe/typescript/indexer.ts
@@ -33,6 +33,17 @@
}
/**
+ * A indexer plugin adds extra functionality with the same inputs as the base
+ * indexer.
+ */
+export interface Plugin {
+ name: string;
+ index:
+ (pathToVName: (path: string) => VName, paths: string[],
+ program: ts.Program, emit?: (obj: {}) => void) => void;
+}
+
+/**
* toArray converts an Iterator to an array of its values.
* It's necessary when running in ES5 environments where for-of loops
* don't iterate through Iterators.
@@ -54,6 +65,21 @@
}
/**
+ * getFileVName returns the VName for a given file path.
+ */
+function getFileVName(
+ path: string, cache: Map<string, VName>, compilationUnit: VName): VName {
+ const vname = cache.get(path);
+ return {
+ signature: '',
+ language: '',
+ corpus: vname && vname.corpus ? vname.corpus : compilationUnit.corpus,
+ root: vname && vname.corpus ? vname.root : compilationUnit.root,
+ path: vname && vname.path ? vname.path : path,
+ };
+}
+
+/**
* TSNamespace represents the two namespaces of TypeScript: types and values.
* A given symbol may be a type, it may be a value, and the two may even
* be unrelated.
@@ -70,7 +96,7 @@
}
/** Visitor manages the indexing process for a single TypeScript SourceFile. */
-class Vistor {
+class Visitor {
/** kFile is the VName for the 'file' node representing the source file. */
kFile: VName;
@@ -166,15 +192,7 @@
* newFileVName returns a new VName for the given file path.
*/
newFileVName(path: string): VName {
- const vname = this.pathVNames.get(path);
- return {
- signature: '',
- language: '',
- corpus: vname && vname.corpus ? vname.corpus :
- this.compilationUnit.corpus,
- root: vname && vname.corpus ? vname.root : this.compilationUnit.root,
- path: vname && vname.path ? vname.path : path,
- };
+ return getFileVName(path, this.pathVNames, this.compilationUnit);
}
/**
@@ -790,7 +808,7 @@
kFunc = this.newVName('TODO', 'TODOPath');
}
if (kFunc) {
- this.emitEdge(this.newAnchor(decl), 'defines', kFunc);
+ this.emitEdge(this.newAnchor(decl), 'defines', kFunc);
}
if (kFunc && decl.parent) {
@@ -1030,6 +1048,8 @@
* @param pathVNames A map of file path to path-specific VName.
* @param emit If provided, a function that receives objects as they are
* emitted; otherwise, they are printed to stdout.
+ * @param plugins If provided, a list of plugin indexers to run after the
+ * TypeScript program has been indexed.
* @param readFile If provided, a function that reads a file as bytes to a
* Node Buffer. It'd be nice to just reuse program.getSourceFile but
* unfortunately that returns a (Unicode) string and we need to get at
@@ -1037,7 +1057,7 @@
*/
export function index(
vname: VName, pathVNames: Map<string, VName>, paths: string[],
- program: ts.Program, emit?: (obj: {}) => void,
+ program: ts.Program, emit?: (obj: {}) => void, plugins?: Plugin[],
readFile: (path: string) => Buffer = fs.readFileSync) {
// Note: we only call getPreEmitDiagnostics (which causes type checking to
// happen) on the input paths as provided in paths. This means we don't
@@ -1081,12 +1101,20 @@
throw new Error(`requested indexing ${path} not found in program`);
}
const visitor =
- new Vistor(vname, pathVNames, program, sourceFile, getOffsetTable);
+ new Visitor(vname, pathVNames, program, sourceFile, getOffsetTable);
if (emit != null) {
visitor.emit = emit;
}
visitor.index();
}
+
+ if (plugins) {
+ for (const plugin of plugins) {
+ plugin.index(
+ (path: string) => getFileVName(path, pathVNames, vname), paths,
+ program, emit);
+ }
+ }
}
/**
diff --git a/kythe/typescript/test.ts b/kythe/typescript/test.ts
index cfc1636..950861e 100644
--- a/kythe/typescript/test.ts
+++ b/kythe/typescript/test.ts
@@ -60,8 +60,8 @@
* be run async; if there's an error, it will reject the promise.
*/
function verify(
- host: ts.CompilerHost, options: ts.CompilerOptions,
- test: string): Promise<void> {
+ host: ts.CompilerHost, options: ts.CompilerOptions, test: string,
+ plugins?: indexer.Plugin[]): Promise<void> {
const compilationUnit: indexer.VName = {
corpus: 'testcorpus',
root: '',
@@ -81,7 +81,7 @@
indexer.index(compilationUnit, new Map(), [test], program, (obj: {}) => {
verifier.stdin.write(JSON.stringify(obj) + '\n');
- });
+ }, plugins);
verifier.stdin.end();
return new Promise<void>((resolve, reject) => {
@@ -102,7 +102,7 @@
assert.deepEqual(config.fileNames, [path.resolve('testdata/alt.ts')]);
}
-async function testIndexer(args: string[]) {
+async function testIndexer(args: string[], plugins?: indexer.Plugin[]) {
const config = indexer.loadTsConfig('testdata/tsconfig.json', 'testdata');
let testPaths = args.map(arg => path.resolve(arg));
if (args.length === 0) {
@@ -117,7 +117,7 @@
const start = new Date().valueOf();
process.stdout.write(`${testName}: `);
try {
- await verify(host, config.options, test);
+ await verify(host, config.options, test, plugins);
} catch (e) {
console.log('FAIL');
throw e;
@@ -128,9 +128,38 @@
return 0;
}
+async function testPlugin() {
+ const plugin: indexer.Plugin = {
+ name: 'TestPlugin',
+ index(
+ pathToVName: (path: string) => indexer.VName, paths: string[],
+ program: ts.Program, emit?: (obj: {}) => void) {
+ for (const testPath of paths) {
+ const relPath = path.relative(
+ program.getCompilerOptions().rootDir!,
+ program.getSourceFile(testPath)!.fileName)
+ .replace(/\.(d\.)?ts$/, '');
+
+ const pluginMod = {
+ ...pathToVName(relPath),
+ signature: 'plugin-module',
+ language: 'plugin-language',
+ };
+ emit!({
+ source: pluginMod,
+ fact_name: '/kythe/node/pluginKind',
+ fact_value: Buffer.from('pluginRecord').toString('base64'),
+ });
+ }
+ },
+ };
+ return testIndexer(['testdata/plugin.ts'], [plugin]);
+}
+
async function testMain(args: string[]) {
testLoadTsConfig();
await testIndexer(args);
+ await testPlugin();
}
testMain(process.argv.slice(2))
diff --git a/kythe/typescript/testdata/plugin.ts b/kythe/typescript/testdata/plugin.ts
new file mode 100644
index 0000000..7639306
--- /dev/null
+++ b/kythe/typescript/testdata/plugin.ts
@@ -0,0 +1,5 @@
+// Tests that a plugin emits entries on top of the base indexer.
+
+// From the TypeScript indexer
+//- _Mod=vname("module", _, _, "testdata/plugin", "typescript").node/kind record
+//- _PluginMod=vname("plugin-module", _, _, "testdata/plugin", "plugin-language").node/pluginKind pluginRecord
diff --git a/kythe/typescript/testdata/tsconfig.json b/kythe/typescript/testdata/tsconfig.json
index 2da85c9..9a059fb 100644
--- a/kythe/typescript/testdata/tsconfig.json
+++ b/kythe/typescript/testdata/tsconfig.json
@@ -28,6 +28,7 @@
]
},
"exclude": [
- "compilefail.ts"
+ "compilefail.ts",
+ "plugin.ts"
]
}