feat(typescript_indexer): VName schema compliance for constructors (#3785)
* add constructor spec impl
* Ensure members declared in a ctor are scoped to the class. Add tests for this.
* Remove getter/setter code and conflicts
* Remove comment artifact, make class properties emit "childof" entries of the class.
* parameters in constructors and functions are children of those functions
* use more idiomatic type check
diff --git a/kythe/typescript/indexer.ts b/kythe/typescript/indexer.ts
index 50fd4ac..6068856 100644
--- a/kythe/typescript/indexer.ts
+++ b/kythe/typescript/indexer.ts
@@ -200,6 +200,28 @@
}
/**
+ * Determines whether a parameter in a constructor is a class member. Applies
+ * to class properties defined with the shorthand
+ * constructor(private member: string)
+ */
+ isClassMemberInCtor(node: ts.Node, ctor: ts.Node): boolean {
+ return node.kind === ts.SyntaxKind.Parameter &&
+ node.parent === ctor && // has to be in this ctor
+ node.modifiers !== undefined &&
+ (node.modifiers.find(
+ m => m.kind === ts.SyntaxKind.PrivateKeyword ||
+ m.kind === ts.SyntaxKind.PublicKeyword) !== undefined);
+ }
+
+ /**
+ * Determines if a node is a class or interface.
+ */
+ isClassOrInterface(node: ts.Node): boolean {
+ return ts.isClassDeclaration(node) || ts.isClassExpression(node) ||
+ ts.isInterfaceDeclaration(node);
+ }
+
+ /**
* newFileVName returns a new VName for the given file path.
*/
newFileVName(path: string): VName {
@@ -343,7 +365,11 @@
}
break;
case ts.SyntaxKind.Constructor:
- parts.push('constructor');
+ // Class members declared with a shorthand in the constructor should
+ // be scoped to the class, not the constructor.
+ if (!this.isClassMemberInCtor(startNode, node)) {
+ parts.push('constructor');
+ }
break;
case ts.SyntaxKind.ModuleDeclaration:
const modDecl = node as ts.ModuleDeclaration;
@@ -448,6 +474,11 @@
if (ns === TSNamespace.TYPE) {
vname.signature += '#type';
}
+ // The signature of a class declaration value is its constructor, as the
+ // constructor has more semantic meaning.
+ if ((sym.flags & ts.SymbolFlags.Class) && ns === TSNamespace.VALUE) {
+ vname.signature += ':ctor';
+ }
if (!contextApplies) {
// Save it in the appropriate slot in the symbolNames table.
@@ -582,6 +613,17 @@
}
/**
+ * Returns the location of a text in the source code of a node.
+ */
+ getTextSpan(node: ts.Node, text: string): {start: number, end: number} {
+ const ofs = node.getText().indexOf(text);
+ if (ofs < 0) throw new Error(`${text} not found in ${node.getText()}`);
+ const start = node.getStart() + ofs;
+ const end = start + text.length;
+ return {start, end};
+ }
+
+ /**
* visitImportSpecifier handles a single entry in an import statement, e.g.
* "bar" in code like
* import {foo, bar} from 'baz';
@@ -718,6 +760,25 @@
}
/**
+ * Emits a "childof" edge on class/interface members. Takes the Parent node
+ * and the VName of the node that is its child.
+ */
+ emitChildOf(
+ vname: VName, parent: ts.Node,
+ namespace: TSNamespace = TSNamespace.TYPE) {
+ const parentName = (parent as ts.ClassLikeDeclaration).name;
+ if (parentName !== undefined) {
+ const parentSym = this.getSymbolAtLocation(parentName);
+ if (!parentSym) {
+ this.todo(parentName, `parent ${parentName} has no symbol`);
+ return;
+ }
+ const kParent = this.getSymbolName(parentSym, namespace);
+ this.emitEdge(vname, 'childof', kParent);
+ }
+ }
+
+ /**
* 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"
@@ -764,10 +825,8 @@
// So instead we link the keyword "default" itself to the VName.
// The TypeScript AST does not expose the location of the 'default'
// keyword so we just find it in the source text to link it.
- const ofs = assign.getText().indexOf('default');
- if (ofs < 0) throw new Error(`'export default' without 'default'?`);
- const start = assign.getStart() + ofs;
- const anchor = this.newAnchor(assign, start, start + 'default'.length);
+ const span = this.getTextSpan(assign, 'default');
+ const anchor = this.newAnchor(assign, span.start, span.end);
this.emitEdge(anchor, 'defines/binding', this.scopedSignature(assign));
}
}
@@ -824,8 +883,8 @@
* the decl parameter is the union of the attributes of the two types.
* @return the generated VName for the declaration, if any.
*/
- visitVariableDeclaration(decl: {
- name: ts.BindingName|ts.PropertyName,
+ visitVariableDeclaration(decl: ts.Node&{
+ name: ts.BindingName | ts.PropertyName,
type?: ts.TypeNode,
initializer?: ts.Expression, kind: ts.SyntaxKind,
}): VName|undefined {
@@ -862,6 +921,10 @@
this.emitFact(vname, 'tag/static', '');
}
}
+ if (vname && this.isClassOrInterface(decl.parent)) {
+ // Emit a "childof" edge on class/interface members.
+ this.emitChildOf(vname, decl.parent);
+ }
return vname;
}
@@ -911,22 +974,9 @@
this.emitEdge(this.newAnchor(decl), 'defines', kFunc);
}
- if (kFunc && decl.parent) {
+ if (kFunc && this.isClassOrInterface(decl.parent)) {
// Emit a "childof" edge on class/interface members.
- if (decl.parent.kind === ts.SyntaxKind.ClassDeclaration ||
- decl.parent.kind === ts.SyntaxKind.ClassExpression ||
- decl.parent.kind === ts.SyntaxKind.InterfaceDeclaration) {
- const parentName = (decl.parent as ts.ClassLikeDeclaration).name;
- if (parentName !== undefined) {
- const parentSym = this.getSymbolAtLocation(parentName);
- if (!parentSym) {
- this.todo(parentName, `parent ${parentName} has no symbol`);
- return;
- }
- const kParent = this.getSymbolName(parentSym, TSNamespace.TYPE);
- this.emitEdge(kFunc, 'childof', kParent);
- }
- }
+ this.emitChildOf(kFunc, decl.parent);
// TODO: emit an "overrides" edge if this method overrides.
// It appears the TS API doesn't make finding that information easy,
@@ -948,6 +998,20 @@
this.emitNode(kParam, 'variable');
if (kFunc) this.emitEdge(kFunc, `param.${index}`, kParam);
+ if (this.isClassMemberInCtor(param, decl)) {
+ // Class members defined in the parameters of a constructor are children
+ // of the class.
+ this.emitChildOf(kParam, decl.parent);
+ } else if (ts.isConstructorDeclaration(decl)) {
+ // Other parameters of a constructor should be children of the
+ // constructor. The constructor is the value binding of the class.
+ this.emitChildOf(kParam, decl.parent, TSNamespace.VALUE);
+ } else {
+ // All other parameters on functions are just children of that function.
+ this.emitChildOf(kParam, decl, TSNamespace.VALUE);
+ }
+
+
this.emitEdge(this.newAnchor(param.name), 'defines/binding', kParam);
if (param.type) this.visitType(param.type);
@@ -985,16 +1049,24 @@
// instances of the class) and a value (the constructor).
const kClass = this.getSymbolName(sym, TSNamespace.TYPE);
this.emitNode(kClass, 'record');
- const kClassCtor = this.getSymbolName(sym, TSNamespace.VALUE);
- this.emitNode(kClassCtor, 'function');
- // TODO: the specific constructor() should really be the thing tagged
- // with constructor, but we also need to handle classes that don't declare
- // a constructor. Fix me later.
- this.emitFact(kClassCtor, 'subkind', 'constructor');
+ const classAnchor = this.newAnchor(decl.name);
+ this.emitEdge(classAnchor, 'defines/binding', kClass);
- const anchor = this.newAnchor(decl.name);
- this.emitEdge(anchor, 'defines/binding', kClass);
- this.emitEdge(anchor, 'defines/binding', kClassCtor);
+ const kClassCtor = this.getSymbolName(sym, TSNamespace.VALUE);
+ let classCtorAnchor;
+ const ctor = sym.members!.get(ts.InternalSymbolName.Constructor);
+ if (ctor) {
+ const decl = ctor.declarations[0];
+ const span = this.getTextSpan(decl, 'constructor');
+ classCtorAnchor = this.newAnchor(decl, span.start, span.end);
+ } else {
+ // No constructor on the class, so just point the constructor to the
+ // class identifier.
+ classCtorAnchor = classAnchor;
+ }
+ this.emitNode(kClassCtor, 'function');
+ this.emitFact(kClassCtor, 'subkind', 'constructor');
+ this.emitEdge(classCtorAnchor, 'defines/binding', kClassCtor);
this.visitJSDoc(decl, kClass);
}
diff --git a/kythe/typescript/testdata/class.ts b/kythe/typescript/testdata/class.ts
index c5d4505..9ed33b4 100644
--- a/kythe/typescript/testdata/class.ts
+++ b/kythe/typescript/testdata/class.ts
@@ -19,39 +19,45 @@
//- @Class defines/binding Class
//- Class.node/kind record
-//- @Class defines/binding ClassCtor
-//- ClassCtor.node/kind function
-//- ClassCtor.subkind constructor
//- @IFace ref IFace
class Class implements IFace {
//- @member defines/binding Member
//- Member.node/kind variable
+ //- Member childof Class
//- !{ Member.tag/static _ }
member: number;
//- @staticMember defines/binding StaticMember
//- StaticMember.tag/static _
+ //- StaticMember childof Class
static staticMember: number;
- // TODO: ClassCtor should really point at this constructor, not at the
- // top-level class declaration.
// This ctor declares a new member var named 'otherMember', and also
// declares an ordinary parameter named 'member' (to ensure we don't get
// confused about params to the ctor vs true member variables).
+ //- @constructor defines/binding ClassCtor
+ //- ClassCtor.node/kind function
+ //- ClassCtor.subkind constructor
//- @otherMember defines/binding OtherMember
//- OtherMember.node/kind variable
+ //- OtherMember childof Class
+ //- !{ OtherMember childof ClassCtor }
//- @member defines/binding FakeMember
//- FakeMember.node/kind variable
+ //- FakeMember childof ClassCtor
+ //- !{ FakeMember childof Class }
constructor(public otherMember: number, member: string) {}
//- @method defines/binding Method
//- Method.node/kind function
//- Method childof Class
- method() {
+ //- @param defines/binding MethodParam
+ //- MethodParam childof Method
+ method(param: number) {
//- @member ref Member
this.member;
//- @method ref Method
- this.method();
+ this.method(param);
}
// TODO: ensure the method is linked to the interface too.
@@ -59,11 +65,21 @@
ifaceMethod(): void {}
}
+// A class without a constructor binds the constructor to its declaration.
//- @Class ref ClassCtor
+//- @Subclass defines/binding SubClassCtor
+//- SubClassCtor.node/kind function
+//- SubClassCtor.subkind constructor
class Subclass extends Class {
method() {
//- @member ref Member
this.member;
+
+ //- @Class ref ClassCtor
+ new Class(0, '');
+
+ //- @Class ref Class
+ type CC = Class;
}
}