Skip to content

Commit 5e108b6

Browse files
committed
Add warnings for bad @param tags
Resolves #2368
1 parent 1963bd1 commit 5e108b6

File tree

6 files changed

+544
-1086
lines changed

6 files changed

+544
-1086
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Features
44

5+
- TypeDoc now provides warnings if a signature comment is directly specified on a signature and contains `@param` tags which do not apply, #2368.
56
- Extended reflection preview view for interfaces to include type parameters, #2455.
67
- Added special cases for converting methods which are documented as returning `this` or accepting `this` as a parameter, #2458.
78
Note: This will only happen if a method is declared as `method(): this`, it will not happen if the method implicitly returns `this`

src/lib/converter/plugins/CommentPlugin.ts

Lines changed: 78 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
removeIfPresent,
2222
unique,
2323
partition,
24+
removeIf,
2425
} from "../../utils";
2526
import { CategoryPlugin } from "./CategoryPlugin";
2627

@@ -388,6 +389,7 @@ export class CommentPlugin extends ConverterComponent {
388389
: reflection.comment;
389390

390391
for (const signature of signatures) {
392+
const signatureHadOwnComment = !!signature.comment;
391393
const childComment = (signature.comment ||= comment?.clone());
392394
if (!childComment) continue;
393395

@@ -405,7 +407,6 @@ export class CommentPlugin extends ConverterComponent {
405407
}
406408
}
407409

408-
moveNestedParamTags(childComment, parameter);
409410
const tag = childComment.getIdentifiedTag(
410411
parameter.name,
411412
"@param",
@@ -439,6 +440,13 @@ export class CommentPlugin extends ConverterComponent {
439440
}
440441
}
441442

443+
this.validateParamTags(
444+
signature,
445+
childComment,
446+
signature.parameters || [],
447+
signatureHadOwnComment,
448+
);
449+
442450
childComment?.removeTags("@param");
443451
childComment?.removeTags("@typeParam");
444452
childComment?.removeTags("@template");
@@ -590,6 +598,33 @@ export class CommentPlugin extends ConverterComponent {
590598

591599
return excludeCategories.some((cat) => categories.has(cat));
592600
}
601+
602+
private validateParamTags(
603+
signature: SignatureReflection,
604+
comment: Comment,
605+
params: ParameterReflection[],
606+
signatureHadOwnComment: boolean,
607+
) {
608+
const paramTags = comment.blockTags.filter(
609+
(tag) => tag.tag === "@param",
610+
);
611+
612+
removeIf(paramTags, (tag) =>
613+
params.some((param) => param.name === tag.name),
614+
);
615+
616+
moveNestedParamTags(/* in-out */ paramTags, params);
617+
618+
if (signatureHadOwnComment && paramTags.length) {
619+
for (const tag of paramTags) {
620+
this.application.logger.warn(
621+
`The signature ${signature.getFriendlyFullName()} has an @param with name "${
622+
tag.name
623+
}", which was not used.`,
624+
);
625+
}
626+
}
627+
}
593628
}
594629

