diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index aa503e5d8..708307afb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -22,8 +22,9 @@ labels: bug Note: Turn off skipErrorChecks before reporting a crash. Bug reports for crashes with that option on are out of scope. -If possible, please create a *minimal* repo reproducing your problem and link it. -You can easily do this by submitting a pull request to https://github.com/TypeStrong/typedoc-repros +If possible, please create a *minimal* repo reproducing your problem. +If it is more than a single small file, please submit a pull request to +https://github.com/TypeStrong/typedoc-repros which changes the files necessary to reproduce your bug. If this is not possible, include at least: diff --git a/CHANGELOG.md b/CHANGELOG.md index 9add42f68..749f2f535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Beta +Docs needing updating: +@license, @import +sitemapBaseUrl +markedOptions -> markdownItOptions, markdownItLoader, navigation + ## Breaking Changes - Drop support for Node 16. @@ -9,6 +14,11 @@ - Updated Shiki from 0.14 to 1.3. This should mostly be a transparent update which adds another 23 supported languages and 13 supported themes. - Renamed `--sitemapBaseUrl` to `--hostedBaseUrl` to reflect that it can be used for more than just the sitemap. - Removed deprecated `navigation.fullTree` option. +- All function-likes may now have comments directly attached to them. This is a change from previous versions of TypeDoc where functions comments + were always moved down to the signature level. This mostly worked, but caused problems with type aliases, so was partially changed in 0.25.13. + This change was extended to apply not only to type aliases, but also other function-likes declared with variables and callable properties. + As a part of this change, comments on the implementation signature of overloaded functions will now be added to the function reflection, and will + not be inherited by signatures of that function, #2521. - API: `MapOptionDeclaration.mapError` has been removed. - API: Deprecated `BindOption` decorator has been removed. - API: `DeclarationReflection.indexSignature` has been renamed to `DeclarationReflection.indexSignatures`. diff --git a/src/lib/converter/comments/discovery.ts b/src/lib/converter/comments/discovery.ts index e37a9b605..2b1c06c83 100644 --- a/src/lib/converter/comments/discovery.ts +++ b/src/lib/converter/comments/discovery.ts @@ -178,11 +178,11 @@ export function discoverComment( } seen.add(node); - // Special behavior here! We temporarily put the implementation comment - // on the reflection which contains all the signatures. This lets us pull - // the comment on the implementation if some signature does not have a comment. - // However, we don't want to skip the node if it is a reference to something. - // See the gh1770 test for an example. + // Special behavior here! + // Signatures and symbols have two distinct discovery methods as of TypeDoc 0.26. + // This method discovers comments for symbols, and function-likes will only have + // a symbol comment if there is more than one signature (== more than one declaration) + // and there is a comment on the implementation signature. if ( kind & ReflectionKind.ContainsCallSignatures && [ @@ -190,7 +190,10 @@ export function discoverComment( ts.SyntaxKind.MethodDeclaration, ts.SyntaxKind.Constructor, ].includes(node.kind) && - !(node as ts.FunctionDeclaration).body + (symbol.declarations!.filter((d) => + wantedKinds[kind].includes(d.kind), + ).length === 1 || + !(node as ts.FunctionDeclaration).body) ) { continue; } diff --git a/src/lib/converter/plugins/CommentPlugin.ts b/src/lib/converter/plugins/CommentPlugin.ts index 07d1343fe..0232495e6 100644 --- a/src/lib/converter/plugins/CommentPlugin.ts +++ b/src/lib/converter/plugins/CommentPlugin.ts @@ -10,10 +10,10 @@ import { SignatureReflection, type ParameterReflection, Comment, - ReflectionType, type SourceReference, type TypeVisitor, CommentTag, + ReflectionType, } from "../../models"; import { Option, @@ -359,123 +359,78 @@ export class CommentPlugin extends ConverterComponent { movePropertyTags(reflection.comment, reflection); } - if (!(reflection instanceof DeclarationReflection)) { - return; + if (reflection instanceof DeclarationReflection && reflection.comment) { + let sigs: SignatureReflection[]; + if (reflection.type instanceof ReflectionType) { + sigs = reflection.type.declaration.getNonIndexSignatures(); + } else { + sigs = reflection.getNonIndexSignatures(); + } + + // For variables and properties, the symbol might own the comment but we might also + // have @param and @returns comments for an owned signature. Only do this if there is + // exactly one signature as otherwise we have no hope of doing validation right. + if (sigs.length === 1 && !sigs[0].comment) { + this.moveSignatureParamComments(sigs[0], reflection.comment); + const returnsTag = reflection.comment.getTag("@returns"); + if (returnsTag) { + sigs[0].comment = new Comment(); + sigs[0].comment.blockTags.push(returnsTag); + reflection.comment.removeTags("@returns"); + } + } } - if (reflection.type instanceof ReflectionType) { - this.moveCommentToSignatures( - reflection, - reflection.type.declaration.getNonIndexSignatures(), - ); - } else { - this.moveCommentToSignatures( - reflection, - reflection.getNonIndexSignatures(), - ); + if (reflection instanceof SignatureReflection) { + this.moveSignatureParamComments(reflection); } } - private moveCommentToSignatures( - reflection: DeclarationReflection, - signatures: SignatureReflection[], + private moveSignatureParamComments( + signature: SignatureReflection, + comment = signature.comment, ) { - if (!signatures.length) { - return; - } - - const comment = reflection.kindOf(ReflectionKind.ClassOrInterface) - ? undefined - : reflection.comment; - - for (const signature of signatures) { - const signatureHadOwnComment = !!signature.comment; - const childComment = (signature.comment ||= comment?.clone()); - if (!childComment) continue; - - signature.parameters?.forEach((parameter, index) => { - if (parameter.name === "__namedParameters") { - const commentParams = childComment.blockTags.filter( - (tag) => - tag.tag === "@param" && !tag.name?.includes("."), - ); - if ( - signature.parameters?.length === commentParams.length && - commentParams[index].name - ) { - parameter.name = commentParams[index].name!; - } - } + if (!comment) return; - const tag = childComment.getIdentifiedTag( - parameter.name, - "@param", + signature.parameters?.forEach((parameter, index) => { + if (parameter.name === "__namedParameters") { + const commentParams = comment.blockTags.filter( + (tag) => tag.tag === "@param" && !tag.name?.includes("."), ); - - if (tag) { - parameter.comment = new Comment( - Comment.cloneDisplayParts(tag.content), - ); - } - }); - - for (const parameter of signature.typeParameters || []) { - const tag = - childComment.getIdentifiedTag( - parameter.name, - "@typeParam", - ) || - childComment.getIdentifiedTag( - parameter.name, - "@template", - ) || - childComment.getIdentifiedTag( - `<${parameter.name}>`, - "@param", - ); - if (tag) { - parameter.comment = new Comment( - Comment.cloneDisplayParts(tag.content), - ); + if ( + signature.parameters?.length === commentParams.length && + commentParams[index].name + ) { + parameter.name = commentParams[index].name!; } } - this.validateParamTags( - signature, - childComment, - signature.parameters || [], - signatureHadOwnComment, - ); + const tag = comment.getIdentifiedTag(parameter.name, "@param"); - childComment?.removeTags("@param"); - childComment?.removeTags("@typeParam"); - childComment?.removeTags("@template"); - } + if (tag) { + parameter.comment = new Comment( + Comment.cloneDisplayParts(tag.content), + ); + } + }); - // Since this reflection has signatures, we need to remove the comment from the non-primary - // declaration location. For functions/methods/constructors, this means removing it from - // the wrapping reflection. For type aliases, classes, and interfaces, this means removing - // it from the contained signatures... if it's the same as what is on the signature. - // This is important so that in type aliases we don't end up with a comment rendered twice. - if (reflection.kindOf(ReflectionKind.SignatureContainer)) { - delete reflection.comment; - } else { - reflection.comment?.removeTags("@param"); - reflection.comment?.removeTags("@typeParam"); - reflection.comment?.removeTags("@template"); - - const parentComment = Comment.combineDisplayParts( - reflection.comment?.summary, - ); - for (const sig of signatures) { - if ( - Comment.combineDisplayParts(sig.comment?.summary) === - parentComment - ) { - delete sig.comment; - } + for (const parameter of signature.typeParameters || []) { + const tag = + comment.getIdentifiedTag(parameter.name, "@typeParam") || + comment.getIdentifiedTag(parameter.name, "@template") || + comment.getIdentifiedTag(`<${parameter.name}>`, "@param"); + if (tag) { + parameter.comment = new Comment( + Comment.cloneDisplayParts(tag.content), + ); } } + + this.validateParamTags(signature, comment, signature.parameters || []); + + comment.removeTags("@param"); + comment.removeTags("@typeParam"); + comment.removeTags("@template"); } private removeExcludedTags(comment: Comment) { @@ -607,7 +562,6 @@ export class CommentPlugin extends ConverterComponent { signature: SignatureReflection, comment: Comment, params: ParameterReflection[], - signatureHadOwnComment: boolean, ) { const paramTags = comment.blockTags.filter( (tag) => tag.tag === "@param", @@ -619,7 +573,7 @@ export class CommentPlugin extends ConverterComponent { moveNestedParamTags(/* in-out */ paramTags, params); - if (signatureHadOwnComment && paramTags.length) { + if (paramTags.length) { for (const tag of paramTags) { this.application.logger.warn( this.application.i18n.signature_0_has_unused_param_with_name_1( diff --git a/src/lib/converter/symbols.ts b/src/lib/converter/symbols.ts index 4c0d9e35e..324f910f2 100644 --- a/src/lib/converter/symbols.ts +++ b/src/lib/converter/symbols.ts @@ -486,8 +486,6 @@ function convertFunctionOrMethod( const scope = context.withScope(reflection); - // Can't use zip here. We might have less declarations than signatures - // or less signatures than declarations. for (const sig of signatures) { createSignature(scope, ReflectionKind.CallSignature, sig, symbol); } diff --git a/src/lib/models/reflections/abstract.ts b/src/lib/models/reflections/abstract.ts index 6ea5ed43c..f406e7d43 100644 --- a/src/lib/models/reflections/abstract.ts +++ b/src/lib/models/reflections/abstract.ts @@ -329,8 +329,10 @@ export abstract class Reflection { * Test whether this reflection is of the given kind. */ kindOf(kind: ReflectionKind | ReflectionKind[]): boolean { - const kindArray = Array.isArray(kind) ? kind : [kind]; - return kindArray.some((kind) => (this.kind & kind) !== 0); + const kindFlags = Array.isArray(kind) + ? kind.reduce((a, b) => a | b, 0) + : kind; + return (this.kind & kindFlags) !== 0; } /** diff --git a/src/lib/utils/options/sources/typedoc.ts b/src/lib/utils/options/sources/typedoc.ts index 961b10ea3..acdbd775b 100644 --- a/src/lib/utils/options/sources/typedoc.ts +++ b/src/lib/utils/options/sources/typedoc.ts @@ -318,7 +318,9 @@ export function addTypeDocOptions(options: Pick) { help: (i18n) => i18n.help_markdownItOptions(), type: ParameterType.Mixed, configFileOnly: true, - defaultValue: {}, + defaultValue: { + linkify: true, + }, validate(value, i18n) { if (!Validation.validate({}, value)) { throw new Error( diff --git a/src/lib/validation/documentation.ts b/src/lib/validation/documentation.ts index dcf3bdb8e..da9df9db4 100644 --- a/src/lib/validation/documentation.ts +++ b/src/lib/validation/documentation.ts @@ -43,7 +43,7 @@ export function validateDocumentation( if (seen.has(ref)) continue; seen.add(ref); - // If we're a non-parameter inside a parameter, we shouldn't care. Parameters don't get deeply documented + // If inside a parameter, we shouldn't care. Callback parameter's values don't get deeply documented. let r: Reflection | undefined = ref.parent; while (r) { if (r.kindOf(ReflectionKind.Parameter)) { @@ -71,7 +71,7 @@ export function validateDocumentation( continue; } - // Call signatures are considered documented if they are directly within a documented type alias. + // Construct signatures are considered documented if they are directly within a documented type alias. if ( ref.kindOf(ReflectionKind.ConstructorSignature) && ref.parent?.parent?.kindOf(ReflectionKind.TypeAlias) diff --git a/src/test/behavior.c2.test.ts b/src/test/behavior.c2.test.ts index 8471ae621..52bd9cefd 100644 --- a/src/test/behavior.c2.test.ts +++ b/src/test/behavior.c2.test.ts @@ -957,8 +957,11 @@ describe("Behavior Tests", () => { const barComments = bar.signatures?.map((sig) => Comment.combineDisplayParts(sig.comment?.summary), ); - equal(barComments, ["Implementation comment", "Custom comment"]); - equal(bar.comment, undefined); + equal(barComments, ["", "Custom comment"]); + equal( + Comment.combineDisplayParts(bar.comment?.summary), + "Implementation comment", + ); logger.expectMessage( 'warn: The label "bad" for badLabel cannot be referenced with a declaration reference. Labels may only contain A-Z, 0-9, and _, and may not start with a number.', diff --git a/src/test/converter/function/function.ts b/src/test/converter/function/function.ts index c0a4ed128..c07486051 100644 --- a/src/test/converter/function/function.ts +++ b/src/test/converter/function/function.ts @@ -95,12 +95,12 @@ export function multipleSignatures(value: string): string; export function multipleSignatures(value: { name: string }): string; /** - * This is the actual implementation, this comment will not be visible - * in the generated documentation. The `@inheritdoc` tag can not be used - * to pull content from this signature into documentation for the real - * signatures. - * - * @return This is the return value of the function. + * This comment is on the actual implementation of the function. + * TypeDoc used to allow this for providing "default" comments that would be + * copied to each signature. It no longer does this, and instead treats + * this comment as belonging to the function reflection itself. + * Any `@param` or `@returns` tags within this comment won't be applied + * to signatures. */ export function multipleSignatures(): string { if (arguments.length > 0) { diff --git a/src/test/converter/function/specs.json b/src/test/converter/function/specs.json index 95c195124..158305f96 100644 --- a/src/test/converter/function/specs.json +++ b/src/test/converter/function/specs.json @@ -425,6 +425,14 @@ "variant": "declaration", "kind": 64, "flags": {}, + "comment": { + "summary": [ + { + "kind": "text", + "text": "Returns true if fn returns true for every item in the iterator\n\nReturns true if the iterator is empty" + } + ] + }, "sources": [ { "fileName": "function.ts", @@ -440,14 +448,6 @@ "variant": "signature", "kind": 4096, "flags": {}, - "comment": { - "summary": [ - { - "kind": "text", - "text": "Returns true if fn returns true for every item in the iterator\n\nReturns true if the iterator is empty" - } - ] - }, "sources": [ { "fileName": "function.ts", @@ -565,14 +565,6 @@ "variant": "signature", "kind": 4096, "flags": {}, - "comment": { - "summary": [ - { - "kind": "text", - "text": "Returns true if fn returns true for every item in the iterator\n\nReturns true if the iterator is empty" - } - ] - }, "sources": [ { "fileName": "function.ts", @@ -1745,6 +1737,30 @@ "variant": "declaration", "kind": 64, "flags": {}, + "comment": { + "summary": [ + { + "kind": "text", + "text": "This comment is on the actual implementation of the function.\nTypeDoc used to allow this for providing \"default\" comments that would be\ncopied to each signature. It no longer does this, and instead treats\nthis comment as belonging to the function reflection itself.\nAny " + }, + { + "kind": "code", + "text": "`@param`" + }, + { + "kind": "text", + "text": " or " + }, + { + "kind": "code", + "text": "`@returns`" + }, + { + "kind": "text", + "text": " tags within this comment won't be applied\nto signatures." + } + ] + }, "sources": [ { "fileName": "function.ts", @@ -1921,6 +1937,14 @@ "variant": "declaration", "kind": 64, "flags": {}, + "comment": { + "summary": [ + { + "kind": "text", + "text": "This is a function that is assigned to a variable." + } + ] + }, "sources": [ { "fileName": "function.ts", @@ -1937,12 +1961,7 @@ "kind": 4096, "flags": {}, "comment": { - "summary": [ - { - "kind": "text", - "text": "This is a function that is assigned to a variable." - } - ], + "summary": [], "blockTags": [ { "tag": "@returns", diff --git a/src/test/converter/js/export-eq-type.js b/src/test/converter/js/export-eq-type.js index 3bf3ea400..feeddbe07 100644 --- a/src/test/converter/js/export-eq-type.js +++ b/src/test/converter/js/export-eq-type.js @@ -1,6 +1,6 @@ /** @typedef {string} Foo */ -/** @param {Foo} x */ +/** @param {Foo} x x desc */ const foo = (x) => x; module.exports = foo; diff --git a/src/test/converter/js/specs.json b/src/test/converter/js/specs.json index a51677c34..8ef8ad974 100644 --- a/src/test/converter/js/specs.json +++ b/src/test/converter/js/specs.json @@ -67,6 +67,14 @@ "variant": "param", "kind": 32768, "flags": {}, + "comment": { + "summary": [ + { + "kind": "text", + "text": "x desc" + } + ] + }, "type": { "type": "intrinsic", "name": "string" diff --git a/src/test/converter/mixin/specs.json b/src/test/converter/mixin/specs.json index c96ba825a..a9016177b 100644 --- a/src/test/converter/mixin/specs.json +++ b/src/test/converter/mixin/specs.json @@ -1532,6 +1532,14 @@ "variant": "declaration", "kind": 64, "flags": {}, + "comment": { + "summary": [ + { + "kind": "text", + "text": "The \"mixin function\" of the Mixin1" + } + ] + }, "sources": [ { "fileName": "mixin.ts", @@ -1547,14 +1555,6 @@ "variant": "signature", "kind": 4096, "flags": {}, - "comment": { - "summary": [ - { - "kind": "text", - "text": "The \"mixin function\" of the Mixin1" - } - ] - }, "sources": [ { "fileName": "mixin.ts", @@ -1720,6 +1720,14 @@ "variant": "declaration", "kind": 64, "flags": {}, + "comment": { + "summary": [ + { + "kind": "text", + "text": "The \"mixin function\" of the Mixin2" + } + ] + }, "sources": [ { "fileName": "mixin.ts", @@ -1735,14 +1743,6 @@ "variant": "signature", "kind": 4096, "flags": {}, - "comment": { - "summary": [ - { - "kind": "text", - "text": "The \"mixin function\" of the Mixin2" - } - ] - }, "sources": [ { "fileName": "mixin.ts", @@ -1919,6 +1919,14 @@ "variant": "declaration", "kind": 64, "flags": {}, + "comment": { + "summary": [ + { + "kind": "text", + "text": "The \"mixin function\" of the Mixin3" + } + ] + }, "sources": [ { "fileName": "mixin.ts", @@ -1940,14 +1948,6 @@ "variant": "signature", "kind": 4096, "flags": {}, - "comment": { - "summary": [ - { - "kind": "text", - "text": "The \"mixin function\" of the Mixin3" - } - ] - }, "sources": [ { "fileName": "mixin.ts", diff --git a/src/test/issues.c2.test.ts b/src/test/issues.c2.test.ts index 61355d5c6..b27c4f5ad 100644 --- a/src/test/issues.c2.test.ts +++ b/src/test/issues.c2.test.ts @@ -406,13 +406,13 @@ describe("Issue Tests", () => { ); const comments = [ - project.children[0].comment?.summary, - project.children[0].children[0].comment?.summary, - project.children[0].children[1].signatures![0].comment?.summary, - project.children[0].signatures![0].comment?.summary, - ].map(Comment.combineDisplayParts); + query(project, "bar"), + query(project, "bar.metadata"), + querySig(project, "bar.fn"), + querySig(project, "bar"), + ].map((r) => Comment.combineDisplayParts(r.comment?.summary)); - equal(comments, ["", "metadata", "fn", "bar"]); + equal(comments, ["bar", "metadata", "fn", ""]); }); it("#1660", () => { @@ -475,7 +475,7 @@ describe("Issue Tests", () => { const project = convert(); const sym1 = query(project, "sym1"); equal( - Comment.combineDisplayParts(sym1.signatures?.[0].comment?.summary), + Comment.combineDisplayParts(sym1.comment?.summary), "Docs for Sym1", ); @@ -717,9 +717,7 @@ describe("Issue Tests", () => { equal(comments, ["A override", "B module"]); const comments2 = ["A.a", "B.b"].map((n) => - Comment.combineDisplayParts( - query(project, n).signatures![0].comment?.summary, - ), + Comment.combineDisplayParts(query(project, n).comment?.summary), ); equal(comments2, ["Comment for a", "Comment for b"]); @@ -792,7 +790,7 @@ describe("Issue Tests", () => { it("#2008", () => { const project = convert(); - const fn = query(project, "myFn").signatures![0]; + const fn = query(project, "myFn"); equal(Comment.combineDisplayParts(fn.comment?.summary), "Docs"); }); @@ -891,20 +889,25 @@ describe("Issue Tests", () => { it("#2042", () => { const project = convert(); - for (const [name, docs] of [ - ["built", "inner docs"], - ["built2", "outer docs"], - ["fn", "inner docs"], - ["fn2", "outer docs"], + for (const [name, docs, sigDocs] of [ + ["built", "", "inner docs"], + ["built2", "outer docs", ""], + ["fn", "", "inner docs"], + ["fn2", "outer docs", ""], ]) { const refl = query(project, name); ok(refl.signatures?.[0]); + equal( + Comment.combineDisplayParts(refl.comment?.summary), + docs, + name + " docs", + ); equal( Comment.combineDisplayParts( refl.signatures[0].comment?.summary, ), - docs, - name, + sigDocs, + name + " sig docs", ); } }); @@ -1009,7 +1012,7 @@ describe("Issue Tests", () => { const foo = query(project, "foo"); equal(foo.signatures?.length, 1); equal( - Comment.combineDisplayParts(foo.signatures[0].comment?.summary), + Comment.combineDisplayParts(foo.comment?.summary), "Is documented", ); });