feat(typescript_indexer): Getter/Setter entries (#3784)

diff --git a/CONTRIBUTORS b/CONTRIBUTORS
index c0739e4..cdd95f1 100644
--- a/CONTRIBUTORS
+++ b/CONTRIBUTORS
@@ -36,3 +36,4 @@
 Salvatore Guarnieri <salguarnieri@google.com>
 Jay Sachs <jsachs@google.com>
 Alexander Bezzubov <alex@sourced.tech>
+Ayaz Hafiz <ayaz.hafiz.1@gmail.com>
diff --git a/kythe/typescript/indexer.ts b/kythe/typescript/indexer.ts
index fdbf95c..b76857c 100644
--- a/kythe/typescript/indexer.ts
+++ b/kythe/typescript/indexer.ts
@@ -95,6 +95,17 @@
   VALUE,
 }
 
+/**
+ * Context represents the environment a node is declared in, and only applies to
+ * nodes with multiple declarations. The context may be used for disambiguating
+ * node declarations. A Getter context means the node is declared as a getter; a
+ * Setter context means it is declared as a setter.
+ */
+enum Context {
+  Getter,
+  Setter,
+}
+
 /** Visitor manages the indexing process for a single TypeScript SourceFile. */
 class Visitor {
   /** kFile is the VName for the 'file' node representing the source file. */
@@ -285,7 +296,8 @@
         case ts.SyntaxKind.Block:
           if (node.parent &&
               (node.parent.kind === ts.SyntaxKind.FunctionDeclaration ||
-               node.parent.kind === ts.SyntaxKind.MethodDeclaration)) {
+               node.parent.kind === ts.SyntaxKind.MethodDeclaration ||
+               node.parent.kind === ts.SyntaxKind.Constructor)) {
             // A block that's an immediate child of a function is the
             // function's body, so it doesn't need a separate name.
             continue;
@@ -312,9 +324,19 @@
         case ts.SyntaxKind.TypeAliasDeclaration:
         case ts.SyntaxKind.TypeParameter:
         case ts.SyntaxKind.VariableDeclaration:
+        case ts.SyntaxKind.GetAccessor:
+        case ts.SyntaxKind.SetAccessor:
           const decl = node as ts.NamedDeclaration;
           if (decl.name && decl.name.kind === ts.SyntaxKind.Identifier) {
-            parts.push(decl.name.text);
+            let part = decl.name.text;
+            // Getters and setters semantically refer to the same entities but
+            // are declared differently, so they are differentiated.
+            if (ts.isGetAccessor(decl)) {
+              part += ':getter';
+            } else if (ts.isSetAccessor(decl)) {
+              part += ':setter';
+            }
+            parts.push(part);
           } else {
             // TODO: handle other declarations, e.g. binding patterns.
             parts.push(this.anonName(node));
@@ -380,17 +402,46 @@
     return this.typeChecker.getSymbolAtLocation(node);
   }
 
-  /** getSymbolName computes the VName (and signature) of a ts.Symbol. */
-  getSymbolName(sym: ts.Symbol, ns: TSNamespace): VName {
+  /**
+   * getSymbolName computes the VName (and signature) of a ts.Symbol. A Context
+   * can be optionally specified to help disambiguate nodes with multiple
+   * declarations. See the documentation of Context for more information.
+   */
+  getSymbolName(sym: ts.Symbol, ns: TSNamespace, context?: Context): VName {
     let vnames = this.symbolNames.get(sym);
-    if (vnames && vnames[ns]) return vnames[ns]!;
+    let declarations = sym.declarations;
 
-    if (!sym.declarations || sym.declarations.length < 1) {
+    // Symbols with multiple declarations are disambiguated by the context
+    // they are used in.
+    const contextApplies = context !== undefined && declarations.length > 1;
+
+    if (!contextApplies && vnames && vnames[ns]) return vnames[ns]!;
+    // TODO: update symbolNames table to account for context kind
+
+    if (!declarations || declarations.length < 1) {
       throw new Error('TODO: symbol has no declarations?');
     }
-    // TODO: think about symbols with multiple declarations.
 
-    const decl = sym.declarations[0];
+    // Disambiguate symbols with multiple declarations using a context. This
+    // only applies to getters and setters currently.
+    if (contextApplies) {
+      switch (context) {
+        case Context.Getter:
+          declarations = declarations.filter(ts.isGetAccessor);
+          break;
+        case Context.Setter:
+          declarations = declarations.filter(ts.isSetAccessor);
+          break;
+      }
+    }
+    // Otherwise, if there are multiple declarations but no context is
+    // provided, try to return the getter declaration.
+    else if (declarations.length > 1) {
+      const getDecls = declarations.filter(ts.isGetAccessor);
+      if (getDecls.length > 0) declarations = getDecls;
+    }
+
+    const decl = declarations[0];
     const vname = this.scopedSignature(decl);
     // The signature of a value is undecorated;
     // the signature of a type has the #type suffix.
@@ -398,10 +449,12 @@
       vname.signature += '#type';
     }
 
-    // Save it in the appropriate slot in the symbolNames table.
-    if (!vnames) vnames = [null, null];
-    vnames[ns] = vname;
-    this.symbolNames.set(sym, vnames);
+    if (!contextApplies) {
+      // Save it in the appropriate slot in the symbolNames table.
+      if (!vnames) vnames = [null, null];
+      vnames[ns] = vname;
+      this.symbolNames.set(sym, vnames);
+    }
 
     return vname;
   }
@@ -665,6 +718,38 @@
   }
 
   /**
+   * Emits an implicit property for a getter or setter.
+   * For instance, a getter/setter `foo` in class `A` will emit an implicit
+   * property on that class with signature `A.foo`, and create "property/reads"
+   * and "property/writes" from the getters/setters to the implicit property.
+   */
+  emitImplicitProperty(
+      decl: ts.GetAccessorDeclaration|ts.SetAccessorDeclaration, anchor: VName,
+      funcVName: VName) {
+    // Remove trailing ":getter"/":setter" suffix
+    const propSignature = funcVName.signature.split(':').slice(0, -1).join(':');
+    const implicitProp = {...funcVName, signature: propSignature};
+
+    this.emitNode(implicitProp, 'variable');
+    this.emitFact(implicitProp, 'subkind', 'implicit');
+    this.emitEdge(anchor, 'defines/binding', implicitProp);
+
+    const sym = this.getSymbolAtLocation(decl.name);
+    if (!sym) throw new Error('Getter/setter declaration has no symbols.');
+
+    if (sym.declarations.find(ts.isGetAccessor)) {
+      // Emit a "property/reads" edge between the getter and the property
+      const getter = this.getSymbolName(sym, TSNamespace.VALUE, Context.Getter);
+      this.emitEdge(getter, 'property/reads', implicitProp);
+    }
+    if (sym.declarations.find(ts.isSetAccessor)) {
+      // Emit a "property/writes" edge between the setter and the property
+      const setter = this.getSymbolName(sym, TSNamespace.VALUE, Context.Setter);
+      this.emitEdge(setter, 'property/writes', implicitProp);
+    }
+  }
+
+  /**
    * Handles code like:
    *   export default ...;
    *   export = ...;
@@ -783,6 +868,12 @@
   visitFunctionLikeDeclaration(decl: ts.FunctionLikeDeclaration) {
     this.visitDecorators(decl.decorators || []);
     let kFunc: VName|undefined = undefined;
+    let context: Context|undefined = undefined;
+    if (ts.isGetAccessor(decl)) {
+      context = Context.Getter;
+    } else if (ts.isSetAccessor(decl)) {
+      context = Context.Setter;
+    }
     if (decl.name) {
       const sym = this.getSymbolAtLocation(decl.name);
       if (decl.name.kind === ts.SyntaxKind.ComputedPropertyName) {
@@ -796,10 +887,19 @@
               `function declaration ${decl.name.getText()} has no symbol`);
           return;
         }
-        kFunc = this.getSymbolName(sym, TSNamespace.VALUE);
-        this.emitNode(kFunc, 'function');
+        kFunc = this.getSymbolName(sym, TSNamespace.VALUE, context);
 
-        this.emitEdge(this.newAnchor(decl.name), 'defines/binding', kFunc);
+        const declAnchor = this.newAnchor(decl.name);
+        this.emitNode(kFunc, 'function');
+        this.emitEdge(declAnchor, 'defines/binding', kFunc);
+
+        // Getters/setters also emit an implicit class property entry. If a
+        // getter is present, it will bind this entry; otherwise a setter will.
+        if (ts.isGetAccessor(decl) ||
+            (ts.isSetAccessor(decl) &&
+             !sym.declarations.find(ts.isGetAccessor))) {
+          this.emitImplicitProperty(decl, declAnchor, kFunc);
+        }
 
         this.visitJSDoc(decl, kFunc);
       }
@@ -929,6 +1029,20 @@
     this.emitEdge(this.newAnchor(decl.name), 'defines/binding', kMember);
   }
 
+  visitExpressionMember(node: ts.Node) {
+    const sym = this.getSymbolAtLocation(node);
+    if (!sym) {
+      // E.g. a field of an "any".
+      return;
+    }
+    if (!sym.declarations || sym.declarations.length === 0) {
+      // An undeclared symbol, e.g. "undefined".
+      return;
+    }
+    const name = this.getSymbolName(sym, TSNamespace.VALUE);
+    this.emitEdge(this.newAnchor(node), 'ref', name);
+  }
+
   /**
    * visitJSDoc attempts to attach a 'doc' node to a given target, by looking
    * for JSDoc comments.
@@ -984,6 +1098,8 @@
       case ts.SyntaxKind.FunctionDeclaration:
       case ts.SyntaxKind.MethodDeclaration:
       case ts.SyntaxKind.MethodSignature:
+      case ts.SyntaxKind.GetAccessor:
+      case ts.SyntaxKind.SetAccessor:
         return this.visitFunctionLikeDeclaration(
             node as ts.FunctionLikeDeclaration);
       case ts.SyntaxKind.ClassDeclaration:
@@ -1005,17 +1121,7 @@
         // Assume that this identifer is occurring as part of an
         // expression; we handle identifiers that occur in other
         // circumstances (e.g. in a type) separately in visitType.
-        const sym = this.getSymbolAtLocation(node);
-        if (!sym) {
-          // E.g. a field of an "any".
-          return;
-        }
-        if (!sym.declarations || sym.declarations.length === 0) {
-          // An undeclared symbol, e.g. "undefined".
-          return;
-        }
-        const name = this.getSymbolName(sym, TSNamespace.VALUE);
-        this.emitEdge(this.newAnchor(node), 'ref', name);
+        this.visitExpressionMember(node);
         return;
       default:
         // Use default recursive processing.
diff --git a/kythe/typescript/testdata/getter_setter.ts b/kythe/typescript/testdata/getter_setter.ts
new file mode 100644
index 0000000..863fb77
--- /dev/null
+++ b/kythe/typescript/testdata/getter_setter.ts
@@ -0,0 +1,72 @@
+// Tests the behavior of getter and setter entries.
+
+// Both getters and setters
+class A {
+  prop = 0;
+
+  //- @foo defines/binding PropFoo=VName("A.foo", _, _, _, _)
+  //- PropFoo.node/kind variable
+  //- PropFoo.subkind implicit
+  //- @foo defines/binding GetFoo=VName("A.foo:getter", _, _, _, _)
+  //- GetFoo.node/kind function
+  //- GetFoo property/reads PropFoo
+  get foo() {
+    return this.prop;
+  }
+
+  //- @foo defines/binding SetFoo=VName("A.foo:setter", _, _, _, _)
+  //- SetFoo.node/kind function
+  //- SetFoo property/writes PropFoo
+  set foo(nFoo) {
+    this.prop = nFoo;
+  }
+
+  method() {
+    //- @foo ref GetFoo
+    this.foo;
+    //- @foo ref GetFoo
+    this.foo = 0;
+  }
+}
+
+// Only getters
+class B {
+  iProp = 0;
+
+  //- @prop defines/binding PropProp=VName("B.prop", _, _, _, _)
+  //- PropProp.node/kind variable
+  //- PropProp.subkind implicit
+  //- @prop defines/binding GetProp=VName("B.prop:getter", _, _, _, _)
+  //- GetProp.node/kind function
+  //- GetProp property/reads PropProp
+  get prop() {
+    return this.iProp;
+  }
+
+  method() {
+    //- @prop ref GetProp
+    this.prop;
+  }
+}
+
+// Only setters
+class C {
+  prop = 0;
+
+  //- @mem defines/binding PropMem=VName("C.mem", _, _, _, _)
+  //- PropMem.node/kind variable
+  //- PropMem.subkind implicit
+  //- @mem defines/binding SetMem=VName("C.mem:setter", _, _, _, _)
+  //- SetMem.node/kind function
+  //- SetMem property/writes PropMem
+  set mem(nMem) {
+    this.prop = nMem;
+  }
+
+  method() {
+    //- @mem ref SetMem
+    this.mem;
+    //- @mem ref SetMem
+    this.mem = 0;
+  }
+}