-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Merge existing JSDoc comments #27978
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Note that part of the code, in formatting.ts, is cloned but should be extracted to a function instead.
But I see 4 failures with whitespace, so perhaps not.
The indentation code is very complex so I'm just going to avoid breaking our single-line tests for now, plus add a simple jsdoc test to show that multiline jsdoc indentation isn't destroyed in the common case.
Still doesn't merge correctly though
(not for @param yet)
Not all of them; I got cold feet since I'll have to write tests for them. I'll do that tomorrow.
And single tests (at least) for all tags
(Plus a little more in textChanges.ts)
instead of space.
} | ||
} | ||
syntheticTags.forEach(v => tags.push(v)); | ||
const tag = createJSDocComment(comments.join("\n"), createNodeArray(tags)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"\n" [](start = 53, length = 4)
Some later step will replace this with a platform-specific line break?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the formatting code re-breaks lines and reindents them as well.
* 1 | ||
* 2 | ||
* 3 | ||
* @returns {number} First one |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@returns {number} First one [](start = 7, length = 27)
Based on our experience with imports, it seems likely that users will expect the merged JS Doc to be sorted (e.g. all of the @returns
s together at the end).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. I filed #28090 to track that.
I skimmed it and it seems sensible. Thanks! In reply to: 431172674 [](ancestors = 431172674) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be a test where the codefix infers types for multiple parameters / return type of the same function, since that can cause issues with codeFixAll.
src/compiler/emitter.ts
Outdated
} | ||
|
||
function emitJSDocAugmentsTag(tag: JSDocAugmentsTag) { | ||
write("@extends"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be better to always use tagName
since this might be @augments
or @extends
. Also you could share more code between these functions since you can always unconditionally emit tagName
first rather than needing a different string literal for each tag kind.
} | ||
const typeTags = createMap<JSDocTypeTag | JSDocReturnTag>(); | ||
const typeTag = isGetAccessorDeclaration(declaration) ? createJSDocReturnTag(typeNode, "") : createJSDocTypeTag(typeNode, ""); | ||
typeTags.set("TAG", typeTag); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See earlier comment at "UNUSED"
1. Refactor emit code into smaller functions. 2. Preceding-whitespace utility is slightly easier to use. 3. Better casts and types in inferFromUsage make it easier to read.
I think this is ready for another look now. |
typeTags.set("TAG" as __String, typeTag); | ||
insertMergedJSDoc( | ||
typeTags, changes, sourceFile, parent, | ||
t => t.kind === typeTag.kind && "TAG" as __String, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO using the type __String | false
is just confusing -- I was beginning to question my knowledge of operator precedence. Would be more clear if getKey
took __String | undefined
and this were t.kind === typeTag.kind ? "TAG" : undefined
.
return; | ||
} | ||
const paramTags = createUnderscoreEscapedMap<JSDocParameterTag>(); | ||
forEach(parameterInferences, inference => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is typed as a non-nullable array so this could just be a for loop? Also, this function never writes to parameterInferences
so that could be a ReadonlyArray
.
const key = getKey(existing); | ||
const synthetic = key && syntheticTags.get(key); | ||
if (synthetic) { | ||
merge(synthetic, existing as T); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be simpler as tags.push(merge(synthetic, existing as T));
where we create a new synthetic node instead of mutating an existing one -- I believe there were plans to make nodes immutable in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also might want to comment the reason we push to tags
here instead of doing it at the bottom -- looks like we're trying to preserve the original order of tags as much as possible.
if (synthetic) { | ||
merge(synthetic, existing as T); | ||
tags.push(synthetic); | ||
syntheticTags.delete(key as __String); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure we can avoid a cast here. See my earlier comment on getKey
.
Here's my take on improving jsdoc comment merging: function annotateJSDocParameters(changes: textChanges.ChangeTracker, sourceFile: SourceFile, parameterInferences: ReadonlyArray<ParameterInference>, program: Program, host: LanguageServiceHost): void {
const signature = parameterInferences.length && parameterInferences[0].declaration.parent;
if (!signature) {
return;
}
const paramTags = mapDefined(group(parameterInferences, i => String(getNodeId(i.declaration))), inferences => {
const param = first(inferences).declaration;
// only infer parameters that have (1) no type and (2) an accessible inferred type
if (param.initializer || getJSDocType(param) || !isIdentifier(param.name)) return;
// TODO: improve combining inferences
const type = firstDefined(inferences, i => i.type);
const isOptional = inferences.some(i => !!i.isOptional);
const typeNode = type && getTypeNodeIfAccessible(type, param, program, host);
return typeNode && createJSDocParamTag(createJSDocTypeExpression(typeNode), param.name, isOptional, "");
});
addJsdocTags(changes, sourceFile, signature, paramTags);
}
function addJsdocTags(changes: textChanges.ChangeTracker, sourceFile: SourceFile, parent: HasJSDoc, newTags: ReadonlyArray<JSDocTag>): void {
const comments = mapDefined(parent.jsDoc, j => j.comment);
const oldTags = flatMap(parent.jsDoc, j => j.tags);
const unmergedNewTags = newTags.filter(newTag => !oldTags || !oldTags.some((tag, i) => {
const merged = tryMergeJsdocTags(tag, newTag);
if (merged) oldTags[i] = merged;
return !!merged;
}));
const tag = createJSDocComment(comments.join("\n"), createNodeArray([...(oldTags || emptyArray), ...unmergedNewTags]));
changes.insertJsdocCommentBefore(sourceFile, parent, tag);
}
function tryMergeJsdocTags(oldTag: JSDocTag, newTag: JSDocTag): JSDocTag | undefined {
if (oldTag.kind !== newTag.kind) return undefined;
switch (oldTag.kind) {
case SyntaxKind.JSDocParameterTag: {
const oldParam = oldTag as JSDocParameterTag, newParam = newTag as JSDocParameterTag;
return isIdentifier(oldParam.name) && isIdentifier(newParam.name) && oldParam.name.escapedText === newParam.name.escapedText
? createJSDocParamTag(newParam.typeExpression, newParam.name, newParam.isBracketed, oldParam.comment)
: undefined;
}
case SyntaxKind.JSDocReturnTag:
return createJSDocReturnTag((newTag as JSDocReturnTag).typeExpression, oldTag.comment);
default:
return undefined;
}
}
|
I like your suggestion, although I note that the (Edit: And the order isn't changed from the original parameter list) |
This PR is a followup to the infer-from-usage codefix PR. Instead of generating comment text in textChanges.ts, it uses the existing infrastructure in the emitter and formatter. That means that:
It additionally means that
4. inferFromUsage.ts constructs JSDoc tags instead of its own data structures.
As a result, you'll see a lot of new, very predictable code in the emitter and lots of deletes in textChanges.ts.
I also wrote code to merge with existing JSDoc comments. The biggest change to existing comments will be the merging of multiple JSDoc comments into one, eg
Will become