Provide snippet completions for @param in JSDoc (#53260)

This commit is contained in:
Gabriela Araujo Britto
2023-04-04 15:35:09 -03:00
committed by GitHub
parent a280cafbf8
commit e83d61398e
8 changed files with 34214 additions and 3 deletions

View File

@@ -45703,6 +45703,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return symbol ? getDeclaredTypeOfSymbol(symbol) : errorType;
}
if (isBindingElement(node)) {
return getTypeForVariableLikeDeclaration(node, /*includeOptionality*/ true, CheckMode.Normal) || errorType;
}
if (isDeclaration(node)) {
// In this case, we call getSymbolOfNode instead of getSymbolAtLocation because it is a declaration
const symbol = getSymbolOfDeclaration(node);

View File

@@ -3,6 +3,8 @@ import {
addToSeen,
append,
BinaryExpression,
BindingElement,
BindingPattern,
BreakOrContinueStatement,
CancellationToken,
canUsePropertyAccess,
@@ -32,6 +34,7 @@ import {
concatenate,
ConstructorDeclaration,
ContextFlags,
countWhere,
createModuleSpecifierResolutionHost,
createPackageJsonImportFilter,
createPrinter,
@@ -44,6 +47,8 @@ import {
Diagnostics,
diagnosticToString,
displayPart,
DotDotDotToken,
EmitFlags,
EmitHint,
EmitTextWriter,
EntityName,
@@ -79,6 +84,7 @@ import {
getEscapedTextOfIdentifierOrLiteral,
getExportInfoMap,
getFormatCodeSettingsForWriting,
getJSDocParameterTags,
getLanguageVariant,
getLeftmostAccessExpression,
getLineAndCharacterOfPosition,
@@ -309,6 +315,7 @@ import {
ScriptElementKindModifier,
ScriptTarget,
SemanticMeaning,
setEmitFlags,
setSnippetElement,
shouldUseUriStyleNodeCoreModules,
SignatureHelp,
@@ -344,6 +351,7 @@ import {
tokenToString,
tryCast,
tryGetImportFromModuleSpecifier,
tryGetTextOfPropertyName,
Type,
TypeChecker,
TypeElement,
@@ -669,9 +677,10 @@ export function getCompletionsAtPosition(
}
const compilerOptions = program.getCompilerOptions();
const checker = program.getTypeChecker();
// If the request is a continuation of an earlier `isIncomplete` response,
// we can continue it from the cached previous response.
const compilerOptions = program.getCompilerOptions();
const incompleteCompletionsCache = preferences.allowIncompleteCompletions ? host.getIncompleteCompletionsCache?.() : undefined;
if (incompleteCompletionsCache && completionKind === CompletionTriggerKind.TriggerForIncompleteCompletions && previousToken && isIdentifier(previousToken)) {
const incompleteContinuation = continuePreviousIncompleteResponse(incompleteCompletionsCache, sourceFile, previousToken, program, host, preferences, cancellationToken, position);
@@ -707,10 +716,26 @@ export function getCompletionsAtPosition(
return response;
case CompletionDataKind.JsDocTagName:
// If the current position is a jsDoc tag name, only tag names should be provided for completion
return jsdocCompletionInfo(JsDoc.getJSDocTagNameCompletions());
return jsdocCompletionInfo([
...JsDoc.getJSDocTagNameCompletions(),
...getJSDocParameterCompletions(
sourceFile,
position,
checker,
compilerOptions,
preferences,
/*tagNameOnly*/ true)]);
case CompletionDataKind.JsDocTag:
// If the current position is a jsDoc tag, only tags should be provided for completion
return jsdocCompletionInfo(JsDoc.getJSDocTagCompletions());
return jsdocCompletionInfo([
...JsDoc.getJSDocTagCompletions(),
...getJSDocParameterCompletions(
sourceFile,
position,
checker,
compilerOptions,
preferences,
/*tagNameOnly*/ false)]);
case CompletionDataKind.JsDocParameterName:
return jsdocCompletionInfo(JsDoc.getJSDocParameterNameCompletions(completionData.tag));
case CompletionDataKind.Keywords:
@@ -827,6 +852,301 @@ function jsdocCompletionInfo(entries: CompletionEntry[]): CompletionInfo {
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries };
}
function getJSDocParameterCompletions(
sourceFile: SourceFile,
position: number,
checker: TypeChecker,
options: CompilerOptions,
preferences: UserPreferences,
tagNameOnly: boolean): CompletionEntry[] {
const currentToken = getTokenAtPosition(sourceFile, position);
if (!isJSDocTag(currentToken) && !isJSDoc(currentToken)) {
return [];
}
const jsDoc = isJSDoc(currentToken) ? currentToken : currentToken.parent;
if (!isJSDoc(jsDoc)) {
return [];
}
const func = jsDoc.parent;
if (!isFunctionLike(func)) {
return [];
}
const isJs = isSourceFileJS(sourceFile);
const isSnippet = preferences.includeCompletionsWithSnippetText || undefined;
const paramTagCount = countWhere(jsDoc.tags, tag => isJSDocParameterTag(tag) && tag.getEnd() <= position);
return mapDefined(func.parameters, param => {
if (getJSDocParameterTags(param).length) {
return undefined; // Parameter is already annotated.
}
if (isIdentifier(param.name)) { // Named parameter
const tabstopCounter = { tabstop: 1 };
const paramName = param.name.text;
let displayText =
getJSDocParamAnnotation(
paramName,
param.initializer,
param.dotDotDotToken,
isJs,
/*isObject*/ false,
/*isSnippet*/ false,
checker,
options,
preferences);
let snippetText = isSnippet
? getJSDocParamAnnotation(
paramName,
param.initializer,
param.dotDotDotToken,
isJs,
/*isObject*/ false,
/*isSnippet*/ true,
checker,
options,
preferences,
tabstopCounter)
: undefined;
if (tagNameOnly) { // Remove `@`
displayText = displayText.slice(1);
if (snippetText) snippetText = snippetText.slice(1);
}
return {
name: displayText,
kind: ScriptElementKind.parameterElement,
sortText: SortText.LocationPriority,
insertText: isSnippet ? snippetText : undefined,
isSnippet,
};
}
else if (param.parent.parameters.indexOf(param) === paramTagCount) { // Destructuring parameter; do it positionally
const paramPath = `param${paramTagCount}`;
const displayTextResult =
generateJSDocParamTagsForDestructuring(
paramPath,
param.name,
param.initializer,
param.dotDotDotToken,
isJs,
/*isSnippet*/ false,
checker,
options,
preferences,);
const snippetTextResult = isSnippet
? generateJSDocParamTagsForDestructuring(
paramPath,
param.name,
param.initializer,
param.dotDotDotToken,
isJs,
/*isSnippet*/ true,
checker,
options,
preferences,)
: undefined;
let displayText = displayTextResult.join(getNewLineCharacter(options) + "* ");
let snippetText = snippetTextResult?.join(getNewLineCharacter(options) + "* ");
if (tagNameOnly) { // Remove `@`
displayText = displayText.slice(1);
if (snippetText) snippetText = snippetText.slice(1);
}
return {
name: displayText,
kind: ScriptElementKind.parameterElement,
sortText: SortText.LocationPriority,
insertText: isSnippet ? snippetText : undefined,
isSnippet,
};
}
});
}
function generateJSDocParamTagsForDestructuring(
path: string,
pattern: BindingPattern,
initializer: Expression | undefined,
dotDotDotToken: DotDotDotToken | undefined,
isJs: boolean,
isSnippet: boolean,
checker: TypeChecker,
options: CompilerOptions,
preferences: UserPreferences): string[] {
if (!isJs) {
return [
getJSDocParamAnnotation(
path,
initializer,
dotDotDotToken,
isJs,
/*isObject*/ false,
isSnippet,
checker,
options,
preferences,
{ tabstop: 1 })
];
}
return patternWorker(path, pattern, initializer, dotDotDotToken, { tabstop: 1 });
function patternWorker(
path: string,
pattern: BindingPattern,
initializer: Expression | undefined,
dotDotDotToken: DotDotDotToken | undefined,
counter: TabStopCounter): string[] {
if (isObjectBindingPattern(pattern) && !dotDotDotToken) {
const oldTabstop = counter.tabstop;
const childCounter = { tabstop: oldTabstop };
const rootParam =
getJSDocParamAnnotation(
path,
initializer,
dotDotDotToken,
isJs,
/*isObject*/ true,
isSnippet,
checker,
options,
preferences,
childCounter);
let childTags: string[] | undefined = [];
for (const element of pattern.elements) {
const elementTags = elementWorker(path, element, childCounter);
if (!elementTags) {
childTags = undefined;
break;
}
else {
childTags.push(...elementTags);
}
}
if (childTags) {
counter.tabstop = childCounter.tabstop;
return [rootParam, ...childTags];
}
}
return [
getJSDocParamAnnotation(
path,
initializer,
dotDotDotToken,
isJs,
/*isObject*/ false,
isSnippet,
checker,
options,
preferences,
counter)
];
}
// Assumes binding element is inside object binding pattern.
// We can't really deeply annotate an array binding pattern.
function elementWorker(path: string, element: BindingElement, counter: TabStopCounter): string[] | undefined {
if ((!element.propertyName && isIdentifier(element.name)) || isIdentifier(element.name)) { // `{ b }` or `{ b: newB }`
const propertyName = element.propertyName ? tryGetTextOfPropertyName(element.propertyName) : element.name.text;
if (!propertyName) {
return undefined;
}
const paramName = `${path}.${propertyName}`;
return [
getJSDocParamAnnotation(
paramName,
element.initializer,
element.dotDotDotToken,
isJs,
/*isObject*/ false,
isSnippet,
checker,
options,
preferences,
counter)];
}
else if (element.propertyName) { // `{ b: {...} }` or `{ b: [...] }`
const propertyName = tryGetTextOfPropertyName(element.propertyName);
return propertyName
&& patternWorker(`${path}.${propertyName}`, element.name, element.initializer, element.dotDotDotToken, counter);
}
return undefined;
}
}
interface TabStopCounter {
tabstop: number;
}
function getJSDocParamAnnotation(
paramName: string,
initializer: Expression | undefined,
dotDotDotToken: DotDotDotToken | undefined,
isJs: boolean,
isObject: boolean,
isSnippet: boolean,
checker: TypeChecker,
options: CompilerOptions,
preferences: UserPreferences,
tabstopCounter?: TabStopCounter) {
if (isSnippet) {
Debug.assertIsDefined(tabstopCounter);
}
if (initializer) {
paramName = getJSDocParamNameWithInitializer(paramName, initializer);
}
if (isSnippet) {
paramName = escapeSnippetText(paramName);
}
if (isJs) {
let type = "*";
if (isObject) {
Debug.assert(!dotDotDotToken, `Cannot annotate a rest parameter with type 'Object'.`);
type = "Object";
}
else {
if (initializer) {
const inferredType = checker.getTypeAtLocation(initializer.parent);
if (!(inferredType.flags & (TypeFlags.Any | TypeFlags.Void))) {
const sourceFile = initializer.getSourceFile();
const quotePreference = getQuotePreference(sourceFile, preferences);
const builderFlags = (quotePreference === QuotePreference.Single ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : NodeBuilderFlags.None);
const typeNode = checker.typeToTypeNode(inferredType, findAncestor(initializer, isFunctionLike), builderFlags);
if (typeNode) {
const printer = isSnippet
? createSnippetPrinter({
removeComments: true,
module: options.module,
target: options.target,
})
: createPrinter({
removeComments: true,
module: options.module,
target: options.target
});
setEmitFlags(typeNode, EmitFlags.SingleLine);
type = printer.printNode(EmitHint.Unspecified, typeNode, sourceFile);
}
}
}
if (isSnippet && type === "*") {
type = `\${${tabstopCounter!.tabstop++}:${type}}`;
}
}
const dotDotDot = !isObject && dotDotDotToken ? "..." : "";
const description = isSnippet ? `\${${tabstopCounter!.tabstop++}}` : "";
return `@param {${dotDotDot}${type}} ${paramName} ${description}`;
}
else {
const description = isSnippet ? `\${${tabstopCounter!.tabstop++}}` : "";
return `@param ${paramName} ${description}`;
}
}
function getJSDocParamNameWithInitializer(paramName: string, initializer: Expression): string {
const initializerText = initializer.getText().trim();
if (initializerText.includes("\n") || initializerText.length > 80) {
return `[${paramName}]`;
}
return `[${paramName}=${initializerText}]`;
}
function keywordToCompletionEntry(keyword: TokenSyntaxKind) {
return {
name: tokenToString(keyword)!,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,127 @@
///<reference path="fourslash.ts" />
// @allowJs: true
// @Filename: a.ts
//// /**
//// * @para/*0*/
//// */
//// function printValue(value, maximumFractionDigits) {}
////
//// /**
//// * @p/*a*/
//// */
//// function aa({ a = 1 }, b: string) {
//// a;
//// }
////
//// /**
//// * /*b*/
//// */
//// function bb(b: string) {}
////
//// /**
//// *
//// * @p/*c*/
//// */
//// function cc({ b: { a, c } = { a: 1, c: 3 } }) {
////
//// }
////
//// /**
//// *
//// * @p/*d*/
//// */
//// function dd({ a: { b, c }, d: [e, f] }: { a: { b: number, c: number }, d: [string, string] }) {
////
//// }
// @Filename: b.js
//// /**
//// * @p/*ja*/
//// */
//// function aa({ a = 1 }, b) {
//// a;
//// }
////
//// /**
//// * /*jb*/
//// */
//// function bb(b) {}
////
//// /**
//// *
//// * @p/*jc*/
//// */
//// function cc({ b: { a, c } = { a: 1, c: 3 } }) {
////
//// }
////
//// /**
//// *
//// * @p/*jd*/
//// */
//// function dd({ a: { b, c }, d: [e, f] }) {
////
//// }
////
//// const someconst = "aa";
//// /**
//// *
//// * @p/*je*/
//// */
//// function ee({ [someconst]: b }) {
////
//// }
////
//// /**
//// *
//// * @p/*jf*/
//// */
//// function ff({ "a": b }) {
////
//// }
////
//// /**
//// *
//// * @p/*jg*/
//// */
//// function gg(a, { b }) {
////
//// }
////
//// /**
//// *
//// * @param {boolean} a a's description
//// * @p/*jh*/
//// */
//// function hh(a, { b }) {
////
//// }
//// /**
//// *
//// * @p/*ji*/
//// */
//// function ii({ b, ...c }, ...a) {}
////
//// /**
//// *
//// * @p/*jj*/
//// */
//// function jj(...{ length }) {}
////
//// /**
//// *
//// * @p/*jk*/
//// */
//// function kk(...a) {}
////
//// function reallylongfunctionnameabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl(a) {}
//// /**
//// *
//// * @p/*jl*/
//// */
//// function ll(a = reallylongfunctionnameabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl("")) {}
////}
verify.baselineCompletions();

View File

@@ -0,0 +1,39 @@
///<reference path="fourslash.ts" />
// @allowJs: true
// @Filename: a.ts
//// /**
//// * /*b*/
//// */
//// function bb(b: string) {}
// @Filename: b.js
//// /**
//// * /*jb*/
//// */
//// function bb(b) {}
////
//// /**
//// *
//// * @p/*jc*/
//// */
//// function cc({ b: { a, c } = { a: 1, c: 3 } }) {
////
//// }
////
//// /**
//// *
//// * @p/*jd*/
//// */
//// function dd(...a) {}
////
//// /**
//// * @p/*z*/
//// */
//// function zz(a = 3) {}
verify.baselineCompletions({
includeCompletionsWithSnippetText: true,
});

View File

@@ -0,0 +1,39 @@
///<reference path="fourslash.ts" />
// Infer types from initializer
// @allowJs: true
// @Filename: a.js
//// /**
//// * @p/*z*/
//// */
//// function zz(a = 3) {}
//// /**
//// * @p/*y*/
//// */
//// function yy({ a = 3 }) {}
//// /**
//// * @p/*x*/
//// */
//// function xx({ a, o: { b, c: [d, e = 1] }}) {}
//// /**
//// * @p/*w*/
//// */
//// function ww({ a, o: { b, c: [d, e] = [1, true] }}) {}
//// /**
//// * @p/*v*/
//// */
//// function vv({ a = [1, true] }) {}
//// function random(a) { return a }
//// /**
//// * @p/*u*/
//// */
//// function uu({ a = random() }) {}
verify.baselineCompletions();