import ts = require('typescript'); /*** * This module is designed to match simple patterns on Typescript AST Tree * it functionality mirrors jsASTMatchers which allows you to match on jsAST */ //TODO RENAME TO MATCHERS export module Matching { export type Node=ts.Node; export type Expression=ts.Expression; export type Identifier=ts.Identifier; export type SyntaxKind=ts.SyntaxKind; export type CallExpression=ts.CallExpression; export interface NodeMatcher { doMatch(node:Node):any } export interface TypedMatcher extends NodeMatcher { /** * returns null or the value not null means that matched * @param node */ doMatch(node:Node):any nodeType():ts.SyntaxKind; } export interface Transformer { (a:T):R } export interface NodeCallback{ (node: Node) :T } export interface NodesCallback{ (nodes: Node[]): T } /** * do match checks the node type and if node type is ok * calls match function otherwise it returns null */ export class BasicMatcher { protected match(node:Node):any { throw new Error() } nodeType():ts.SyntaxKind { throw new Error() } doMatch(n:Node):any { if(!n){ return null; } if (this.nodeType() == n.kind) { return this.match(n); } } } export class ClassDeclarationMatcher extends BasicMatcher implements TypedMatcher { protected match(node:Node):ts.ClassDeclaration { return node; } constructor() { super() } nodeType():ts.SyntaxKind { return ts.SyntaxKind.ClassDeclaration; } } export class FieldMatcher extends BasicMatcher implements TypedMatcher{ match(node:ts.PropertyDeclaration):ts.PropertyDeclaration { return node; } nodeType():ts.SyntaxKind{ return ts.SyntaxKind.PropertyDeclaration } } export class AssignmentExpressionMatcher extends BasicMatcher implements TypedMatcher { match(node:ts.BinaryExpression):any { if (node.operatorToken.kind==ts.SyntaxKind.EqualsToken) { if (this.left.doMatch(node.left) && this.right.doMatch(node.right)) { return this.tr(node); } } return null; } constructor(private left:TypedMatcher, private right:TypedMatcher, private tr:Transformer) { super() } nodeType():ts.SyntaxKind { return ts.SyntaxKind.BinaryExpression; } } export class VariableDeclarationMatcher extends BasicMatcher implements TypedMatcher { match(node:ts.VariableDeclaration):any { if (this.left.doMatch(node.name) && this.right.doMatch(node.initializer)) { return this.tr(node); } } constructor(private left:TypedMatcher, private right:TypedMatcher, private tr:Transformer) { super() } nodeType():ts.SyntaxKind { return ts.SyntaxKind.VariableDeclaration; } } class ExpressionStatementMatcher extends BasicMatcher implements TypedMatcher { match(node:ts.ExpressionStatement):any { var exp = this.expression.doMatch(node.expression); if (exp) { var v = this.tr(node.expression); if (v == true) { return exp; } return v; } return null; } constructor(private expression:TypedMatcher, private tr:Transformer) { super() } nodeType():ts.SyntaxKind { return ts.SyntaxKind.ExpressionStatement; } } class SimpleIdentMatcher extends BasicMatcher implements TypedMatcher { match(node:Identifier):any { if (node.text == this.val) { return true; } return null; } constructor(private val:string) { super() } nodeType():SyntaxKind { return ts.SyntaxKind.Identifier; } } class TrueMatcher implements TypedMatcher { doMatch(node:Node):any { return true; } nodeType():ts.SyntaxKind { return null; } } class CallExpressionMatcher extends BasicMatcher implements TypedMatcher { match(node:CallExpression):any { if (this.calleeMatcher.doMatch(node.expression)) { return this.tr(node); } return null; } constructor(private calleeMatcher:TypedMatcher, private tr:Transformer) { super() } nodeType():SyntaxKind { return ts.SyntaxKind.CallExpression; } } export var SKIP={} export function visit(n:Node,cb:NodeCallback):T{ var r0=cb(n); if (r0){ if(r0==SKIP){ return null; } return r0; } var r:T= ts.forEachChild(n,x=>{ var r= visit(x,cb); if (r){ return r; } }); return r; } export class PathNode { name:string arguments:ReadonlyArray = null; _callExpression:ts.CallExpression constructor(name:string,private _base:Node) { this.name = name; } } export class CallPath { base:string; start(){ return this._baseNode.pos; } startLocation(){ return this._baseNode.getSourceFile().getLineAndCharacterOfPosition(this.start()) } endLocation(){ return this._baseNode.getSourceFile().getLineAndCharacterOfPosition(this.end()) } end(){ var ce=this.path[this.path.length-1]._callExpression; if (ce){ return ce.end } return this.start(); } constructor(base:string,private _baseNode:ts.Node) { this.base = base; } path:PathNode[] = []; toString():string{ return this.path.map(x=>x.name).join("."); } } class MemberExpressionMatcher extends BasicMatcher implements TypedMatcher { match(node:ts.PropertyAccessExpression):any { if (this.objectMatcher.doMatch(node.expression) && this.propertyMatcher.doMatch(node.name)) { return this.tr(node); } return null; } nodeType():SyntaxKind { return ts.SyntaxKind.PropertyAccessExpression; } constructor(private objectMatcher:TypedMatcher, private propertyMatcher:TypedMatcher, private tr:Transformer) { super() } } export function memberFromExp(objMatcher:string, tr:Transformer = x=>true):TypedMatcher { var array:string[] = objMatcher.split("."); var result:TypedMatcher = null; for (var a = 0; a < array.length; a++) { var arg = array[a]; var ci = arg.indexOf("(*)"); var isCall = false; if (ci != -1) { arg = arg.substr(0, ci); isCall = true; } if (result == null) { result = arg == '*' ? anyNode() : ident(arg); } else { result = new MemberExpressionMatcher(result, arg == '*' ? anyNode() : ident(arg), tr); } if (isCall) { result = new CallExpressionMatcher(result, tr); } } //console.log(result) return result; } export class CallBaseMatcher implements TypedMatcher { doMatch(node:Expression):CallPath { var original = node; if (node.kind == ts.SyntaxKind.CallExpression) { var call = (node); var res:CallPath = this.doMatch(call.expression); if (res) { if (res.path.length > 0 && res.path[res.path.length - 1].arguments == null) { res.path[res.path.length - 1].arguments = call.arguments; res.path[res.path.length - 1]._callExpression=call; return res; } //This case should not exist in type script clients now //but leaving it here for possible future use at the moment; //if (res.path.length==0&&call.arguments.length==1){ // //this is not resource based call!!! // if (call.arguments[0].kind==ts.SyntaxKind.StringLiteral){ // var l:ts.LiteralExpression=call.arguments[0]; // var url=l.text; // var uriPath=url.toString().split("/"); // uriPath.forEach(x=>res.path.push( // new PathNode(x) // )) // return res; // } //} return null; } } else if (node.kind ==ts.SyntaxKind.PropertyAccessExpression) { var me = (node); var v:CallPath = this.doMatch(me.expression); if (v) { if (me.name.kind == ts.SyntaxKind.Identifier) { v.path.push(new PathNode((me.name).text,me.name)); return v; } return null; } } else if (node.kind == ts.SyntaxKind.Identifier) { var id:Identifier = node if (this.rootMatcher.doMatch(id)) { return new CallPath(id.text,id); } } return null; } nodeType():ts.SyntaxKind { return null; } constructor(private rootMatcher:TypedMatcher) { } } export function ident(name:string):TypedMatcher { return new SimpleIdentMatcher(name); } export function anyNode():TypedMatcher { return new TrueMatcher(); } export function call(calleeMatcher:TypedMatcher, tr:Transformer = x=>true):TypedMatcher { return new CallExpressionMatcher(calleeMatcher, tr); } export function exprStmt(eM:TypedMatcher, tr:Transformer = x=>true):TypedMatcher { return new ExpressionStatementMatcher(eM, tr); } export function assign(left:TypedMatcher, right:TypedMatcher, tr:Transformer = x=>true):TypedMatcher { return new AssignmentExpressionMatcher(left, right, tr); } export function varDecl(left:TypedMatcher, right:TypedMatcher, tr:Transformer = x=>true):TypedMatcher { return new VariableDeclarationMatcher(left, right, tr); } export function field(){ return new FieldMatcher() } export function classDeclaration(){ return new ClassDeclarationMatcher(); } }