Skip to content

Commit 34127d2

Browse files
committed
feat(11378): check param names in JSDoc
1 parent b7b6483 commit 34127d2

20 files changed

+641
-46
lines changed

src/compiler/checker.ts

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -34413,6 +34413,7 @@ namespace ts {
3441334413
}
3441434414

3441534415
checkTypeParameters(getEffectiveTypeParameterDeclarations(node));
34416+
checkUnmatchedJSDocParameters(node);
3441634417

3441734418
forEach(node.parameters, checkParameter);
3441834419

@@ -36178,40 +36179,7 @@ namespace ts {
3617836179

3617936180
function checkJSDocParameterTag(node: JSDocParameterTag) {
3618036181
checkSourceElement(node.typeExpression);
36181-
if (!getParameterSymbolFromJSDoc(node)) {
36182-
const decl = getHostSignatureFromJSDoc(node);
36183-
// don't issue an error for invalid hosts -- just functions --
36184-
// and give a better error message when the host function mentions `arguments`
36185-
// but the tag doesn't have an array type
36186-
if (decl) {
36187-
const i = getJSDocTags(decl).filter(isJSDocParameterTag).indexOf(node);
36188-
if (i > -1 && i < decl.parameters.length && isBindingPattern(decl.parameters[i].name)) {
36189-
return;
36190-
}
36191-
if (!containsArgumentsReference(decl)) {
36192-
if (isQualifiedName(node.name)) {
36193-
error(node.name,
36194-
Diagnostics.Qualified_name_0_is_not_allowed_without_a_leading_param_object_1,
36195-
entityNameToString(node.name),
36196-
entityNameToString(node.name.left));
36197-
}
36198-
else {
36199-
error(node.name,
36200-
Diagnostics.JSDoc_param_tag_has_name_0_but_there_is_no_parameter_with_that_name,
36201-
idText(node.name));
36202-
}
36203-
}
36204-
else if (findLast(getJSDocTags(decl), isJSDocParameterTag) === node &&
36205-
node.typeExpression && node.typeExpression.type &&
36206-
!isArrayType(getTypeFromTypeNode(node.typeExpression.type))) {
36207-
error(node.name,
36208-
Diagnostics.JSDoc_param_tag_has_name_0_but_there_is_no_parameter_with_that_name_It_would_match_arguments_if_it_had_an_array_type,
36209-
idText(node.name.kind === SyntaxKind.QualifiedName ? node.name.right : node.name));
36210-
}
36211-
}
36212-
}
3621336182
}
36214-
3621536183
function checkJSDocPropertyTag(node: JSDocPropertyTag) {
3621636184
checkSourceElement(node.typeExpression);
3621736185
}
@@ -38506,6 +38474,45 @@ namespace ts {
3850638474
}
3850738475
}
3850838476

38477+
function checkUnmatchedJSDocParameters(node: SignatureDeclaration) {
38478+
const jsdocParameters = filter(getJSDocTags(node), isJSDocParameterTag);
38479+
if (length(jsdocParameters) === 0) return;
38480+
38481+
const isJs = isInJSFile(node);
38482+
const parameters = new Set<__String>();
38483+
const excludedParameters = new Set<number>();
38484+
forEach(node.parameters, ({ name }, index) => {
38485+
if (isIdentifier(name)) {
38486+
parameters.add(name.escapedText);
38487+
}
38488+
if (isBindingPattern(name)) {
38489+
excludedParameters.add(index);
38490+
}
38491+
});
38492+
38493+
const containsArguments = containsArgumentsReference(node);
38494+
if (containsArguments) {
38495+
const lastJSDocParam = lastOrUndefined(jsdocParameters);
38496+
if (lastJSDocParam && isIdentifier(lastJSDocParam.name) && lastJSDocParam.typeExpression &&
38497+
lastJSDocParam.typeExpression.type && !parameters.has(lastJSDocParam.name.escapedText) && !isArrayType(getTypeFromTypeNode(lastJSDocParam.typeExpression.type))) {
38498+
errorOrSuggestion(isJs, lastJSDocParam.name, Diagnostics.JSDoc_param_tag_has_name_0_but_there_is_no_parameter_with_that_name_It_would_match_arguments_if_it_had_an_array_type, idText(lastJSDocParam.name));
38499+
}
38500+
}
38501+
else {
38502+
forEach(jsdocParameters, ({ name }, index) => {
38503+
if (excludedParameters.has(index) || isIdentifier(name) && parameters.has(name.escapedText)) {
38504+
return;
38505+
}
38506+
if (isQualifiedName(name)) {
38507+
errorOrSuggestion(isJs, name, Diagnostics.Qualified_name_0_is_not_allowed_without_a_leading_param_object_1, entityNameToString(name), entityNameToString(name.left));
38508+
}
38509+
else {
38510+
errorOrSuggestion(isJs, name, Diagnostics.JSDoc_param_tag_has_name_0_but_there_is_no_parameter_with_that_name, idText(name));
38511+
}
38512+
});
38513+
}
38514+
}
38515+
3850938516
/**
3851038517
* Check each type parameter and check that type parameters have no duplicate type parameter declarations
3851138518
*/

