From a30ec40e11dd27f63f3c4fac81fc33a8b3e7e991 Mon Sep 17 00:00:00 2001 From: Oleksandr T Date: Sun, 12 Nov 2023 23:22:21 +0200 Subject: [PATCH] fix(48260): handle spread elements in the signature help --- src/services/completions.ts | 2 +- src/services/signatureHelp.ts | 67 +- src/services/stringCompletions.ts | 2 +- .../reference/signatureHelpRestArgs.baseline | 709 ++++++++++++++++++ .../cases/fourslash/signatureHelpRestArgs.ts | 13 + 5 files changed, 773 insertions(+), 20 deletions(-) create mode 100644 tests/baselines/reference/signatureHelpRestArgs.baseline create mode 100644 tests/cases/fourslash/signatureHelpRestArgs.ts diff --git a/src/services/completions.ts b/src/services/completions.ts index 11fbce92c6b41..10208ffe5960e 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -3113,7 +3113,7 @@ function getContextualType(previousToken: Node, position: number, sourceFile: So case SyntaxKind.OpenBraceToken: return isJsxExpression(parent) && !isJsxElement(parent.parent) && !isJsxFragment(parent.parent) ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined; default: - const argInfo = SignatureHelp.getArgumentInfoForCompletions(previousToken, position, sourceFile); + const argInfo = SignatureHelp.getArgumentInfoForCompletions(previousToken, position, sourceFile, checker); return argInfo ? // At `,`, treat this as the next argument after the comma. checker.getContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex + (previousToken.kind === SyntaxKind.CommaToken ? 1 : 0)) : diff --git a/src/services/signatureHelp.ts b/src/services/signatureHelp.ts index bde65d54583c5..2caf2aaa8530c 100644 --- a/src/services/signatureHelp.ts +++ b/src/services/signatureHelp.ts @@ -12,6 +12,7 @@ import { createTextSpanFromBounds, createTextSpanFromNode, Debug, + ElementFlags, EmitHint, emptyArray, Expression, @@ -49,6 +50,7 @@ import { isPropertyAccessExpression, isSourceFile, isSourceFileJS, + isSpreadElement, isTaggedTemplateExpression, isTemplateHead, isTemplateLiteralToken, @@ -58,6 +60,7 @@ import { JsxTagNameExpression, last, lastOrUndefined, + length, ListFormat, map, mapToDisplayParts, @@ -77,6 +80,7 @@ import { skipTrivia, SourceFile, spacePart, + SpreadElement, Symbol, SymbolDisplayPart, symbolToDisplayParts, @@ -85,6 +89,7 @@ import { TemplateExpression, TextSpan, tryCast, + TupleTypeReference, Type, TypeChecker, TypeParameter, @@ -272,25 +277,25 @@ export interface ArgumentInfoForCompletions { readonly argumentCount: number; } /** @internal */ -export function getArgumentInfoForCompletions(node: Node, position: number, sourceFile: SourceFile): ArgumentInfoForCompletions | undefined { - const info = getImmediatelyContainingArgumentInfo(node, position, sourceFile); +export function getArgumentInfoForCompletions(node: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): ArgumentInfoForCompletions | undefined { + const info = getImmediatelyContainingArgumentInfo(node, position, sourceFile, checker); return !info || info.isTypeParameterList || info.invocation.kind !== InvocationKind.Call ? undefined : { invocation: info.invocation.node, argumentCount: info.argumentCount, argumentIndex: info.argumentIndex }; } -function getArgumentOrParameterListInfo(node: Node, position: number, sourceFile: SourceFile): { readonly list: Node; readonly argumentIndex: number; readonly argumentCount: number; readonly argumentsSpan: TextSpan; } | undefined { - const info = getArgumentOrParameterListAndIndex(node, sourceFile); +function getArgumentOrParameterListInfo(node: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): { readonly list: Node; readonly argumentIndex: number; readonly argumentCount: number; readonly argumentsSpan: TextSpan; } | undefined { + const info = getArgumentOrParameterListAndIndex(node, sourceFile, checker); if (!info) return undefined; const { list, argumentIndex } = info; - const argumentCount = getArgumentCount(list, /*ignoreTrailingComma*/ isInString(sourceFile, position, node)); + const argumentCount = getArgumentCount(list, /*ignoreTrailingComma*/ isInString(sourceFile, position, node), checker); if (argumentIndex !== 0) { Debug.assertLessThan(argumentIndex, argumentCount); } const argumentsSpan = getApplicableSpanForArguments(list, sourceFile); return { list, argumentIndex, argumentCount, argumentsSpan }; } -function getArgumentOrParameterListAndIndex(node: Node, sourceFile: SourceFile): { readonly list: Node; readonly argumentIndex: number; } | undefined { +function getArgumentOrParameterListAndIndex(node: Node, sourceFile: SourceFile, checker: TypeChecker): { readonly list: Node; readonly argumentIndex: number; } | undefined { if (node.kind === SyntaxKind.LessThanToken || node.kind === SyntaxKind.OpenParenToken) { // Find the list that starts right *after* the < or ( token. // If the user has just opened a list, consider this item 0. @@ -304,7 +309,7 @@ function getArgumentOrParameterListAndIndex(node: Node, sourceFile: SourceFile): // - On the target of the call (parent.func) // - On the 'new' keyword in a 'new' expression const list = findContainingList(node); - return list && { list, argumentIndex: getArgumentIndex(list, node) }; + return list && { list, argumentIndex: getArgumentIndex(list, node, checker) }; } } @@ -312,7 +317,7 @@ function getArgumentOrParameterListAndIndex(node: Node, sourceFile: SourceFile): * Returns relevant information for the argument list and the current argument if we are * in the argument of an invocation; returns undefined otherwise. */ -function getImmediatelyContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo | undefined { +function getImmediatelyContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): ArgumentListInfo | undefined { const { parent } = node; if (isCallOrNewExpression(parent)) { const invocation = parent; @@ -331,7 +336,7 @@ function getImmediatelyContainingArgumentInfo(node: Node, position: number, sour // Case 3: // foo(a#, #b#) -> The token is buried inside a list, and should give signature help // Find out if 'node' is an argument, a type argument, or neither - const info = getArgumentOrParameterListInfo(node, position, sourceFile); + const info = getArgumentOrParameterListInfo(node, position, sourceFile, checker); if (!info) return undefined; const { list, argumentIndex, argumentCount, argumentsSpan } = info; const isTypeParameterList = !!parent.typeArguments && parent.typeArguments.pos === list.pos; @@ -397,7 +402,7 @@ function getImmediatelyContainingArgumentInfo(node: Node, position: number, sour } function getImmediatelyContainingArgumentOrContextualParameterInfo(node: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): ArgumentListInfo | undefined { - return tryGetParameterInfo(node, position, sourceFile, checker) || getImmediatelyContainingArgumentInfo(node, position, sourceFile); + return tryGetParameterInfo(node, position, sourceFile, checker) || getImmediatelyContainingArgumentInfo(node, position, sourceFile, checker); } function getHighestBinary(b: BinaryExpression): BinaryExpression { @@ -452,7 +457,7 @@ function getContextualSignatureLocationInfo(node: Node, sourceFile: SourceFile, case SyntaxKind.MethodDeclaration: case SyntaxKind.FunctionExpression: case SyntaxKind.ArrowFunction: - const info = getArgumentOrParameterListInfo(node, position, sourceFile); + const info = getArgumentOrParameterListInfo(node, position, sourceFile, checker); if (!info) return undefined; const { argumentIndex, argumentCount, argumentsSpan } = info; const contextualType = isMethodDeclaration(parent) ? checker.getContextualTypeForObjectLiteralElement(parent) : checker.getContextualType(parent as ParenthesizedExpression | FunctionExpression | ArrowFunction); @@ -476,7 +481,7 @@ function chooseBetterSymbol(s: Symbol): Symbol { : s; } -function getArgumentIndex(argumentsList: Node, node: Node) { +function getArgumentIndex(argumentsList: Node, node: Node, checker: TypeChecker) { // The list we got back can include commas. In the presence of errors it may // also just have nodes without commas. For example "Foo(a b c)" will have 3 // args without commas. We want to find what index we're at. So we count @@ -488,20 +493,39 @@ function getArgumentIndex(argumentsList: Node, node: Node) { // on. In that case, even if we're after the trailing comma, we'll still see // that trailing comma in the list, and we'll have generated the appropriate // arg index. + const args = argumentsList.getChildren(); let argumentIndex = 0; - for (const child of argumentsList.getChildren()) { + for (let pos = 0; pos < length(args); pos++) { + const child = args[pos]; if (child === node) { break; } - if (child.kind !== SyntaxKind.CommaToken) { - argumentIndex++; + if (isSpreadElement(child)) { + argumentIndex = argumentIndex + getSpreadElementCount(child, checker) + (pos > 0 ? pos : 0); + } + else { + if (child.kind !== SyntaxKind.CommaToken) { + argumentIndex++; + } } } - return argumentIndex; } -function getArgumentCount(argumentsList: Node, ignoreTrailingComma: boolean) { +function getSpreadElementCount(node: SpreadElement, checker: TypeChecker) { + const spreadType = checker.getTypeAtLocation(node.expression); + if (checker.isTupleType(spreadType)) { + const { elementFlags, fixedLength } = (spreadType as TupleTypeReference).target; + if (fixedLength === 0) { + return 0; + } + const firstOptionalIndex = findIndex(elementFlags, f => !(f & ElementFlags.Required)); + return firstOptionalIndex < 0 ? fixedLength : firstOptionalIndex; + } + return 0; +} + +function getArgumentCount(argumentsList: Node, ignoreTrailingComma: boolean, checker: TypeChecker) { // The argument count for a list is normally the number of non-comma children it has. // For example, if you have "Foo(a,b)" then there will be three children of the arg // list 'a' '' 'b'. So, in this case the arg count will be 2. However, there @@ -515,7 +539,14 @@ function getArgumentCount(argumentsList: Node, ignoreTrailingComma: boolean) { // arg count of 3. const listChildren = argumentsList.getChildren(); - let argumentCount = countWhere(listChildren, arg => arg.kind !== SyntaxKind.CommaToken); + let argumentCount = 0; + for (const child of listChildren) { + if (isSpreadElement(child)) { + argumentCount = argumentCount + getSpreadElementCount(child, checker); + } + } + + argumentCount = argumentCount + countWhere(listChildren, arg => arg.kind !== SyntaxKind.CommaToken); if (!ignoreTrailingComma && listChildren.length > 0 && last(listChildren).kind === SyntaxKind.CommaToken) { argumentCount++; } diff --git a/src/services/stringCompletions.ts b/src/services/stringCompletions.ts index b5c28a312d9ec..474c72be3ddec 100644 --- a/src/services/stringCompletions.ts +++ b/src/services/stringCompletions.ts @@ -407,7 +407,7 @@ function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringL case SyntaxKind.NewExpression: case SyntaxKind.JsxAttribute: if (!isRequireCallArgument(node) && !isImportCall(parent)) { - const argumentInfo = SignatureHelp.getArgumentInfoForCompletions(parent.kind === SyntaxKind.JsxAttribute ? parent.parent : node, position, sourceFile); + const argumentInfo = SignatureHelp.getArgumentInfoForCompletions(parent.kind === SyntaxKind.JsxAttribute ? parent.parent : node, position, sourceFile, typeChecker); // Get string literal completions from specialized signatures of the target // i.e. declare function f(a: 'A'); // f("/*completion position*/") diff --git a/tests/baselines/reference/signatureHelpRestArgs.baseline b/tests/baselines/reference/signatureHelpRestArgs.baseline new file mode 100644 index 0000000000000..9242555b3adcf --- /dev/null +++ b/tests/baselines/reference/signatureHelpRestArgs.baseline @@ -0,0 +1,709 @@ +// === SignatureHelp === +=== /tests/cases/fourslash/signatureHelpRestArgs.ts === +// function fn(a: number, b: number, c: number) {} +// const a = [1, 2] as const; +// const b = [1] as const; +// +// fn(...a, ); +// ^ +// | ---------------------------------------------------------------------- +// | fn(a: number, b: number, **c: number**): void +// | ---------------------------------------------------------------------- +// fn(, ...a); +// ^ +// | ---------------------------------------------------------------------- +// | fn(**a: number**, b: number, c: number): void +// | ---------------------------------------------------------------------- +// +// fn(...b, ); +// ^ +// | ---------------------------------------------------------------------- +// | fn(a: number, **b: number**, c: number): void +// | ---------------------------------------------------------------------- +// fn(, ...b, ); +// ^ +// | ---------------------------------------------------------------------- +// | fn(**a: number**, b: number, c: number): void +// | ---------------------------------------------------------------------- +// ^ +// | ---------------------------------------------------------------------- +// | fn(a: number, b: number, **c: number**): void +// | ---------------------------------------------------------------------- + +[ + { + "marker": { + "fileName": "/tests/cases/fourslash/signatureHelpRestArgs.ts", + "position": 109, + "name": "1" + }, + "item": { + "items": [ + { + "isVariadic": false, + "prefixDisplayParts": [ + { + "text": "fn", + "kind": "functionName" + }, + { + "text": "(", + "kind": "punctuation" + } + ], + "suffixDisplayParts": [ + { + "text": ")", + "kind": "punctuation" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "void", + "kind": "keyword" + } + ], + "separatorDisplayParts": [ + { + "text": ",", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + } + ], + "parameters": [ + { + "name": "a", + "documentation": [], + "displayParts": [ + { + "text": "a", + "kind": "parameterName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "number", + "kind": "keyword" + } + ], + "isOptional": false, + "isRest": false + }, + { + "name": "b", + "documentation": [], + "displayParts": [ + { + "text": "b", + "kind": "parameterName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "number", + "kind": "keyword" + } + ], + "isOptional": false, + "isRest": false + }, + { + "name": "c", + "documentation": [], + "displayParts": [ + { + "text": "c", + "kind": "parameterName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "number", + "kind": "keyword" + } + ], + "isOptional": false, + "isRest": false + } + ], + "documentation": [], + "tags": [] + } + ], + "applicableSpan": { + "start": 103, + "length": 6 + }, + "selectedItemIndex": 0, + "argumentIndex": 2, + "argumentCount": 4 + } + }, + { + "marker": { + "fileName": "/tests/cases/fourslash/signatureHelpRestArgs.ts", + "position": 115, + "name": "2" + }, + "item": { + "items": [ + { + "isVariadic": false, + "prefixDisplayParts": [ + { + "text": "fn", + "kind": "functionName" + }, + { + "text": "(", + "kind": "punctuation" + } + ], + "suffixDisplayParts": [ + { + "text": ")", + "kind": "punctuation" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "void", + "kind": "keyword" + } + ], + "separatorDisplayParts": [ + { + "text": ",", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + } + ], + "parameters": [ + { + "name": "a", + "documentation": [], + "displayParts": [ + { + "text": "a", + "kind": "parameterName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "number", + "kind": "keyword" + } + ], + "isOptional": false, + "isRest": false + }, + { + "name": "b", + "documentation": [], + "displayParts": [ + { + "text": "b", + "kind": "parameterName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "number", + "kind": "keyword" + } + ], + "isOptional": false, + "isRest": false + }, + { + "name": "c", + "documentation": [], + "displayParts": [ + { + "text": "c", + "kind": "parameterName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "number", + "kind": "keyword" + } + ], + "isOptional": false, + "isRest": false + } + ], + "documentation": [], + "tags": [] + } + ], + "applicableSpan": { + "start": 115, + "length": 6 + }, + "selectedItemIndex": 0, + "argumentIndex": 0, + "argumentCount": 3 + } + }, + { + "marker": { + "fileName": "/tests/cases/fourslash/signatureHelpRestArgs.ts", + "position": 134, + "name": "3" + }, + "item": { + "items": [ + { + "isVariadic": false, + "prefixDisplayParts": [ + { + "text": "fn", + "kind": "functionName" + }, + { + "text": "(", + "kind": "punctuation" + } + ], + "suffixDisplayParts": [ + { + "text": ")", + "kind": "punctuation" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "void", + "kind": "keyword" + } + ], + "separatorDisplayParts": [ + { + "text": ",", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + } + ], + "parameters": [ + { + "name": "a", + "documentation": [], + "displayParts": [ + { + "text": "a", + "kind": "parameterName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "number", + "kind": "keyword" + } + ], + "isOptional": false, + "isRest": false + }, + { + "name": "b", + "documentation": [], + "displayParts": [ + { + "text": "b", + "kind": "parameterName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "number", + "kind": "keyword" + } + ], + "isOptional": false, + "isRest": false + }, + { + "name": "c", + "documentation": [], + "displayParts": [ + { + "text": "c", + "kind": "parameterName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "number", + "kind": "keyword" + } + ], + "isOptional": false, + "isRest": false + } + ], + "documentation": [], + "tags": [] + } + ], + "applicableSpan": { + "start": 128, + "length": 6 + }, + "selectedItemIndex": 0, + "argumentIndex": 1, + "argumentCount": 3 + } + }, + { + "marker": { + "fileName": "/tests/cases/fourslash/signatureHelpRestArgs.ts", + "position": 140, + "name": "4" + }, + "item": { + "items": [ + { + "isVariadic": false, + "prefixDisplayParts": [ + { + "text": "fn", + "kind": "functionName" + }, + { + "text": "(", + "kind": "punctuation" + } + ], + "suffixDisplayParts": [ + { + "text": ")", + "kind": "punctuation" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "void", + "kind": "keyword" + } + ], + "separatorDisplayParts": [ + { + "text": ",", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + } + ], + "parameters": [ + { + "name": "a", + "documentation": [], + "displayParts": [ + { + "text": "a", + "kind": "parameterName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "number", + "kind": "keyword" + } + ], + "isOptional": false, + "isRest": false + }, + { + "name": "b", + "documentation": [], + "displayParts": [ + { + "text": "b", + "kind": "parameterName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "number", + "kind": "keyword" + } + ], + "isOptional": false, + "isRest": false + }, + { + "name": "c", + "documentation": [], + "displayParts": [ + { + "text": "c", + "kind": "parameterName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "number", + "kind": "keyword" + } + ], + "isOptional": false, + "isRest": false + } + ], + "documentation": [], + "tags": [] + } + ], + "applicableSpan": { + "start": 140, + "length": 8 + }, + "selectedItemIndex": 0, + "argumentIndex": 0, + "argumentCount": 3 + } + }, + { + "marker": { + "fileName": "/tests/cases/fourslash/signatureHelpRestArgs.ts", + "position": 148, + "name": "5" + }, + "item": { + "items": [ + { + "isVariadic": false, + "prefixDisplayParts": [ + { + "text": "fn", + "kind": "functionName" + }, + { + "text": "(", + "kind": "punctuation" + } + ], + "suffixDisplayParts": [ + { + "text": ")", + "kind": "punctuation" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "void", + "kind": "keyword" + } + ], + "separatorDisplayParts": [ + { + "text": ",", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + } + ], + "parameters": [ + { + "name": "a", + "documentation": [], + "displayParts": [ + { + "text": "a", + "kind": "parameterName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "number", + "kind": "keyword" + } + ], + "isOptional": false, + "isRest": false + }, + { + "name": "b", + "documentation": [], + "displayParts": [ + { + "text": "b", + "kind": "parameterName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "number", + "kind": "keyword" + } + ], + "isOptional": false, + "isRest": false + }, + { + "name": "c", + "documentation": [], + "displayParts": [ + { + "text": "c", + "kind": "parameterName" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "number", + "kind": "keyword" + } + ], + "isOptional": false, + "isRest": false + } + ], + "documentation": [], + "tags": [] + } + ], + "applicableSpan": { + "start": 140, + "length": 8 + }, + "selectedItemIndex": 0, + "argumentIndex": 2, + "argumentCount": 3 + } + } +] \ No newline at end of file diff --git a/tests/cases/fourslash/signatureHelpRestArgs.ts b/tests/cases/fourslash/signatureHelpRestArgs.ts new file mode 100644 index 0000000000000..49f5088b91268 --- /dev/null +++ b/tests/cases/fourslash/signatureHelpRestArgs.ts @@ -0,0 +1,13 @@ +/// + +////function fn(a: number, b: number, c: number) {} +////const a = [1, 2] as const; +////const b = [1] as const; +//// +////fn(...a, /*1*/); +////fn(/*2*/, ...a); +//// +////fn(...b, /*3*/); +////fn(/*4*/, ...b, /*5*/); + +verify.baselineSignatureHelp();