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
+4
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);
+323 -3
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
@@ -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();
@@ -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,
});
@@ -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();