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;
+ }
+}