UNPKG

@atomist/automation-client

Version:

Atomist API for software low-level client

162 lines (141 loc) 4.92 kB
import { AllNodeTest, isNamedNodeTest, isUnionPathExpression, LocationStep, PathExpression, stringify, TreeNode, } from "@atomist/tree-path"; import * as _ from "lodash"; import * as ts from "typescript"; import { File } from "../../../project/File"; import { logger } from "../../../util/logger"; import { FileParser } from "../FileParser"; /** * Allow path expressions against ASTs from the TypeScript parser. * For reference material on the grammar, and which productions are legal * names in path expressions, see the grammar at * https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#A. * See also the ES6 grammar of which TypeScript is a superset: * http://www.ecma-international.org/ecma-262/6.0/#sec-grammar-summary. * and the SyntaxKind type defined by * the TypeScript compiler. Invalid production names will be rejected * with a runtime error. * * Will try to determine TypeScript ScriptKind from the file extension. */ export class TypeScriptFileParser implements FileParser { public rootName: string = ts.SyntaxKind[ts.SyntaxKind.SourceFile]; constructor(public scriptTarget: ts.ScriptTarget) { } public toAst(f: File): Promise<TreeNode> { return f.getContent() .then(content => { const sourceFile = ts.createSourceFile(f.name, content, this.scriptTarget, false, scriptKindFor(f)); const root = new TypeScriptAstNodeTreeNode(sourceFile, sourceFile, undefined); return root; }); } /** * Check that this path expression uses only valid TypeScript constructs * @param {PathExpression} pex */ public validate(pex: PathExpression): void { for (const ls of locationSteps(pex)) { if (isNamedNodeTest(ls.test) && ls.test !== AllNodeTest) { if (!ts.SyntaxKind[ls.test.name]) { throw new Error(`Invalid path expression '${stringify(pex)}': ` + `No such TypeScript element: '${ls.test.name}'`); } } } } } /** * Determine the script kind of the file from its extension * @param {File} f * @return {ts.ScriptKind} */ function scriptKindFor(f: File): ts.ScriptKind { switch (f.extension) { case "js": return ts.ScriptKind.JS; case "jsx": return ts.ScriptKind.JSX; case "ts": return ts.ScriptKind.TS; case "tsx": return ts.ScriptKind.TSX; default: return ts.ScriptKind.Unknown; } } function locationSteps(pex: PathExpression): LocationStep[] { return isUnionPathExpression(pex) ? _.flatten(pex.unions.map(locationSteps)) : pex.locationSteps; } /** * TreeNode implementation backed by a node from the TypeScript parser */ class TypeScriptAstNodeTreeNode implements TreeNode { public readonly $children: TreeNode[] = []; public readonly $name: string; public readonly $offset: number; constructor(private readonly sourceFile: ts.SourceFile, private readonly node: ts.Node, public $parent: TreeNode) { this.$name = extractName(node); try { this.$offset = node.getStart(sourceFile, true); } catch (e) { // Ignore and continue if (!!node.pos) { this.$offset = node.pos; } else { logger.debug("Cannot get start for node with kind %s", ts.SyntaxKind[node.kind]); } } for (const n of node.getChildren(sourceFile)) { if (!!n) { this.$children.push(new TypeScriptAstNodeTreeNode(sourceFile, n, this)); } } if (this.$children.length === 0) { // Get it off the JSON if it doesn't matter this.$children = undefined; } else { // It's a non-terminal, so the name needs to be the kind this.$name = ts.SyntaxKind[node.kind]; } } get $value(): string { return extractValue(this.sourceFile, this.node); } } function extractName(node: any): string { if (!!node.name && node.name.escapedText) { return node.name.escapedText; } else { return ts.SyntaxKind[node.kind]; } } function extractValue(sourceFile: ts.SourceFile, node: any): string { if (!!node.text) { return node.text; } try { return node.getText(sourceFile); } catch (te) { const start = node.getStart(sourceFile, true); const end = node.getEnd(sourceFile, true); if (!!start && !!end) { return sourceFile.text.substr(start, end - start); } return undefined; } } /** * Parser for TypeScript and JavaScript * @type {TypeScriptFileParser} */ export const TypeScriptES6FileParser = new TypeScriptFileParser(ts.ScriptTarget.ES2016);