-
Notifications
You must be signed in to change notification settings - Fork 12.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use documentation comments from inherited properties when @inheritDoc is present #18804
Conversation
@Andy-MS can you please review this change. |
For posterity: rebased on fcb48dd and re-ran |
df96aba
to
6ff0aed
Compare
src/services/services.ts
Outdated
if (this.documentationComment === undefined) { | ||
this.documentationComment = this.declaration ? JsDoc.getJsDocCommentsFromDeclarations([this.declaration]) : []; | ||
|
||
if (this.declaration && this.documentationComment.length === 0 && hasJSDocInheritDocTag(this) && typeChecker) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might whole block might be neater as if (this.declaration) { ... } else { this.documentationComment = []; }
src/services/services.ts
Outdated
* @returns `true` if `symbol` has a JSDoc "inheritDoc" tag on it, otherwise `false`. | ||
*/ | ||
function hasJSDocInheritDocTag(symbol: Signature | Symbol) { | ||
return !!find(symbol.getJsDocTags(), tag => tag.name === "inheritDoc"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
symbol.getJsDocTags().some(...)
src/services/jsDoc.ts
Outdated
@@ -70,7 +71,7 @@ namespace ts.JsDoc { | |||
const tags: JSDocTagInfo[] = []; | |||
forEachUnique(declarations, declaration => { | |||
for (const tag of getJSDocTags(declaration)) { | |||
if (tag.kind === SyntaxKind.JSDocTag) { | |||
if (tag.kind === SyntaxKind.JSDocTag || tag.kind === SyntaxKind.JSDocInheritDocTag) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't think you need to modify this function for what you want; use ts.getJSDocTags
(from compiler/utilities.ts
) instead, which returns tags of all kinds.
src/services/services.ts
Outdated
function findInheritedJSDocComments(declaration: Declaration, propertyName: string, typeChecker: TypeChecker): SymbolDisplayPart[] { | ||
let documentationComment: SymbolDisplayPart[] = []; | ||
|
||
if (isClassDeclaration(declaration.parent) || isInterfaceDeclaration(declaration.parent)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer an early return. Also, use isClassLike
.
src/services/services.ts
Outdated
let documentationComment: SymbolDisplayPart[] = []; | ||
|
||
if (isClassDeclaration(declaration.parent) || isInterfaceDeclaration(declaration.parent)) { | ||
const container: ClassDeclaration | InterfaceDeclaration = declaration.parent; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can just declare this outside (as a Node
) and rely on isClassDeclaration
and isInterfaceDeclaration
working as type predicates.
src/services/services.ts
Outdated
const container: ClassDeclaration | InterfaceDeclaration = declaration.parent; | ||
const baseTypeNode = getClassExtendsHeritageClauseElement(container); | ||
|
||
if (baseTypeNode) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Early return
src/services/services.ts
Outdated
|
||
if (isClassDeclaration(declaration.parent) || isInterfaceDeclaration(declaration.parent)) { | ||
const container: ClassDeclaration | InterfaceDeclaration = declaration.parent; | ||
const baseTypeNode = getClassExtendsHeritageClauseElement(container); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a function getBaseTypes
in checker.ts
that might be better suited. You could expose it internally by adding /* @internal */ getBaseTypes(): ReadonlyArray<BaseType>;
to interface TypeChecker
.
The advantages are that this gets you both classes and interfaces, and you don't have to call getTypeAtLocation
.
////const p1 = b.property1/*4*/; | ||
////const p2 = b.property2/*5*/; | ||
|
||
verify.baselineQuickInfo(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be breifer using just verify.quickInfoAt
-- baselineQuickInfo
is more so we can check all the displayParts
, but that's not what we're testing here.
What if we just inherited documentation by default without requiring users to add |
Thanks for the review, @Andy-MS! I'll get this fixed up today or tomorrow.
I think it'd be pretty intuitive to be honest. I'd considered doing that here but didn't want to take too big of a step 😃 Should be relatively simple to get working, so I'll switch to that approach. Thanks again! ❤️ |
6ff0aed
to
f3f6769
Compare
Got a few updates here based on your feedback!
I unfortunately don't see a way to get Thanks for your patience while I updated this 😄 |
OK, I was wrong about function findInheritedJSDocComments(declaration: Declaration, propertyName: string, typeChecker: TypeChecker): SymbolDisplayPart[] {
return flatMap(getAllSuperTypeNodes(declaration), superType => {
const baseProperty = typeChecker.getPropertyOfType(typeChecker.getTypeAtLocation(superType), propertyName);
return baseProperty && baseProperty.getDocumentationComment(typeChecker);
});
}
function getAllSuperTypeNodes(declaration: Declaration): ReadonlyArray<TypeNode> {
const container = declaration.parent;
if (!container || (!isClassDeclaration(container) && !isInterfaceDeclaration(container))) {
return emptyArray;
}
const extended = getClassExtendsHeritageClauseElement(container);
const types = extended ? [extended] : emptyArray;
return isClassLike(container) ? concatenate(types, getClassImplementsHeritageClauseElements(container)) : types;
} |
src/services/services.ts
Outdated
if (this.declarations) { | ||
this.documentationComment = JsDoc.getJsDocCommentsFromDeclarations(this.declarations); | ||
|
||
if (this.documentationComment.length === 0 || this.declarations.some(dec => hasJSDocInheritDocTag(dec))) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: dec => hasJSDocInheritDocTag(dec)
-- just use hasJSDocInheritDocTag
directly
src/services/services.ts
Outdated
if (this.declaration) { | ||
this.documentationComment = JsDoc.getJsDocCommentsFromDeclarations([this.declaration]); | ||
|
||
if (this.documentationComment.length === 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a case where I think we should use both inherited and own doc comments:
///<reference path="fourslash.ts" />
////interface I {
//// /** Interface doc */
//// m(): void;
////}
////
////class C implements I {
//// /**
//// * Class doc
//// * @inheritDoc
//// */
//// m/**/() {}
////}
verify.quickInfoAt("", "(method) C.m(): void", "Inferface doc\nClass doc");
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great point! I agree. This would make TypeScript's handling of @inheritDoc
different than vanilla JSDoc's though, which ignores documentation on anything with @inheritDoc
, e.g.:
/** @class */
class Foo {
/** Foo docs. */
test() {
}
}
/**
* @class
* @extends Foo
*/
class Bar {
/**
* Bar docs.
* @inheritDoc
*/
test() {
}
}
results in JSDoc for Bar#test
of "Foo docs" only.
That said, I think it's helpful for TypeScript users and I'd love to have it. As long as you're okay with differing from upstream here, I'm 100% fine with it 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, http://usejsdoc.org/tags-inheritdoc.html says:
The @inheritdoc tag indicates that a symbol should inherit its documentation from its parent class. Any other tags that you include in the JSDoc comment will be ignored.
This tag is provided for compatibility with Closure Compiler. By default, if you do not add a JSDoc comment to a symbol, the symbol will inherit documentation from its parent.
The presence of the @inheritdoc tag implies the presence of the @OverRide tag.
This makes it a rather strange tag -- one whose sole purpose is to make all documentation on the current node ignored! Presumably people who don't want to see that comment in quick info should just make it a regular comment and not a doc comment. I think it would be a more useful tag if it meant to add docs from a parent in addition to anything on the child.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes perfect sense to me. I've updated the PR to support that :)
src/compiler/parser.ts
Outdated
@@ -6380,6 +6380,9 @@ namespace ts { | |||
case "constructor": | |||
tag = parseClassTag(atToken, tagName); | |||
break; | |||
case "inheritDoc": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't seem to be needed as its own SyntaxKind
any more?
src/services/types.ts
Outdated
@@ -32,7 +32,7 @@ namespace ts { | |||
getEscapedName(): __String; | |||
getName(): string; | |||
getDeclarations(): Declaration[] | undefined; | |||
getDocumentationComment(): SymbolDisplayPart[]; | |||
getDocumentationComment(typeChecker?: TypeChecker): SymbolDisplayPart[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on #19455 (comment) maybe it would be better to just take a compile-time breaking change and make this typeChecker: TypeChecker | undefined
. @mhegazy
351dcbf
to
424606c
Compare
Well this is embarrassing, I forgot to update |
…is present The JSDoc `@ineheritDoc` [tag](http://usejsdoc.org/tags-inheritdoc.html) "indicates that a symbol should inherit its documentation from its parent class". In the case of a TypeScript file, this also includes implemented interfaces and parent interfaces. With this change, a class method or property (or an interface property) with the `@inheritDoc` tag in its JSDoc comment will automatically use the comments from its nearest ancestor that has no `@inheritDoc` tag. To prevent breaking backwards compatibility, `Symbol.getDocumentationComment` now accepts an optional `TypeChecker` instance to support this feature. fixes microsoft#8912
23ac1ed
to
5ba044a
Compare
Rebased on 1a7a587! Dang, |
Thanks! |
Thank you @sjbarag, this is super awesome! |
The JSDoc
@ineheritDoc
tag "indicates that a symbol should inherit its documentation from its parent class". In the case of a TypeScript file, this also includes implemented interfaces and parent interfaces.With this change, a class method or property (or an interface property) with the
@inheritDoc
tag in its JSDoc comment will automatically use the comments from its nearest ancestor that has no@inheritDoc
tag. To prevent breaking backwards compatibility,Symbol.getDocumentationComment
now accepts an optionalTypeChecker
instance to support this feature.fixes #8912