595630
function inTypeLiteral(refl: Reflection | undefined) {
@@ -603,37 +638,51 @@ function inTypeLiteral(refl: Reflection | undefined) {
603638
}
604639

605640
// Moves tags like `@param foo.bar docs for bar` into the `bar` property of the `foo` parameter.
606-
function moveNestedParamTags(comment: Comment, parameter: ParameterReflection) {
607-
const visitor: Partial<TypeVisitor> = {
608-
reflection(target) {
609-
const tags = comment.blockTags.filter(
610-
(t) =>
611-
t.tag === "@param" &&
612-
t.name?.startsWith(`${parameter.name}.`),
613-
);
641+
function moveNestedParamTags(
642+
/* in-out */ paramTags: CommentTag[],
643+
parameters: ParameterReflection[],
644+
) {
645+
const used = new Set<number>();
646+
647+
for (const param of parameters) {
648+
const visitor: Partial<TypeVisitor> = {
649+
reflection(target) {
650+
const tags = paramTags.filter(
651+
(t) => t.name?.startsWith(`${param.name}.`),
652+
);
614653

615-
for (const tag of tags) {
616-
const path = tag.name!.split(".");
617-
path.shift();
618-
const child = target.declaration.getChildByName(path);
654+
for (const tag of tags) {
655+
const path = tag.name!.split(".");
656+
path.shift();
657+
const child = target.declaration.getChildByName(path);
619658

620-
if (child && !child.comment) {
621-
child.comment = new Comment(
622-
Comment.cloneDisplayParts(tag.content),
623-
);
659+
if (child && !child.comment) {
660+
child.comment = new Comment(
661+
Comment.cloneDisplayParts(tag.content),
662+
);
663+
used.add(paramTags.indexOf(tag));
664+
}
624665
}
625-
}
626-
},
627-
// #1876, also do this for unions/intersections.
628-
union(u) {
629-
u.types.forEach((t) => t.visit(visitor));
630-
},
631-
intersection(i) {
632-
i.types.forEach((t) => t.visit(visitor));
633-
},
634-
};
635-
636-
parameter.type?.visit(visitor);
666+
},
667+
// #1876, also do this for unions/intersections.
668+
union(u) {
669+
u.types.forEach((t) => t.visit(visitor));
670+
},
671+
intersection(i) {
672+
i.types.forEach((t) => t.visit(visitor));
673+
},
674+
};
675+
676+
param.type?.visit(visitor);
677+
}
678+
679+
const toRemove = Array.from(used)
680+
.sort((a, b) => a - b)
681+
.reverse();
682+
683+
for (const index of toRemove) {
684+
paramTags.splice(index, 1);
685+
}
637686
}
638687

639688
function movePropertyTags(comment: Comment, container: Reflection) {

src/test/behavior.c2.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,4 +1046,43 @@ describe("Behavior Tests", () => {
10461046
"this",
10471047
);
10481048
});
1049+
1050+
it("Handles renaming of destructured parameters via @param tag name inference", () => {
1051+
const project = convert("destructuredParamRenames");
1052+
1053+
const params = (name: string) =>
1054+
querySig(project, name).parameters?.map((p) => p.name);
1055+
1056+
equal(params("functionWithADestructuredParameter"), [
1057+
"destructuredParam",
1058+
]);
1059+
1060+
equal(params("functionWithADestructuredParameterAndExtraParameters"), [
1061+
"__namedParameters",
1062+
"extraParameter",
1063+
]);
1064+
1065+
equal(
1066+
params(
1067+
"functionWithADestructuredParameterAndAnExtraParamDirective",
1068+
),
1069+
["__namedParameters"],
1070+
);
1071+
1072+
const logs = [
1073+
'warn: The signature functionWithADestructuredParameterAndExtraParameters has an @param with name "destructuredParam", which was not used.',
1074+
'warn: The signature functionWithADestructuredParameterAndExtraParameters has an @param with name "destructuredParam.paramZ", which was not used.',
1075+
'warn: The signature functionWithADestructuredParameterAndExtraParameters has an @param with name "destructuredParam.paramG", which was not used.',
1076+
'warn: The signature functionWithADestructuredParameterAndExtraParameters has an @param with name "destructuredParam.paramA", which was not used.',
1077+
'warn: The signature functionWithADestructuredParameterAndAnExtraParamDirective has an @param with name "fakeParameter", which was not used.',
1078+
'warn: The signature functionWithADestructuredParameterAndAnExtraParamDirective has an @param with name "destructuredParam", which was not used.',
1079+
'warn: The signature functionWithADestructuredParameterAndAnExtraParamDirective has an @param with name "destructuredParam.paramZ", which was not used.',
1080+
'warn: The signature functionWithADestructuredParameterAndAnExtraParamDirective has an @param with name "destructuredParam.paramG", which was not used.',
1081+
'warn: The signature functionWithADestructuredParameterAndAnExtraParamDirective has an @param with name "destructuredParam.paramA", which was not used.',
1082+
];
1083+
for (const log of logs) {
1084+
logger.expectMessage(log);
1085+
}
1086+
logger.expectNoOtherMessages();
1087+
});
10491088
});

src/test/converter/function/function.ts

Lines changed: 0 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -79,102 +79,6 @@ export function functionWithRest(...rest: string[]): string {
7979
return rest.join(", ");
8080
}
8181

82-
/**
83-
* This is a function with a destructured parameter.
84-
*
85-
* @param destructuredParam - This is the parameter that is destructured.
86-
* @param destructuredParam.paramZ - This is a string parameter.
87-
* @param destructuredParam.paramG - This is a parameter flagged with any.
88-
* This sentence is placed in the next line.
89-
*
90-
* @param destructuredParam.paramA
91-
* This is a **parameter** pointing to an interface.
92-
*
93-
* ```
94-
* const value:BaseClass = new BaseClass('test');
95-
* functionWithArguments('arg', 0, value);
96-
* ```
97-
*
98-
* @returns This is the return value of the function.
99-
*/
100-
export function functionWithADestructuredParameter({
101-
paramZ,
102-
paramG,
103-
paramA,
104-
}: {
105-
paramZ: string;
106-
paramG: any;
107-
paramA: Object;
108-
}): number {
109-
return 0;
110-
}
111-
112-
/**
113-
* This is a function with a destructured parameter and additional undocumented parameters.
114-
* The `@param` directives are ignored because we cannot be certain which parameter they refer to.
115-
*
116-
* @param destructuredParam - This is the parameter that is destructured.
117-
* @param destructuredParam.paramZ - This is a string parameter.
118-
* @param destructuredParam.paramG - This is a parameter flagged with any.
119-
* This sentence is placed in the next line.
120-
*
121-
* @param destructuredParam.paramA
122-
* This is a **parameter** pointing to an interface.
123-
*
124-
* ```
125-
* const value:BaseClass = new BaseClass('test');
126-
* functionWithArguments('arg', 0, value);
127-
* ```
128-
*
129-
* @returns This is the return value of the function.
130-
*/
131-
export function functionWithADestructuredParameterAndExtraParameters(
132-
{
133-
paramZ,
134-
paramG,
135-
paramA,
136-
}: {
137-
paramZ: string;
138-
paramG: any;
139-
paramA: Object;
140-
},
141-
extraParameter: string,
142-
): number {
143-
return 0;
144-
}
145-
146-
/**
147-
* This is a function with a destructured parameter and an extra `@param` directive with no corresponding parameter.
148-
* The `@param` directives are ignored because we cannot be certain which corresponds to the real parameter.
149-
*
150-
* @param fakeParameter - This directive does not have a corresponding parameter.
151-
* @param destructuredParam - This is the parameter that is destructured.
152-
* @param destructuredParam.paramZ - This is a string parameter.
153-
* @param destructuredParam.paramG - This is a parameter flagged with any.
154-
* This sentence is placed in the next line.
155-
*
156-
* @param destructuredParam.paramA
157-
* This is a **parameter** pointing to an interface.
158-
*
159-
* ```
160-
* const value:BaseClass = new BaseClass('test');
161-
* functionWithArguments('arg', 0, value);
162-
* ```
163-
*
164-
* @returns This is the return value of the function.
165-
*/
166-
export function functionWithADestructuredParameterAndAnExtraParamDirective({
167-
paramZ,
168-
paramG,
169-
paramA,
170-
}: {
171-
paramZ: string;
172-
paramG: any;
173-
paramA: Object;
174-
}): number {
175-
return 0;
176-
}
177-
17882
/**
17983
* This is the first signature of a function with multiple signatures.
18084
*

0 commit comments

Comments
 (0)