Skip to content

Commit 77acb4a

Browse files
a-tarasyuksandersn
authored andcommitted
fix(48260): Incorrect parameter hint is highlighted when arguments contain spread syntax (microsoft#56372)
Co-authored-by: Nathan Shively-Sanders <[email protected]>
1 parent 7e21131 commit 77acb4a

File tree

5 files changed

+773
-20
lines changed

5 files changed

+773
-20
lines changed

src/services/completions.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3122,7 +3122,7 @@ function getContextualType(previousToken: Node, position: number, sourceFile: So
31223122
case SyntaxKind.OpenBraceToken:
31233123
return isJsxExpression(parent) && !isJsxElement(parent.parent) && !isJsxFragment(parent.parent) ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined;
31243124
default:
3125-
const argInfo = SignatureHelp.getArgumentInfoForCompletions(previousToken, position, sourceFile);
3125+
const argInfo = SignatureHelp.getArgumentInfoForCompletions(previousToken, position, sourceFile, checker);
31263126
return argInfo ?
31273127
// At `,`, treat this as the next argument after the comma.
31283128
checker.getContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex + (previousToken.kind === SyntaxKind.CommaToken ? 1 : 0)) :

src/services/signatureHelp.ts

+49-18
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
createTextSpanFromBounds,
1313
createTextSpanFromNode,
1414
Debug,
15+
ElementFlags,
1516
EmitHint,
1617
emptyArray,
1718
Expression,
@@ -49,6 +50,7 @@ import {
4950
isPropertyAccessExpression,
5051
isSourceFile,
5152
isSourceFileJS,
53+
isSpreadElement,
5254
isTaggedTemplateExpression,
5355
isTemplateHead,
5456
isTemplateLiteralToken,
@@ -58,6 +60,7 @@ import {
5860
JsxTagNameExpression,
5961
last,
6062
lastOrUndefined,
63+
length,
6164
ListFormat,
6265
map,
6366
mapToDisplayParts,
@@ -77,6 +80,7 @@ import {
7780
skipTrivia,
7881
SourceFile,
7982
spacePart,
83+
SpreadElement,
8084
Symbol,
8185
SymbolDisplayPart,
8286
symbolToDisplayParts,
@@ -85,6 +89,7 @@ import {
8589
TemplateExpression,
8690
TextSpan,
8791
tryCast,
92+
TupleTypeReference,
8893
Type,
8994
TypeChecker,
9095
TypeParameter,
@@ -272,25 +277,25 @@ export interface ArgumentInfoForCompletions {
272277
readonly argumentCount: number;
273278
}
274279
/** @internal */
275-
export function getArgumentInfoForCompletions(node: Node, position: number, sourceFile: SourceFile): ArgumentInfoForCompletions | undefined {
276-
const info = getImmediatelyContainingArgumentInfo(node, position, sourceFile);
280+
export function getArgumentInfoForCompletions(node: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): ArgumentInfoForCompletions | undefined {
281+
const info = getImmediatelyContainingArgumentInfo(node, position, sourceFile, checker);
277282
return !info || info.isTypeParameterList || info.invocation.kind !== InvocationKind.Call ? undefined
278283
: { invocation: info.invocation.node, argumentCount: info.argumentCount, argumentIndex: info.argumentIndex };
279284
}
280285

281-
function getArgumentOrParameterListInfo(node: Node, position: number, sourceFile: SourceFile): { readonly list: Node; readonly argumentIndex: number; readonly argumentCount: number; readonly argumentsSpan: TextSpan; } | undefined {
282-
const info = getArgumentOrParameterListAndIndex(node, sourceFile);
286+
function getArgumentOrParameterListInfo(node: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): { readonly list: Node; readonly argumentIndex: number; readonly argumentCount: number; readonly argumentsSpan: TextSpan; } | undefined {
287+
const info = getArgumentOrParameterListAndIndex(node, sourceFile, checker);
283288
if (!info) return undefined;
284289
const { list, argumentIndex } = info;
285290

286-
const argumentCount = getArgumentCount(list, /*ignoreTrailingComma*/ isInString(sourceFile, position, node));
291+
const argumentCount = getArgumentCount(list, /*ignoreTrailingComma*/ isInString(sourceFile, position, node), checker);
287292
if (argumentIndex !== 0) {
288293
Debug.assertLessThan(argumentIndex, argumentCount);
289294
}
290295
const argumentsSpan = getApplicableSpanForArguments(list, sourceFile);
291296
return { list, argumentIndex, argumentCount, argumentsSpan };
292297
}
293-
function getArgumentOrParameterListAndIndex(node: Node, sourceFile: SourceFile): { readonly list: Node; readonly argumentIndex: number; } | undefined {
298+
function getArgumentOrParameterListAndIndex(node: Node, sourceFile: SourceFile, checker: TypeChecker): { readonly list: Node; readonly argumentIndex: number; } | undefined {
294299
if (node.kind === SyntaxKind.LessThanToken || node.kind === SyntaxKind.OpenParenToken) {
295300
// Find the list that starts right *after* the < or ( token.
296301
// If the user has just opened a list, consider this item 0.
@@ -304,15 +309,15 @@ function getArgumentOrParameterListAndIndex(node: Node, sourceFile: SourceFile):
304309
// - On the target of the call (parent.func)
305310
// - On the 'new' keyword in a 'new' expression
306311
const list = findContainingList(node);
307-
return list && { list, argumentIndex: getArgumentIndex(list, node) };
312+
return list && { list, argumentIndex: getArgumentIndex(list, node, checker) };
308313
}
309314
}
310315

311316
/**
312317
* Returns relevant information for the argument list and the current argument if we are
313318
* in the argument of an invocation; returns undefined otherwise.
314319
*/
315-
function getImmediatelyContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo | undefined {
320+
function getImmediatelyContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): ArgumentListInfo | undefined {
316321
const { parent } = node;
317322
if (isCallOrNewExpression(parent)) {
318323
const invocation = parent;
@@ -331,7 +336,7 @@ function getImmediatelyContainingArgumentInfo(node: Node, position: number, sour
331336
// Case 3:
332337
// foo<T#, U#>(a#, #b#) -> The token is buried inside a list, and should give signature help
333338
// Find out if 'node' is an argument, a type argument, or neither
334-
const info = getArgumentOrParameterListInfo(node, position, sourceFile);
339+
const info = getArgumentOrParameterListInfo(node, position, sourceFile, checker);
335340
if (!info) return undefined;
336341
const { list, argumentIndex, argumentCount, argumentsSpan } = info;
337342
const isTypeParameterList = !!parent.typeArguments && parent.typeArguments.pos === list.pos;
@@ -397,7 +402,7 @@ function getImmediatelyContainingArgumentInfo(node: Node, position: number, sour
397402
}
398403

399404
function getImmediatelyContainingArgumentOrContextualParameterInfo(node: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): ArgumentListInfo | undefined {
400-
return tryGetParameterInfo(node, position, sourceFile, checker) || getImmediatelyContainingArgumentInfo(node, position, sourceFile);
405+
return tryGetParameterInfo(node, position, sourceFile, checker) || getImmediatelyContainingArgumentInfo(node, position, sourceFile, checker);
401406
}
402407

403408
function getHighestBinary(b: BinaryExpression): BinaryExpression {
@@ -452,7 +457,7 @@ function getContextualSignatureLocationInfo(node: Node, sourceFile: SourceFile,
452457
case SyntaxKind.MethodDeclaration:
453458
case SyntaxKind.FunctionExpression:
454459
case SyntaxKind.ArrowFunction:
455-
const info = getArgumentOrParameterListInfo(node, position, sourceFile);
460+
const info = getArgumentOrParameterListInfo(node, position, sourceFile, checker);
456461
if (!info) return undefined;
457462
const { argumentIndex, argumentCount, argumentsSpan } = info;
458463
const contextualType = isMethodDeclaration(parent) ? checker.getContextualTypeForObjectLiteralElement(parent) : checker.getContextualType(parent as ParenthesizedExpression | FunctionExpression | ArrowFunction);
@@ -476,7 +481,7 @@ function chooseBetterSymbol(s: Symbol): Symbol {
476481
: s;
477482
}
478483

479-
function getArgumentIndex(argumentsList: Node, node: Node) {
484+
function getArgumentIndex(argumentsList: Node, node: Node, checker: TypeChecker) {
480485
// The list we got back can include commas. In the presence of errors it may
481486
// also just have nodes without commas. For example "Foo(a b c)" will have 3
482487
// 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) {
488493
// on. In that case, even if we're after the trailing comma, we'll still see
489494
// that trailing comma in the list, and we'll have generated the appropriate
490495
// arg index.
496+
const args = argumentsList.getChildren();
491497
let argumentIndex = 0;
492-
for (const child of argumentsList.getChildren()) {
498+
for (let pos = 0; pos < length(args); pos++) {
499+
const child = args[pos];
493500
if (child === node) {
494501
break;
495502
}
496-
if (child.kind !== SyntaxKind.CommaToken) {
497-
argumentIndex++;
503+
if (isSpreadElement(child)) {
504+
argumentIndex = argumentIndex + getSpreadElementCount(child, checker) + (pos > 0 ? pos : 0);
505+
}
506+
else {
507+
if (child.kind !== SyntaxKind.CommaToken) {
508+
argumentIndex++;
509+
}
498510
}
499511
}
500-
501512
return argumentIndex;
502513
}
503514

504-
function getArgumentCount(argumentsList: Node, ignoreTrailingComma: boolean) {
515+
function getSpreadElementCount(node: SpreadElement, checker: TypeChecker) {
516+
const spreadType = checker.getTypeAtLocation(node.expression);
517+
if (checker.isTupleType(spreadType)) {
518+
const { elementFlags, fixedLength } = (spreadType as TupleTypeReference).target;
519+
if (fixedLength === 0) {
520+
return 0;
521+
}
522+
const firstOptionalIndex = findIndex(elementFlags, f => !(f & ElementFlags.Required));
523+
return firstOptionalIndex < 0 ? fixedLength : firstOptionalIndex;
524+
}
525+
return 0;
526+
}
527+
528+
function getArgumentCount(argumentsList: Node, ignoreTrailingComma: boolean, checker: TypeChecker) {
505529
// The argument count for a list is normally the number of non-comma children it has.
506530
// For example, if you have "Foo(a,b)" then there will be three children of the arg
507531
// list 'a' '<comma>' 'b'. So, in this case the arg count will be 2. However, there
@@ -515,7 +539,14 @@ function getArgumentCount(argumentsList: Node, ignoreTrailingComma: boolean) {
515539
// arg count of 3.
516540
const listChildren = argumentsList.getChildren();
517541

518-
let argumentCount = countWhere(listChildren, arg => arg.kind !== SyntaxKind.CommaToken);
542+
let argumentCount = 0;
543+
for (const child of listChildren) {
544+
if (isSpreadElement(child)) {
545+
argumentCount = argumentCount + getSpreadElementCount(child, checker);
546+
}
547+
}
548+
549+
argumentCount = argumentCount + countWhere(listChildren, arg => arg.kind !== SyntaxKind.CommaToken);
519550
if (!ignoreTrailingComma && listChildren.length > 0 && last(listChildren).kind === SyntaxKind.CommaToken) {
520551
argumentCount++;
521552
}

src/services/stringCompletions.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringL
407407
case SyntaxKind.NewExpression:
408408
case SyntaxKind.JsxAttribute:
409409
if (!isRequireCallArgument(node) && !isImportCall(parent)) {
410-
const argumentInfo = SignatureHelp.getArgumentInfoForCompletions(parent.kind === SyntaxKind.JsxAttribute ? parent.parent : node, position, sourceFile);
410+
const argumentInfo = SignatureHelp.getArgumentInfoForCompletions(parent.kind === SyntaxKind.JsxAttribute ? parent.parent : node, position, sourceFile, typeChecker);
411411
// Get string literal completions from specialized signatures of the target
412412
// i.e. declare function f(a: 'A');
413413
// f("/*completion position*/")

0 commit comments

Comments
 (0)