src/compiler/diagnosticMessages.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7135,6 +7135,18 @@
71357135
"category": "Message",
71367136
"code": 95169
71377137
},
7138+
"Delete unused '@param' tag '{0}'": {
7139+
"category": "Message",
7140+
"code": 95170
7141+
},
7142+
"Delete all unused '@param' tags": {
7143+
"category": "Message",
7144+
"code": 95171
7145+
},
7146+
"Rename '@param' tag name '{0}' to '{1}'": {
7147+
"category": "Message",
7148+
"code": 95172
7149+
},
71387150

71397151
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
71407152
"category": "Error",
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/* @internal */
2+
namespace ts.codefix {
3+
const deleteUnmatchedParameter = "deleteUnmatchedParameter";
4+
const renameUnmatchedParameter = "renameUnmatchedParameter";
5+
6+
const errorCodes = [
7+
Diagnostics.JSDoc_param_tag_has_name_0_but_there_is_no_parameter_with_that_name.code,
8+
];
9+
10+
registerCodeFix({
11+
fixIds: [deleteUnmatchedParameter, renameUnmatchedParameter],
12+
errorCodes,
13+
getCodeActions: function getCodeActionsToFixUnmatchedParameter(context) {
14+
const { sourceFile, span } = context;
15+
const actions: CodeFixAction[] = [];
16+
const info = getInfo(sourceFile, span.start);
17+
if (info) {
18+
append(actions, getDeleteAction(context, info));
19+
append(actions, getRenameAction(context, info));
20+
return actions;
21+
}
22+
return undefined;
23+
},
24+
getAllCodeActions: function getAllCodeActionsToFixUnmatchedParameter(context) {
25+
const tagsToSignature = new Map<SignatureDeclaration, JSDocTag[]>();
26+
return createCombinedCodeActions(textChanges.ChangeTracker.with(context, changes => {
27+
eachDiagnostic(context, errorCodes, ({ file, start }) => {
28+
const info = getInfo(file, start);
29+
if (info) {
30+
tagsToSignature.set(info.signature, append(tagsToSignature.get(info.signature), info.jsDocParameterTag));
31+
}
32+
});
33+
34+
tagsToSignature.forEach((tags, signature) => {
35+
if (context.fixId === deleteUnmatchedParameter) {
36+
const tagsSet = new Set(tags);
37+
changes.filterJSDocTags(signature.getSourceFile(), signature, t => !tagsSet.has(t));
38+
}
39+
});
40+
}));
41+
}
42+
});
43+
44+
function getDeleteAction(context: CodeFixContext, { name, signature, jsDocParameterTag }: Info) {
45+
const changes = textChanges.ChangeTracker.with(context, changeTracker =>
46+
changeTracker.filterJSDocTags(context.sourceFile, signature, t => t !== jsDocParameterTag));
47+
return createCodeFixAction(
48+
deleteUnmatchedParameter,
49+
changes,
50+
[Diagnostics.Delete_unused_param_tag_0, name.getText(context.sourceFile)],
51+
deleteUnmatchedParameter,
52+
Diagnostics.Delete_all_unused_param_tags
53+
);
54+
}
55+
56+
function getRenameAction(context: CodeFixContext, { name, signature, jsDocParameterTag }: Info) {
57+
if (!length(signature.parameters)) return undefined;
58+
59+
const sourceFile = context.sourceFile;
60+
const tags = getJSDocTags(signature);
61+
const names = new Set<__String>();
62+
for (const tag of tags) {
63+
if (isJSDocParameterTag(tag) && isIdentifier(tag.name)) {
64+
names.add(tag.name.escapedText);
65+
}
66+
}
67+
const parameterName = firstDefined(signature.parameters, p =>
68+
isIdentifier(p.name) && !names.has(p.name.escapedText) ? p.name.getText(sourceFile) : undefined);
69+
if (parameterName === undefined) return undefined;
70+
71+
const newJSDocParameterTag = factory.updateJSDocParameterTag(
72+
jsDocParameterTag,
73+
jsDocParameterTag.tagName,
74+
factory.createIdentifier(parameterName),
75+
jsDocParameterTag.isBracketed,
76+
jsDocParameterTag.typeExpression,
77+
jsDocParameterTag.isNameFirst,
78+
jsDocParameterTag.comment
79+
);
80+
const changes = textChanges.ChangeTracker.with(context, changeTracker =>
81+
changeTracker.replaceJSDocComment(sourceFile, signature, map(tags, t => t === jsDocParameterTag ? newJSDocParameterTag : t)));
82+
return createCodeFixActionWithoutFixAll(renameUnmatchedParameter, changes, [Diagnostics.Rename_param_tag_name_0_to_1, name.getText(sourceFile), parameterName]);
83+
}
84+
85+
interface Info {
86+
readonly signature: SignatureDeclaration;
87+
readonly jsDocParameterTag: JSDocParameterTag;
88+
readonly name: Identifier;
89+
}
90+
91+
function getInfo(sourceFile: SourceFile, pos: number): Info | undefined {
92+
const token = getTokenAtPosition(sourceFile, pos);
93+
if (token.parent && isJSDocParameterTag(token.parent) && isIdentifier(token.parent.name)) {
94+
const jsDocParameterTag = token.parent;
95+
const signature = getHostSignatureFromJSDoc(jsDocParameterTag);
96+
if (signature) {
97+
return { signature, name: token.parent.name, jsDocParameterTag };
98+
}
99+
}
100+
return undefined;
101+
}
102+
}

