Skip to content

Commit 4ca04c1

Browse files
committed
Handle @this tag, improved destructured parameters
Resolves #3026
1 parent 1009206 commit 4ca04c1

File tree

13 files changed

+104
-140
lines changed

13 files changed

+104
-140
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ title: Changelog
1010
be copied to the output documentation, #3020.
1111
API: Introduced `typeAnnotation` on `CommentTag`
1212
- Added `excludePrivateClassFields` option to hide `#private` members while allowing `private` members, #3017.
13+
- Added support for TypeScript's `@this` tag for JS files which describe `this` parameters, #3026.
1314

1415
## Bug Fixes
1516

1617
- Fixed conversion of auto-accessor types on properties with the `accessor` keyword, #3019.
1718
- Improved handling of HTML tags within headers for anchor generation, #3023.
19+
- Improved support for detecting destructured parameters and renaming them to the name used in the doc comment, #3026.
1820

1921
## v0.28.13 (2025-09-14)
2022

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,9 @@
7373
"test": "mocha --config .config/mocha.fast.json",
7474
"test:cov": "c8 -r lcov mocha --config .config/mocha.fast.json",
7575
"doc:c": "node bin/typedoc --tsconfig src/test/converter/tsconfig.json",
76-
"doc:cd": "node --inspect-brk bin/typedoc --tsconfig src/test/converter/tsconfig.json",
76+
"doc:cd": "node --inspect-brk dist/lib/cli.js --tsconfig src/test/converter/tsconfig.json",
7777
"doc:c2": "node bin/typedoc --options src/test/converter2 --tsconfig src/test/converter2/tsconfig.json",
78-
"doc:c2d": "node --inspect-brk bin/typedoc --options src/test/converter2 --tsconfig src/test/converter2/tsconfig.json",
78+
"doc:c2d": "node --inspect-brk dist/lib/cli.js --options src/test/converter2 --tsconfig src/test/converter2/tsconfig.json",
7979
"example": "cd example && node ../bin/typedoc",
8080
"test:full": "c8 -r lcov -r text-summary mocha --config .config/mocha.full.json",
8181
"rebuild_specs": "node scripts/rebuild_specs.js",

