From 604e5fca55333857d08eba105cfca97f98b53b6b Mon Sep 17 00:00:00 2001 From: Zzzen Date: Tue, 10 May 2022 06:57:27 +0800 Subject: [PATCH] Add JSDoc's @inheritDoc Support for Static Class Members for TypeScript (#46719) * Add JSDoc's @inheritDoc Support for Static Class Members for TypeScript * use public api * fix * add tests * simplify implementation * extract comments from inherticDoc --- src/services/jsDoc.ts | 8 +- src/services/services.ts | 10 +- .../reference/quickInfoInheritDoc.baseline | 418 ++++++++++++++++++ .../reference/quickInfoInheritDoc2.baseline | 96 ++++ .../reference/quickInfoInheritDoc3.baseline | 84 ++++ tests/cases/fourslash/jsDocInheritDoc.ts | 2 +- tests/cases/fourslash/quickInfoInheritDoc.ts | 65 +++ tests/cases/fourslash/quickInfoInheritDoc2.ts | 22 + tests/cases/fourslash/quickInfoInheritDoc3.ts | 23 + 9 files changed, 721 insertions(+), 7 deletions(-) create mode 100644 tests/baselines/reference/quickInfoInheritDoc.baseline create mode 100644 tests/baselines/reference/quickInfoInheritDoc2.baseline create mode 100644 tests/baselines/reference/quickInfoInheritDoc3.baseline create mode 100644 tests/cases/fourslash/quickInfoInheritDoc.ts create mode 100644 tests/cases/fourslash/quickInfoInheritDoc2.ts create mode 100644 tests/cases/fourslash/quickInfoInheritDoc3.ts diff --git a/src/services/jsDoc.ts b/src/services/jsDoc.ts index ca6b9cc047d..e85246e9eb5 100644 --- a/src/services/jsDoc.ts +++ b/src/services/jsDoc.ts @@ -93,11 +93,12 @@ namespace ts.JsDoc { const parts: SymbolDisplayPart[][] = []; forEachUnique(declarations, declaration => { for (const jsdoc of getCommentHavingNodes(declaration)) { + const inheritDoc = isJSDoc(jsdoc) && jsdoc.tags && find(jsdoc.tags, t => t.kind === SyntaxKind.JSDocTag && (t.tagName.escapedText === "inheritDoc" || t.tagName.escapedText === "inheritdoc")); // skip comments containing @typedefs since they're not associated with particular declarations // Exceptions: // - @typedefs are themselves declarations with associated comments // - @param or @return indicate that the author thinks of it as a 'local' @typedef that's part of the function documentation - if (jsdoc.comment === undefined + if (jsdoc.comment === undefined && !inheritDoc || isJSDoc(jsdoc) && declaration.kind !== SyntaxKind.JSDocTypedefTag && declaration.kind !== SyntaxKind.JSDocCallbackTag && jsdoc.tags @@ -105,7 +106,10 @@ namespace ts.JsDoc { && !jsdoc.tags.some(t => t.kind === SyntaxKind.JSDocParameterTag || t.kind === SyntaxKind.JSDocReturnTag)) { continue; } - const newparts = getDisplayPartsFromComment(jsdoc.comment, checker); + let newparts = jsdoc.comment ? getDisplayPartsFromComment(jsdoc.comment, checker) : []; + if (inheritDoc && inheritDoc.comment) { + newparts = newparts.concat(getDisplayPartsFromComment(inheritDoc.comment, checker)); + } if (!contains(parts, newparts, isIdenticalListOfDisplayParts)) { parts.push(newparts); } diff --git a/src/services/services.ts b/src/services/services.ts index ac731115beb..afb6a108691 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -592,7 +592,7 @@ namespace ts { * @returns `true` if `node` has a JSDoc "inheritDoc" tag on it, otherwise `false`. */ function hasJSDocInheritDocTag(node: Node) { - return getJSDocTags(node).some(tag => tag.tagName.text === "inheritDoc"); + return getJSDocTags(node).some(tag => tag.tagName.text === "inheritDoc" || tag.tagName.text === "inheritdoc"); } function getJsDocTagsOfDeclarations(declarations: Declaration[] | undefined, checker: TypeChecker | undefined): JSDocTagInfo[] { @@ -643,13 +643,15 @@ namespace ts { } function findBaseOfDeclaration(checker: TypeChecker, declaration: Declaration, cb: (symbol: Symbol) => T[] | undefined): T[] | undefined { - if (hasStaticModifier(declaration)) return; - const classOrInterfaceDeclaration = declaration.parent?.kind === SyntaxKind.Constructor ? declaration.parent.parent : declaration.parent; if (!classOrInterfaceDeclaration) return; + const isStaticMember = hasStaticModifier(declaration); return firstDefined(getAllSuperTypeNodes(classOrInterfaceDeclaration), superTypeNode => { - const symbol = checker.getPropertyOfType(checker.getTypeAtLocation(superTypeNode), declaration.symbol.name); + const baseType = checker.getTypeAtLocation(superTypeNode); + const symbol = isStaticMember + ? find(checker.getExportsOfModule(baseType.symbol), s => s.escapedName === declaration.symbol.name) + : checker.getPropertyOfType(baseType, declaration.symbol.name); return symbol ? cb(symbol) : undefined; }); } diff --git a/tests/baselines/reference/quickInfoInheritDoc.baseline b/tests/baselines/reference/quickInfoInheritDoc.baseline new file mode 100644 index 00000000000..29cc8e3544d --- /dev/null +++ b/tests/baselines/reference/quickInfoInheritDoc.baseline @@ -0,0 +1,418 @@ +[ + { + "marker": { + "fileName": "/tests/cases/fourslash/quickInfoInheritDoc.ts", + "position": 817, + "name": "1" + }, + "quickInfo": { + "kind": "method", + "kindModifiers": "public,static", + "textSpan": { + "start": 817, + "length": 17 + }, + "displayParts": [ + { + "text": "(", + "kind": "punctuation" + }, + { + "text": "method", + "kind": "text" + }, + { + "text": ")", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "SubClass", + "kind": "className" + }, + { + "text": ".", + "kind": "punctuation" + }, + { + "text": "doSomethingUseful", + "kind": "methodName" + }, + { + "text": "(", + "kind": "punctuation" + }, + { + "text": "mySpecificStuff", + "kind": "parameterName" + }, + { + "text": "?", + "kind": "punctuation" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "{", + "kind": "punctuation" + }, + { + "text": "\n", + "kind": "lineBreak" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "tiger", + "kind": "propertyName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "string", + "kind": "keyword" + }, + { + "text": ";", + "kind": "punctuation" + }, + { + "text": "\n", + "kind": "lineBreak" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "lion", + "kind": "propertyName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "string", + "kind": "keyword" + }, + { + "text": ";", + "kind": "punctuation" + }, + { + "text": "\n", + "kind": "lineBreak" + }, + { + "text": "}", + "kind": "punctuation" + }, + { + "text": ")", + "kind": "punctuation" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "string", + "kind": "keyword" + } + ], + "documentation": [ + { + "text": "Useful description always applicable", + "kind": "text" + } + ], + "tags": [ + { + "name": "returns", + "text": [ + { + "text": "Useful description of return value always applicable.", + "kind": "text" + } + ] + }, + { + "name": "inheritDoc" + }, + { + "name": "param", + "text": [ + { + "text": "mySpecificStuff", + "kind": "parameterName" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "Description of my specific parameter.", + "kind": "text" + } + ] + } + ] + } + }, + { + "marker": { + "fileName": "/tests/cases/fourslash/quickInfoInheritDoc.ts", + "position": 1143, + "name": "2" + }, + "quickInfo": { + "kind": "method", + "kindModifiers": "public,static", + "textSpan": { + "start": 1143, + "length": 5 + }, + "displayParts": [ + { + "text": "(", + "kind": "punctuation" + }, + { + "text": "method", + "kind": "text" + }, + { + "text": ")", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "SubClass", + "kind": "className" + }, + { + "text": ".", + "kind": "punctuation" + }, + { + "text": "func1", + "kind": "methodName" + }, + { + "text": "(", + "kind": "punctuation" + }, + { + "text": "stuff1", + "kind": "parameterName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "any", + "kind": "keyword" + }, + { + "text": ")", + "kind": "punctuation" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "void", + "kind": "keyword" + } + ], + "documentation": [ + { + "text": "BaseClass.func1", + "kind": "text" + } + ], + "tags": [ + { + "name": "param", + "text": [ + { + "text": "stuff1", + "kind": "parameterName" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "BaseClass.func1.stuff1", + "kind": "text" + } + ] + }, + { + "name": "returns", + "text": [ + { + "text": "BaseClass.func1.returns", + "kind": "text" + } + ] + }, + { + "name": "inheritDoc" + }, + { + "name": "param", + "text": [ + { + "text": "stuff1", + "kind": "parameterName" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "SubClass.func1.stuff1", + "kind": "text" + } + ] + }, + { + "name": "returns", + "text": [ + { + "text": "SubClass.func1.returns", + "kind": "text" + } + ] + } + ] + } + }, + { + "marker": { + "fileName": "/tests/cases/fourslash/quickInfoInheritDoc.ts", + "position": 1282, + "name": "3" + }, + "quickInfo": { + "kind": "property", + "kindModifiers": "public,static", + "textSpan": { + "start": 1282, + "length": 12 + }, + "displayParts": [ + { + "text": "(", + "kind": "punctuation" + }, + { + "text": "property", + "kind": "text" + }, + { + "text": ")", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "SubClass", + "kind": "className" + }, + { + "text": ".", + "kind": "punctuation" + }, + { + "text": "someProperty", + "kind": "propertyName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "string", + "kind": "keyword" + } + ], + "documentation": [ + { + "text": "Applicable description always.", + "kind": "text" + }, + { + "text": "\n", + "kind": "lineBreak" + }, + { + "text": "text over tag", + "kind": "text" + }, + { + "text": "text after tag", + "kind": "text" + } + ], + "tags": [ + { + "name": "inheritDoc", + "text": [ + { + "text": "text after tag", + "kind": "text" + } + ] + } + ] + } + } +] \ No newline at end of file diff --git a/tests/baselines/reference/quickInfoInheritDoc2.baseline b/tests/baselines/reference/quickInfoInheritDoc2.baseline new file mode 100644 index 00000000000..40dc3368efd --- /dev/null +++ b/tests/baselines/reference/quickInfoInheritDoc2.baseline @@ -0,0 +1,96 @@ +[ + { + "marker": { + "fileName": "/tests/cases/fourslash/quickInfoInheritDoc2.ts", + "position": 173, + "name": "1" + }, + "quickInfo": { + "kind": "property", + "kindModifiers": "", + "textSpan": { + "start": 173, + "length": 4 + }, + "displayParts": [ + { + "text": "(", + "kind": "punctuation" + }, + { + "text": "property", + "kind": "text" + }, + { + "text": ")", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "SubClass", + "kind": "className" + }, + { + "text": "<", + "kind": "punctuation" + }, + { + "text": "T", + "kind": "typeParameterName" + }, + { + "text": ">", + "kind": "punctuation" + }, + { + "text": ".", + "kind": "punctuation" + }, + { + "text": "prop", + "kind": "propertyName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "T", + "kind": "typeParameterName" + } + ], + "documentation": [ + { + "text": "Base.prop", + "kind": "text" + }, + { + "text": "\n", + "kind": "lineBreak" + }, + { + "text": "SubClass.prop", + "kind": "text" + } + ], + "tags": [ + { + "name": "inheritdoc", + "text": [ + { + "text": "SubClass.prop", + "kind": "text" + } + ] + } + ] + } + } +] \ No newline at end of file diff --git a/tests/baselines/reference/quickInfoInheritDoc3.baseline b/tests/baselines/reference/quickInfoInheritDoc3.baseline new file mode 100644 index 00000000000..5729c6c1ccf --- /dev/null +++ b/tests/baselines/reference/quickInfoInheritDoc3.baseline @@ -0,0 +1,84 @@ +[ + { + "marker": { + "fileName": "/tests/cases/fourslash/quickInfoInheritDoc3.ts", + "position": 237, + "name": "1" + }, + "quickInfo": { + "kind": "property", + "kindModifiers": "", + "textSpan": { + "start": 237, + "length": 4 + }, + "displayParts": [ + { + "text": "(", + "kind": "punctuation" + }, + { + "text": "property", + "kind": "text" + }, + { + "text": ")", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "SubClass", + "kind": "className" + }, + { + "text": ".", + "kind": "punctuation" + }, + { + "text": "prop", + "kind": "propertyName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "string", + "kind": "keyword" + } + ], + "documentation": [ + { + "text": "Base.prop", + "kind": "text" + }, + { + "text": "\n", + "kind": "lineBreak" + }, + { + "text": "SubClass.prop", + "kind": "text" + } + ], + "tags": [ + { + "name": "inheritdoc", + "text": [ + { + "text": "SubClass.prop", + "kind": "text" + } + ] + } + ] + } + } +] \ No newline at end of file diff --git a/tests/cases/fourslash/jsDocInheritDoc.ts b/tests/cases/fourslash/jsDocInheritDoc.ts index 7ef822ce653..08da10537c8 100644 --- a/tests/cases/fourslash/jsDocInheritDoc.ts +++ b/tests/cases/fourslash/jsDocInheritDoc.ts @@ -58,7 +58,7 @@ verify.quickInfoAt("1", "constructor Bar(value: number): Bar", undefined); // constructors aren't actually inherited verify.quickInfoAt("2", "(method) Bar.method2(): void", "Foo#method2 documentation"); // use inherited docs only -verify.quickInfoAt("3", "(method) Bar.method1(): void", undefined); // statics aren't actually inherited +verify.quickInfoAt("3", "(method) Bar.method1(): void", 'Foo#method1 documentation'); // use inherited docs too verify.quickInfoAt("4", "(property) Bar.property1: string", "Foo#property1 documentation"); // use inherited docs only verify.quickInfoAt("5", "(property) Bar.property2: object", "Baz#property2 documentation\nBar#property2"); // include local and inherited docs verify.quickInfoAt("6", "(property) Bar.property3: string", undefined); diff --git a/tests/cases/fourslash/quickInfoInheritDoc.ts b/tests/cases/fourslash/quickInfoInheritDoc.ts new file mode 100644 index 00000000000..a08f60e98ab --- /dev/null +++ b/tests/cases/fourslash/quickInfoInheritDoc.ts @@ -0,0 +1,65 @@ +/// + +// @noEmit: true +// @allowJs: true + +// @Filename: quickInfoInheritDoc.ts +////abstract class BaseClass { +//// /** +//// * Useful description always applicable +//// * +//// * @returns {string} Useful description of return value always applicable. +//// */ +//// public static doSomethingUseful(stuff?: any): string { +//// throw new Error('Must be implemented by subclass'); +//// } +//// +//// /** +//// * BaseClass.func1 +//// * @param {any} stuff1 BaseClass.func1.stuff1 +//// * @returns {void} BaseClass.func1.returns +//// */ +//// public static func1(stuff1: any): void { +//// } +//// +//// /** +//// * Applicable description always. +//// */ +//// public static readonly someProperty: string = 'general value'; +////} +//// +//// +//// +//// +////class SubClass extends BaseClass { +//// +//// /** +//// * @inheritDoc +//// * +//// * @param {{ tiger: string; lion: string; }} [mySpecificStuff] Description of my specific parameter. +//// */ +//// public static /*1*/doSomethingUseful(mySpecificStuff?: { tiger: string; lion: string; }): string { +//// let useful = ''; +//// +//// // do something useful to useful +//// +//// return useful; +//// } +//// +//// /** +//// * @inheritDoc +//// * @param {any} stuff1 SubClass.func1.stuff1 +//// * @returns {void} SubClass.func1.returns +//// */ +//// public static /*2*/func1(stuff1: any): void { +//// } +//// +//// /** +//// * text over tag +//// * @inheritDoc +//// * text after tag +//// */ +//// public static readonly /*3*/someProperty: string = 'specific to this class value' +////} + +verify.baselineQuickInfo(); \ No newline at end of file diff --git a/tests/cases/fourslash/quickInfoInheritDoc2.ts b/tests/cases/fourslash/quickInfoInheritDoc2.ts new file mode 100644 index 00000000000..8410c4dddf4 --- /dev/null +++ b/tests/cases/fourslash/quickInfoInheritDoc2.ts @@ -0,0 +1,22 @@ +/// + +// @noEmit: true +// @allowJs: true + +// @Filename: quickInfoInheritDoc2.ts +////class Base { +//// /** +//// * Base.prop +//// */ +//// prop: T | undefined; +////} +//// +////class SubClass extends Base { +//// /** +//// * @inheritdoc +//// * SubClass.prop +//// */ +//// /*1*/prop: T | undefined; +////} + +verify.baselineQuickInfo(); \ No newline at end of file diff --git a/tests/cases/fourslash/quickInfoInheritDoc3.ts b/tests/cases/fourslash/quickInfoInheritDoc3.ts new file mode 100644 index 00000000000..82ebe5399db --- /dev/null +++ b/tests/cases/fourslash/quickInfoInheritDoc3.ts @@ -0,0 +1,23 @@ +/// + +// @noEmit: true +// @allowJs: true + +// @Filename: quickInfoInheritDoc3.ts +////function getBaseClass() { +//// return class Base { +//// /** +//// * Base.prop +//// */ +//// prop: string | undefined; +//// } +////} +////class SubClass extends getBaseClass() { +//// /** +//// * @inheritdoc +//// * SubClass.prop +//// */ +//// /*1*/prop: string | undefined; +////} + +verify.baselineQuickInfo(); \ No newline at end of file