src/services/textChanges.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -495,29 +495,30 @@ namespace ts.textChanges {
495495
this.insertNodeAt(sourceFile, fnStart, tag, { preserveLeadingWhitespace: false, suffix: this.newLineCharacter + indent });
496496
}
497497

498+
private createJSDocText(sourceFile: SourceFile, node: HasJSDoc) {
499+
const comments = flatMap(node.jsDoc, jsDoc =>
500+
isString(jsDoc.comment) ? factory.createJSDocText(jsDoc.comment) : jsDoc.comment) as JSDocComment[];
501+
const jsDoc = singleOrUndefined(node.jsDoc);
502+
return jsDoc && positionsAreOnSameLine(jsDoc.pos, jsDoc.end, sourceFile) && length(comments) === 0 ? undefined :
503+
factory.createNodeArray(intersperse(comments, factory.createJSDocText("\n")));
504+
}
505+
506+
public replaceJSDocComment(sourceFile: SourceFile, node: HasJSDoc, tags: readonly JSDocTag[]) {
507+
this.insertJsdocCommentBefore(sourceFile, updateJSDocHost(node), factory.createJSDocComment(this.createJSDocText(sourceFile, node), factory.createNodeArray(tags)));
508+
}
509+
498510
public addJSDocTags(sourceFile: SourceFile, parent: HasJSDoc, newTags: readonly JSDocTag[]): void {
499-
const comments = flatMap(parent.jsDoc, j => typeof j.comment === "string" ? factory.createJSDocText(j.comment) : j.comment) as JSDocComment[];
500511
const oldTags = flatMapToMutable(parent.jsDoc, j => j.tags);
501512
const unmergedNewTags = newTags.filter(newTag => !oldTags.some((tag, i) => {
502513
const merged = tryMergeJsdocTags(tag, newTag);
503514
if (merged) oldTags[i] = merged;
504515
return !!merged;
505516
}));
506-
const tags = [...oldTags, ...unmergedNewTags];
507-
const jsDoc = singleOrUndefined(parent.jsDoc);
508-
const comment = jsDoc && positionsAreOnSameLine(jsDoc.pos, jsDoc.end, sourceFile) && !length(comments) ? undefined :
509-
factory.createNodeArray(intersperse(comments, factory.createJSDocText("\n")));
510-
const tag = factory.createJSDocComment(comment, factory.createNodeArray(tags));
511-
const host = updateJSDocHost(parent);
512-
this.insertJsdocCommentBefore(sourceFile, host, tag);
517+
this.replaceJSDocComment(sourceFile, parent, [...oldTags, ...unmergedNewTags]);
513518
}
514519