site/tags.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ examples for how to use the export ([`@example`](./tags/example.md)).
122122
- [`@license`](./tags/license.md)
123123
- [`@mergeModuleWith`](./tags/mergeModuleWith.md)
124124
- [`@module`](./tags/module.md)
125-
- [`@param`](./tags/param.md)
125+
- [`@param`, `@this`](./tags/param.md)
126126
- [`@preventExpand`](./tags/expand.md#preventexpand)
127127
- [`@preventInline`](./tags/inline.md#preventinline)
128128
- [`@privateRemarks`](./tags/privateRemarks.md)

site/tags/param.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: "@param"
33
---
44

5-
# @param
5+
# @param and @this
66

77
**Tag Kind:** [Block](../tags.md#block-tags) <br>
88
**TSDoc Reference:** [@param](https://tsdoc.org/pages/tags/param/)
@@ -54,6 +54,21 @@ export function configure({ value }: { value: string }) {}
5454
export function configure(options: { value: string }) {}
5555
```
5656

57+
## `this` Parameters
58+
59+
Functions which use `this` in JavaScript files may use TypeScript's `@this` tag to define the type of their `this`
60+
parameter. TypeDoc will check for `@this` tags and use their content in the description of the `this` parameter.
61+
62+
```js
63+
/**
64+
* @this {Request} parameter description for `this`
65+
* @param {Response} response parameter description for `response`
66+
*/
67+
export function hello(response) {
68+
response.write(`Hello ${this.query.name || "world!"}`);
69+
}
70+
```
71+
5772
## TSDoc Compatibility
5873

5974
The TSDoc standard requires that the `@param` tag _not_ include types and that

src/lib/converter/factories/signature.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ export function createConstructSignatureWithType(
163163
}
164164
}
165165

166-
const parameterSymbols: Array<ts.Symbol & { type?: ts.Type }> = signature.thisParameter
166+
const parameterSymbols = signature.thisParameter
167167
? [signature.thisParameter, ...signature.parameters]
168168
: [...signature.parameters];
169169

@@ -207,7 +207,7 @@ export function createConstructSignatureWithType(
207207
function convertParameters(
208208
context: Context,
209209
sigRef: SignatureReflection,
210-
parameters: readonly (ts.Symbol & { type?: ts.Type })[],
210+
parameters: readonly ts.Symbol[],
211211
parameterNodes:
212212
| readonly ts.ParameterDeclaration[]
213213
| readonly ts.JSDocParameterTag[]
@@ -226,7 +226,7 @@ function convertParameters(
226226
ts.isJSDocParameterTag(declaration),
227227
);
228228
const paramRefl = new ParameterReflection(
229-
/__\d+/.test(param.name) ? "__namedParameters" : param.name,
229+
/^__\d+$/.test(param.name) ? "__namedParameters" : param.name,
230230
ReflectionKind.Parameter,
231231
sigRef,
232232
);
@@ -256,7 +256,7 @@ function convertParameters(
256256
typeNode = declaration.typeExpression?.type;
257257
}
258258
} else {
259-
type = param.type;
259+
type = context.checker.getTypeOfSymbol(param);
260260
}
261261

262262
if (
@@ -295,8 +295,9 @@ function convertParameters(
295295
paramRefl.setFlag(ReflectionFlag.Optional, isOptional);
296296

297297
// If we have no declaration, then this is an implicitly defined parameter in JS land
298-
// because the method body uses `arguments`... which is always a rest argument
299-
let isRest = true;
298+
// because the method body uses `arguments`... which is always a rest argument,
299+
// unless it is a this parameter defined with @this in JSDoc.
300+
let isRest = param.name !== "this";
300301
if (declaration) {
301302
isRest = ts.isParameter(declaration)
302303
? !!declaration.dotDotDotToken

src/lib/converter/plugins/CommentPlugin.ts

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ import { CategoryPlugin } from "./CategoryPlugin.js";
3737
* (for JS users) will be consumed by TypeScript and need not be preserved
3838
* in the comment.
3939
*
40-
* Note that param/arg/argument/return/returns are not present.
40+
* Note that param/arg/argument/return/returns/this are not present.
4141
* These tags will have their type information stripped when parsing, but still
42-
* provide useful information for documentation.
42+
* may provide useful information for documentation.
4343
*/
4444
const NEVER_RENDERED = [
4545
"@augments",
@@ -48,7 +48,6 @@ const NEVER_RENDERED = [
4848
"@constructor",
4949
"@enum",
5050
"@extends",
51-
"@this",
5251
"@type",
5352
"@typedef",
5453
"@jsx",
@@ -479,26 +478,29 @@ export class CommentPlugin extends ConverterComponent {
479478
) {
480479
if (!comment) return;
481480

482-
signature.parameters?.forEach((parameter, index) => {
483-
if (parameter.name === "__namedParameters") {
484-
const commentParams = comment.blockTags.filter(
485-
(tag) => tag.tag === "@param" && !tag.name?.includes("."),
486-
);
487-
if (
488-
signature.parameters?.length === commentParams.length &&
489-
commentParams[index].name
490-
) {
491-
parameter.name = commentParams[index].name!;
492-
}
481+
const unusedCommentParams = comment.blockTags.filter(
482+
(tag) =>
483+
tag.tag === "@param" && tag.name && !tag.name.includes(".") &&
484+
!signature.parameters?.some(p => p.name === tag.name),
485+
);
486+
487+
signature.parameters?.forEach((parameter) => {
488+
if (parameter.name === "__namedParameters" && unusedCommentParams.length) {
489+
parameter.name = unusedCommentParams[0].name!;
490+
unusedCommentParams.splice(0, 1);
493491
}
494492

495493
const tag = comment.getIdentifiedTag(parameter.name, "@param");
496494

497495
if (tag) {
498-
parameter.comment = new Comment(
499-
Comment.cloneDisplayParts(tag.content),
500-
);
496+
parameter.comment = new Comment(Comment.cloneDisplayParts(tag.content));
501497
parameter.comment.sourcePath = comment.sourcePath;
498+
} else if (parameter.name === "this") {
499+
const thisTag = comment.getTag("@this");
500+
if (thisTag) {
501+
parameter.comment = new Comment(Comment.cloneDisplayParts(thisTag.content));
502+
parameter.comment.sourcePath = comment.sourcePath;
503+
}
502504
}
503505
});
504506

@@ -516,6 +518,7 @@ export class CommentPlugin extends ConverterComponent {
516518

517519
this.validateParamTags(signature, comment, signature.parameters || []);
518520

521+
comment.removeTags("@this");
519522
comment.removeTags("@param");
520523
comment.removeTags("@typeParam");
521524
comment.removeTags("@template");

src/lib/utils/options/tsdoc-defaults.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const blockTags = [
3939
"@since",
4040
"@sortStrategy",
4141
"@template", // Alias for @typeParam
42+
"@this",
4243
"@type",
4344
"@typedef",
4445
"@summary",

src/test/TestLogger.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,8 @@ export class TestLogger extends Logger {
4141
}
4242
}
4343

44-
expectNoOtherMessages({ ignoreDebug } = { ignoreDebug: true }) {
45-
const messages = ignoreDebug
46-
? this.messages.filter((msg) => !msg.startsWith("debug"))
47-
: this.messages;
44+
expectNoOtherMessages() {
45+
const messages = this.messages.filter((msg) => !msg.startsWith("debug"));
4846

4947
ok(
5048
messages.length === 0,

src/test/behavior.c2.test.ts

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,36 +1118,17 @@ describe("Behavior Tests", () => {
11181118

11191119
const params = (name: string) => querySig(project, name).parameters?.map((p) => p.name);
11201120

1121-
equal(params("functionWithADestructuredParameter"), [
1122-
"destructuredParam",
1123-
]);
1121+
equal(params("singleParam"), ["params"]);
11241122

1125-
equal(params("functionWithADestructuredParameterAndExtraParameters"), [
1126-
"__namedParameters",
1127-
"extraParameter",
1128-
]);
1123+
equal(params("extraParam"), ["params", "extraParameter"]);
11291124

1130-
equal(
1131-
params(
1132-
"functionWithADestructuredParameterAndAnExtraParamDirective",
1133-
),
1134-
["__namedParameters"],
1135-
);
1136-
1137-
const logs = [
1138-
'warn: The signature functionWithADestructuredParameterAndExtraParameters has an @param with name "destructuredParam", which was not used',
1139-
'warn: The signature functionWithADestructuredParameterAndExtraParameters has an @param with name "destructuredParam.paramZ", which was not used',
1140-
'warn: The signature functionWithADestructuredParameterAndExtraParameters has an @param with name "destructuredParam.paramG", which was not used',
1141-
'warn: The signature functionWithADestructuredParameterAndExtraParameters has an @param with name "destructuredParam.paramA", which was not used',
1142-
'warn: The signature functionWithADestructuredParameterAndAnExtraParamDirective has an @param with name "fakeParameter", which was not used',
1143-
'warn: The signature functionWithADestructuredParameterAndAnExtraParamDirective has an @param with name "destructuredParam", which was not used',
1144-
'warn: The signature functionWithADestructuredParameterAndAnExtraParamDirective has an @param with name "destructuredParam.paramZ", which was not used',
1145-
'warn: The signature functionWithADestructuredParameterAndAnExtraParamDirective has an @param with name "destructuredParam.paramG", which was not used',
1146-
'warn: The signature functionWithADestructuredParameterAndAnExtraParamDirective has an @param with name "destructuredParam.paramA", which was not used',
1147-
];
1148-
for (const log of logs) {
1149-
logger.expectMessage(log);
1150-
}
1125+
equal(params("extraParamComment"), ["params"]);
1126+
1127+
equal(params("multiParam"), ["params", "params2", "__namedParameters"]);
1128+
1129+
logger.expectMessage(
1130+
'warn: The signature extraParamComment has an @param with name "fakeParameter", which was not used',
1131+
);
11511132
logger.expectNoOtherMessages();
11521133
});
11531134

Lines changed: 20 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,34 @@
11
/**
2-
* This is a function with a destructured parameter.
3-
*
4-
* @param destructuredParam - This is the parameter that is destructured.
5-
* @param destructuredParam.paramZ - This is a string parameter.
6-
* @param destructuredParam.paramG - This is a parameter flagged with any.
7-
* This sentence is placed in the next line.
8-
*
9-
* @param destructuredParam.paramA
10-
* This is a **parameter** pointing to an interface.
11-
*
12-
* ```
13-
* const value:BaseClass = new BaseClass('test');
14-
* functionWithArguments('arg', 0, value);
15-
* ```
16-
*
17-
* @returns This is the return value of the function.
2+
* @param params - desc
3+
* @param params.a - paramZ desc
184
*/
19-
export function functionWithADestructuredParameter({
20-
paramZ,
21-
paramG,
22-
paramA,
23-
}: {
24-
paramZ: string;
25-
paramG: any;
26-
paramA: Object;
27-
}): number {
5+
export function singleParam({ a }: { a: string }) {
286
return 0;
297
}
308

319
/**
32-
* This is a function with a destructured parameter and additional undocumented parameters.
33-
* The `@param` directives are ignored because we cannot be certain which parameter they refer to.
34-
*
35-
* @param destructuredParam - This is the parameter that is destructured.
36-
* @param destructuredParam.paramZ - This is a string parameter.
37-
* @param destructuredParam.paramG - This is a parameter flagged with any.
38-
* This sentence is placed in the next line.
39-
*
40-
* @param destructuredParam.paramA
41-
* This is a **parameter** pointing to an interface.
42-
*
43-
* ```
44-
* const value:BaseClass = new BaseClass('test');
45-
* functionWithArguments('arg', 0, value);
46-
* ```
47-
*
48-
* @returns This is the return value of the function.
10+
* @param params - desc
4911
*/
50-
export function functionWithADestructuredParameterAndExtraParameters(
51-
{
52-
paramZ,
53-
paramG,
54-
paramA,
55-
}: {
56-
paramZ: string;
57-
paramG: any;
58-
paramA: Object;
59-
},
60-
extraParameter: string,
61-
): number {
12+
export function extraParam({ a }: { a: string }, extraParameter: string) {
6213
return 0;
6314
}
6415

6516
/**
66-
* This is a function with a destructured parameter and an extra `@param` directive with no corresponding parameter.
67-
* The `@param` directives are ignored because we cannot be certain which corresponds to the real parameter.
68-
*
69-
* @param fakeParameter - This directive does not have a corresponding parameter.
70-
* @param destructuredParam - This is the parameter that is destructured.
71-
* @param destructuredParam.paramZ - This is a string parameter.
72-
* @param destructuredParam.paramG - This is a parameter flagged with any.
73-
* This sentence is placed in the next line.
74-
*
75-
* @param destructuredParam.paramA
76-
* This is a **parameter** pointing to an interface.
77-
*
78-
* ```
79-
* const value:BaseClass = new BaseClass('test');
80-
* functionWithArguments('arg', 0, value);
81-
* ```
82-
*
83-
* @returns This is the return value of the function.
17+
* @param params param
18+
* @param fakeParameter param2
8419
*/
85-
export function functionWithADestructuredParameterAndAnExtraParamDirective({
86-
paramZ,
87-
paramG,
88-
paramA,
89-
}: {
90-
paramZ: string;
91-
paramG: any;
92-
paramA: Object;
93-
}): number {
20+
export function extraParamComment({ a }: { a: string }) {
21+
return 0;
22+
}
23+
24+
/**
25+
* @param params params
26+
* @param params2 params2
27+
*/
28+
export function multiParam(
29+
{ a }: { a: string },
30+
{ b }: { b: number },
31+
{ c }: { c: boolean },
32+
) {
9433
return 0;
9534
}

0 commit comments

Comments
 (0)