515520
public filterJSDocTags(sourceFile: SourceFile, parent: HasJSDoc, predicate: (tag: JSDocTag) => boolean): void {
516-
const comments = flatMap(parent.jsDoc, j => typeof j.comment === "string" ? factory.createJSDocText(j.comment) : j.comment) as JSDocComment[];
517-
const oldTags = flatMapToMutable(parent.jsDoc, j => j.tags);
518-
const tag = factory.createJSDocComment(factory.createNodeArray(intersperse(comments, factory.createJSDocText("\n"))), factory.createNodeArray([...(filter(oldTags, predicate) || emptyArray)]));
519-
const host = updateJSDocHost(parent);
520-
this.insertJsdocCommentBefore(sourceFile, host, tag);
521+
this.replaceJSDocComment(sourceFile, parent, filter(flatMapToMutable(parent.jsDoc, j => j.tags), predicate));
521522
}
522523

523524
public replaceRangeWithText(sourceFile: SourceFile, range: TextRange, text: string): void {

src/services/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
"codefixes/fixExtendsInterfaceBecomesImplements.ts",
8888
"codefixes/fixForgottenThisPropertyAccess.ts",
8989
"codefixes/fixInvalidJsxCharacters.ts",
90+
"codefixes/fixUnmatchedParameter.ts",
9091
"codefixes/fixUnusedIdentifier.ts",
9192
"codefixes/fixUnreachableCode.ts",
9293
"codefixes/fixUnusedLabel.ts",
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
tests/cases/conformance/jsdoc/0.js(14,20): error TS8024: JSDoc '@param' tag has name 's', but there is no parameter with that name.
2+
3+
4+
==== tests/cases/conformance/jsdoc/0.js (1 errors) ====
5+
// @ts-check
6+
/**
7+
* @param {number=} n
8+
* @param {string} [s]
9+
*/
10+
var x = function foo(n, s) {}
11+
var y;
12+
/**
13+
* @param {boolean!} b
14+
*/
15+
y = function bar(b) {}
16+
17+
/**
18+
* @param {string} s
19+
~
20+
!!! error TS8024: JSDoc '@param' tag has name 's', but there is no parameter with that name.
21+
*/
22+
var one = function (s) { }, two = function (untyped) { };
23+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @filename: a.ts
4+
/////**
5+
//// * @param {number} a
6+
//// * @param {number} b
7+
//// */
8+
////function foo() {}
9+
10+
verify.codeFixAvailable([
11+
{ description: "Delete unused '@param' tag 'a'" },
12+
{ description: "Delete unused '@param' tag 'b'" },
13+
]);
14+
15+
verify.codeFix({
16+
description: [ts.Diagnostics.Delete_unused_param_tag_0.message, "a"],
17+
index: 0,
18+
newFileContent:
19+
`/**
20+
* @param {number} b
21+
*/
22+
function foo() {}`,
23+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @filename: a.ts
4+
/////**
5+
//// * @param {number} a
6+
//// * @param {string} b
7+
//// */
8+
////function foo(a: number) {
9+
//// a;
10+
////}
11+
12+
verify.codeFixAvailable([
13+
{ description: "Delete unused '@param' tag 'b'" },
14+
]);
15+
16+
verify.codeFix({
17+
description: [ts.Diagnostics.Delete_unused_param_tag_0.message, "b"],
18+
index: 0,
19+
newFileContent:
20+
`/**
21+
* @param {number} a
22+
*/
23+
function foo(a: number) {
24+
a;
25+
}`
26+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @filename: a.ts
4+
/////**
5+
//// * @param {number} a
6+
//// * @param {string} b
7+
//// * @param {number} c
8+
//// */
9+
////function foo(a: number, c: number) {
10+
//// a;
11+
//// c;
12+
////}
13+
14+
verify.codeFixAvailable([
15+
{ description: "Delete unused '@param' tag 'b'" },
16+
]);
17+
18+
verify.codeFix({
19+
description: [ts.Diagnostics.Delete_unused_param_tag_0.message, "b"],
20+
index: 0,
21+
newFileContent:
22+
`/**
23+
* @param {number} a
24+
* @param {number} c
25+
*/
26+
function foo(a: number, c: number) {
27+
a;
28+
c;
29+
}`
30+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @filename: a.ts
4+
/////**
5+
//// * @param {number} a
6+
//// */
7+
////function foo() {}
8+
9+
verify.codeFixAvailable([
10+
{ description: "Delete unused '@param' tag 'a'" },
11+
]);
12+
13+
verify.codeFix({
14+
description: [ts.Diagnostics.Delete_unused_param_tag_0.message, "a"],
15+
index: 0,
16+
newFileContent:
17+
`/** */
18+
function foo() {}`
19+
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy