• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import {
2    ArrowFunction, BinaryExpression, CallLikeExpression, CancellationToken, CheckFlags, contains, countWhere,
3    createPrinterWithRemoveComments, createTextSpan, createTextSpanFromBounds, createTextSpanFromNode, Debug, EmitHint, emptyArray,
4    Expression, factory, findContainingList, findIndex, findPrecedingToken, findTokenOnLeftOfPosition, first,
5    firstDefined, flatMapToMutable, FunctionExpression, getInvokedExpression, getPossibleGenericSignatures,
6    getPossibleTypeArgumentsInfo, Identifier, identity, InternalSymbolName, isBinaryExpression, isBlock,
7    isCallOrNewExpression, isFunctionTypeNode, isIdentifier, isInComment, isInsideTemplateLiteral, isInString,
8    isJsxOpeningLikeElement, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isPropertyAccessExpression,
9    isSourceFile, isSourceFileJS, isTaggedTemplateExpression, isTemplateHead, isTemplateLiteralToken, isTemplateSpan,
10    isTemplateTail, last, lastOrUndefined, ListFormat, map, mapToDisplayParts, Node, NodeBuilderFlags,
11    ParameterDeclaration, ParenthesizedExpression, Printer, Program, punctuationPart, rangeContainsRange, Signature,
12    SignatureHelpItem, SignatureHelpItems, SignatureHelpParameter, SignatureHelpTriggerReason, skipTrivia, SourceFile,
13    spacePart, Symbol, SymbolDisplayPart, symbolToDisplayParts, SyntaxKind, TaggedTemplateExpression,
14    TemplateExpression, TextSpan, TransientSymbol, Type, TypeChecker, TypeParameter,
15} from "./_namespaces/ts";
16
17const enum InvocationKind { Call, TypeArgs, Contextual }
18interface CallInvocation { readonly kind: InvocationKind.Call; readonly node: CallLikeExpression; }
19interface TypeArgsInvocation { readonly kind: InvocationKind.TypeArgs; readonly called: Identifier; }
20interface ContextualInvocation {
21    readonly kind: InvocationKind.Contextual;
22    readonly signature: Signature;
23    readonly node: Node; // Just for enclosingDeclaration for printing types
24    readonly symbol: Symbol;
25}
26type Invocation = CallInvocation | TypeArgsInvocation | ContextualInvocation;
27
28interface ArgumentListInfo {
29    readonly isTypeParameterList: boolean;
30    readonly invocation: Invocation;
31    readonly argumentsSpan: TextSpan;
32    readonly argumentIndex: number;
33    /** argumentCount is the *apparent* number of arguments. */
34    readonly argumentCount: number;
35}
36
37/** @internal */
38export function getSignatureHelpItems(program: Program, sourceFile: SourceFile, position: number, triggerReason: SignatureHelpTriggerReason | undefined, cancellationToken: CancellationToken): SignatureHelpItems | undefined {
39    const typeChecker = program.getTypeChecker();
40
41    // Decide whether to show signature help
42    const startingToken = findTokenOnLeftOfPosition(sourceFile, position);
43    if (!startingToken) {
44        // We are at the beginning of the file
45        return undefined;
46    }
47
48    // Only need to be careful if the user typed a character and signature help wasn't showing.
49    const onlyUseSyntacticOwners = !!triggerReason && triggerReason.kind === "characterTyped";
50
51    // Bail out quickly in the middle of a string or comment, don't provide signature help unless the user explicitly requested it.
52    if (onlyUseSyntacticOwners && (isInString(sourceFile, position, startingToken) || isInComment(sourceFile, position))) {
53        return undefined;
54    }
55
56    const isManuallyInvoked = !!triggerReason && triggerReason.kind === "invoked";
57    const argumentInfo = getContainingArgumentInfo(startingToken, position, sourceFile, typeChecker, isManuallyInvoked);
58    if (!argumentInfo) return undefined;
59
60    cancellationToken.throwIfCancellationRequested();
61
62    // Extra syntactic and semantic filtering of signature help
63    const candidateInfo = getCandidateOrTypeInfo(argumentInfo, typeChecker, sourceFile, startingToken, onlyUseSyntacticOwners);
64    cancellationToken.throwIfCancellationRequested();
65
66    if (!candidateInfo) {
67        // We didn't have any sig help items produced by the TS compiler.  If this is a JS
68        // file, then see if we can figure out anything better.
69        return isSourceFileJS(sourceFile) ? createJSSignatureHelpItems(argumentInfo, program, cancellationToken) : undefined;
70    }
71
72    return typeChecker.runWithCancellationToken(cancellationToken, typeChecker =>
73        candidateInfo.kind === CandidateOrTypeKind.Candidate
74            ? createSignatureHelpItems(candidateInfo.candidates, candidateInfo.resolvedSignature, argumentInfo, sourceFile, typeChecker)
75            : createTypeHelpItems(candidateInfo.symbol, argumentInfo, sourceFile, typeChecker));
76}
77
78const enum CandidateOrTypeKind { Candidate, Type }
79interface CandidateInfo {
80    readonly kind: CandidateOrTypeKind.Candidate;
81    readonly candidates: readonly Signature[];
82    readonly resolvedSignature: Signature;
83}
84interface TypeInfo {
85    readonly kind: CandidateOrTypeKind.Type;
86    readonly symbol: Symbol;
87}
88
89function getCandidateOrTypeInfo({ invocation, argumentCount }: ArgumentListInfo, checker: TypeChecker, sourceFile: SourceFile, startingToken: Node, onlyUseSyntacticOwners: boolean): CandidateInfo | TypeInfo | undefined {
90    switch (invocation.kind) {
91        case InvocationKind.Call: {
92            if (onlyUseSyntacticOwners && !isSyntacticOwner(startingToken, invocation.node, sourceFile)) {
93                return undefined;
94            }
95            const candidates: Signature[] = [];
96            const resolvedSignature = checker.getResolvedSignatureForSignatureHelp(invocation.node, candidates, argumentCount)!; // TODO: GH#18217
97            return candidates.length === 0 ? undefined : { kind: CandidateOrTypeKind.Candidate, candidates, resolvedSignature };
98        }
99        case InvocationKind.TypeArgs: {
100            const { called } = invocation;
101            if (onlyUseSyntacticOwners && !containsPrecedingToken(startingToken, sourceFile, isIdentifier(called) ? called.parent : called)) {
102                return undefined;
103            }
104            const candidates = getPossibleGenericSignatures(called, argumentCount, checker);
105            if (candidates.length !== 0) return { kind: CandidateOrTypeKind.Candidate, candidates, resolvedSignature: first(candidates) };
106
107            const symbol = checker.getSymbolAtLocation(called);
108            return symbol && { kind: CandidateOrTypeKind.Type, symbol };
109        }
110        case InvocationKind.Contextual:
111            return { kind: CandidateOrTypeKind.Candidate, candidates: [invocation.signature], resolvedSignature: invocation.signature };
112        default:
113            return Debug.assertNever(invocation);
114    }
115}
116
117function isSyntacticOwner(startingToken: Node, node: CallLikeExpression, sourceFile: SourceFile): boolean {
118    if (!isCallOrNewExpression(node)) return false;
119    const invocationChildren = node.getChildren(sourceFile);
120    switch (startingToken.kind) {
121        case SyntaxKind.OpenParenToken:
122            return contains(invocationChildren, startingToken);
123        case SyntaxKind.CommaToken: {
124            const containingList = findContainingList(startingToken);
125            return !!containingList && contains(invocationChildren, containingList);
126        }
127        case SyntaxKind.LessThanToken:
128            return containsPrecedingToken(startingToken, sourceFile, node.expression);
129        default:
130            return false;
131    }
132}
133
134function createJSSignatureHelpItems(argumentInfo: ArgumentListInfo, program: Program, cancellationToken: CancellationToken): SignatureHelpItems | undefined {
135    if (argumentInfo.invocation.kind === InvocationKind.Contextual) return undefined;
136    // See if we can find some symbol with the call expression name that has call signatures.
137    const expression = getExpressionFromInvocation(argumentInfo.invocation);
138    const name = isPropertyAccessExpression(expression) ? expression.name.text : undefined;
139    const typeChecker = program.getTypeChecker();
140    return name === undefined ? undefined : firstDefined(program.getSourceFiles(), sourceFile =>
141        firstDefined(sourceFile.getNamedDeclarations().get(name), declaration => {
142            const type = declaration.symbol && typeChecker.getTypeOfSymbolAtLocation(declaration.symbol, declaration);
143            const callSignatures = type && type.getCallSignatures();
144            if (callSignatures && callSignatures.length) {
145                return typeChecker.runWithCancellationToken(
146                    cancellationToken,
147                    typeChecker => createSignatureHelpItems(
148                        callSignatures,
149                        callSignatures[0],
150                        argumentInfo,
151                        sourceFile,
152                        typeChecker,
153                        /*useFullPrefix*/ true));
154            }
155        }));
156}
157
158function containsPrecedingToken(startingToken: Node, sourceFile: SourceFile, container: Node) {
159    const pos = startingToken.getFullStart();
160    // There’s a possibility that `startingToken.parent` contains only `startingToken` and
161    // missing nodes, none of which are valid to be returned by `findPrecedingToken`. In that
162    // case, the preceding token we want is actually higher up the tree—almost definitely the
163    // next parent, but theoretically the situation with missing nodes might be happening on
164    // multiple nested levels.
165    let currentParent: Node | undefined = startingToken.parent;
166    while (currentParent) {
167        const precedingToken = findPrecedingToken(pos, sourceFile, currentParent, /*excludeJsdoc*/ true);
168        if (precedingToken) {
169            return rangeContainsRange(container, precedingToken);
170        }
171        currentParent = currentParent.parent;
172    }
173    return Debug.fail("Could not find preceding token");
174}
175
176/** @internal */
177export interface ArgumentInfoForCompletions {
178    readonly invocation: CallLikeExpression;
179    readonly argumentIndex: number;
180    readonly argumentCount: number;
181}
182/** @internal */
183export function getArgumentInfoForCompletions(node: Node, position: number, sourceFile: SourceFile): ArgumentInfoForCompletions | undefined {
184    const info = getImmediatelyContainingArgumentInfo(node, position, sourceFile);
185    return !info || info.isTypeParameterList || info.invocation.kind !== InvocationKind.Call ? undefined
186        : { invocation: info.invocation.node, argumentCount: info.argumentCount, argumentIndex: info.argumentIndex };
187}
188
189function getArgumentOrParameterListInfo(node: Node, position: number, sourceFile: SourceFile): { readonly list: Node, readonly argumentIndex: number, readonly argumentCount: number, readonly argumentsSpan: TextSpan } | undefined {
190    const info = getArgumentOrParameterListAndIndex(node, sourceFile);
191    if (!info) return undefined;
192    const { list, argumentIndex } = info;
193
194    const argumentCount = getArgumentCount(list, /*ignoreTrailingComma*/ isInString(sourceFile, position, node));
195    if (argumentIndex !== 0) {
196        Debug.assertLessThan(argumentIndex, argumentCount);
197    }
198    const argumentsSpan = getApplicableSpanForArguments(list, sourceFile);
199    return { list, argumentIndex, argumentCount, argumentsSpan };
200}
201function getArgumentOrParameterListAndIndex(node: Node, sourceFile: SourceFile): { readonly list: Node, readonly argumentIndex: number } | undefined {
202    if (node.kind === SyntaxKind.LessThanToken || node.kind === SyntaxKind.OpenParenToken) {
203        // Find the list that starts right *after* the < or ( token.
204        // If the user has just opened a list, consider this item 0.
205        return { list: getChildListThatStartsWithOpenerToken(node.parent, node, sourceFile), argumentIndex: 0 };
206    }
207    else {
208        // findListItemInfo can return undefined if we are not in parent's argument list
209        // or type argument list. This includes cases where the cursor is:
210        //   - To the right of the closing parenthesis, non-substitution template, or template tail.
211        //   - Between the type arguments and the arguments (greater than token)
212        //   - On the target of the call (parent.func)
213        //   - On the 'new' keyword in a 'new' expression
214        const list = findContainingList(node);
215        return list && { list, argumentIndex: getArgumentIndex(list, node) };
216    }
217}
218
219/**
220 * Returns relevant information for the argument list and the current argument if we are
221 * in the argument of an invocation; returns undefined otherwise.
222 */
223function getImmediatelyContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo | undefined {
224    const { parent } = node;
225    if (isCallOrNewExpression(parent)) {
226        const invocation = parent;
227
228        // There are 3 cases to handle:
229        //   1. The token introduces a list, and should begin a signature help session
230        //   2. The token is either not associated with a list, or ends a list, so the session should end
231        //   3. The token is buried inside a list, and should give signature help
232        //
233        // The following are examples of each:
234        //
235        //    Case 1:
236        //          foo<#T, U>(#a, b)    -> The token introduces a list, and should begin a signature help session
237        //    Case 2:
238        //          fo#o<T, U>#(a, b)#   -> The token is either not associated with a list, or ends a list, so the session should end
239        //    Case 3:
240        //          foo<T#, U#>(a#, #b#) -> The token is buried inside a list, and should give signature help
241        // Find out if 'node' is an argument, a type argument, or neither
242        const info = getArgumentOrParameterListInfo(node, position, sourceFile);
243        if (!info) return undefined;
244        const { list, argumentIndex, argumentCount, argumentsSpan } = info;
245        const isTypeParameterList = !!parent.typeArguments && parent.typeArguments.pos === list.pos;
246        return { isTypeParameterList, invocation: { kind: InvocationKind.Call, node: invocation }, argumentsSpan, argumentIndex, argumentCount };
247    }
248    else if (isNoSubstitutionTemplateLiteral(node) && isTaggedTemplateExpression(parent)) {
249        // Check if we're actually inside the template;
250        // otherwise we'll fall out and return undefined.
251        if (isInsideTemplateLiteral(node, position, sourceFile)) {
252            return getArgumentListInfoForTemplate(parent, /*argumentIndex*/ 0, sourceFile);
253        }
254        return undefined;
255    }
256    else if (isTemplateHead(node) && parent.parent.kind === SyntaxKind.TaggedTemplateExpression) {
257        const templateExpression = parent as TemplateExpression;
258        const tagExpression = templateExpression.parent as TaggedTemplateExpression;
259        Debug.assert(templateExpression.kind === SyntaxKind.TemplateExpression);
260
261        const argumentIndex = isInsideTemplateLiteral(node, position, sourceFile) ? 0 : 1;
262
263        return getArgumentListInfoForTemplate(tagExpression, argumentIndex, sourceFile);
264    }
265    else if (isTemplateSpan(parent) && isTaggedTemplateExpression(parent.parent.parent)) {
266        const templateSpan = parent;
267        const tagExpression = parent.parent.parent;
268
269        // If we're just after a template tail, don't show signature help.
270        if (isTemplateTail(node) && !isInsideTemplateLiteral(node, position, sourceFile)) {
271            return undefined;
272        }
273
274        const spanIndex = templateSpan.parent.templateSpans.indexOf(templateSpan);
275        const argumentIndex = getArgumentIndexForTemplatePiece(spanIndex, node, position, sourceFile);
276
277        return getArgumentListInfoForTemplate(tagExpression, argumentIndex, sourceFile);
278    }
279    else if (isJsxOpeningLikeElement(parent)) {
280        // Provide a signature help for JSX opening element or JSX self-closing element.
281        // This is not guarantee that JSX tag-name is resolved into stateless function component. (that is done in "getSignatureHelpItems")
282        // i.e
283        //      export function MainButton(props: ButtonProps, context: any): JSX.Element { ... }
284        //      <MainButton /*signatureHelp*/
285        const attributeSpanStart = parent.attributes.pos;
286        const attributeSpanEnd = skipTrivia(sourceFile.text, parent.attributes.end, /*stopAfterLineBreak*/ false);
287        return {
288            isTypeParameterList: false,
289            invocation: { kind: InvocationKind.Call, node: parent },
290            argumentsSpan: createTextSpan(attributeSpanStart, attributeSpanEnd - attributeSpanStart),
291            argumentIndex: 0,
292            argumentCount: 1
293        };
294    }
295    else {
296        const typeArgInfo = getPossibleTypeArgumentsInfo(node, sourceFile);
297        if (typeArgInfo) {
298            const { called, nTypeArguments } = typeArgInfo;
299            const invocation: Invocation = { kind: InvocationKind.TypeArgs, called };
300            const argumentsSpan = createTextSpanFromBounds(called.getStart(sourceFile), node.end);
301            return { isTypeParameterList: true, invocation, argumentsSpan, argumentIndex: nTypeArguments, argumentCount: nTypeArguments + 1 };
302        }
303        return undefined;
304    }
305}
306
307function getImmediatelyContainingArgumentOrContextualParameterInfo(node: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): ArgumentListInfo | undefined {
308    return tryGetParameterInfo(node, position, sourceFile, checker) || getImmediatelyContainingArgumentInfo(node, position, sourceFile);
309}
310
311function getHighestBinary(b: BinaryExpression): BinaryExpression {
312    return isBinaryExpression(b.parent) ? getHighestBinary(b.parent) : b;
313}
314
315function countBinaryExpressionParameters(b: BinaryExpression): number {
316    return isBinaryExpression(b.left) ? countBinaryExpressionParameters(b.left) + 1 : 2;
317}
318
319function tryGetParameterInfo(startingToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): ArgumentListInfo | undefined {
320    const info = getContextualSignatureLocationInfo(startingToken, sourceFile, position, checker);
321    if (!info) return undefined;
322    const { contextualType, argumentIndex, argumentCount, argumentsSpan } = info;
323
324    // for optional function condition.
325    const nonNullableContextualType = contextualType.getNonNullableType();
326
327    const symbol = nonNullableContextualType.symbol;
328    if (symbol === undefined) return undefined;
329
330    const signature = lastOrUndefined(nonNullableContextualType.getCallSignatures());
331    if (signature === undefined) return undefined;
332
333    const invocation: ContextualInvocation = { kind: InvocationKind.Contextual, signature, node: startingToken, symbol: chooseBetterSymbol(symbol) };
334    return { isTypeParameterList: false, invocation, argumentsSpan, argumentIndex, argumentCount };
335}
336
337interface ContextualSignatureLocationInfo { readonly contextualType: Type; readonly argumentIndex: number; readonly argumentCount: number; readonly argumentsSpan: TextSpan; }
338function getContextualSignatureLocationInfo(startingToken: Node, sourceFile: SourceFile, position: number, checker: TypeChecker): ContextualSignatureLocationInfo | undefined {
339    if (startingToken.kind !== SyntaxKind.OpenParenToken && startingToken.kind !== SyntaxKind.CommaToken) return undefined;
340    const { parent } = startingToken;
341    switch (parent.kind) {
342        case SyntaxKind.ParenthesizedExpression:
343        case SyntaxKind.MethodDeclaration:
344        case SyntaxKind.FunctionExpression:
345        case SyntaxKind.ArrowFunction:
346            const info = getArgumentOrParameterListInfo(startingToken, position, sourceFile);
347            if (!info) return undefined;
348            const { argumentIndex, argumentCount, argumentsSpan } = info;
349            const contextualType = isMethodDeclaration(parent) ? checker.getContextualTypeForObjectLiteralElement(parent) : checker.getContextualType(parent as ParenthesizedExpression | FunctionExpression | ArrowFunction);
350            return contextualType && { contextualType, argumentIndex, argumentCount, argumentsSpan };
351        case SyntaxKind.BinaryExpression: {
352            const highestBinary = getHighestBinary(parent as BinaryExpression);
353            const contextualType = checker.getContextualType(highestBinary);
354            const argumentIndex = startingToken.kind === SyntaxKind.OpenParenToken ? 0 : countBinaryExpressionParameters(parent as BinaryExpression) - 1;
355            const argumentCount = countBinaryExpressionParameters(highestBinary);
356            return contextualType && { contextualType, argumentIndex, argumentCount, argumentsSpan: createTextSpanFromNode(parent) };
357        }
358        default:
359            return undefined;
360    }
361}
362
363// The type of a function type node has a symbol at that node, but it's better to use the symbol for a parameter or type alias.
364function chooseBetterSymbol(s: Symbol): Symbol {
365    return s.name === InternalSymbolName.Type
366        ? firstDefined(s.declarations, d => isFunctionTypeNode(d) ? d.parent.symbol : undefined) || s
367        : s;
368}
369
370function getArgumentIndex(argumentsList: Node, node: Node) {
371    // The list we got back can include commas.  In the presence of errors it may
372    // also just have nodes without commas.  For example "Foo(a b c)" will have 3
373    // args without commas. We want to find what index we're at.  So we count
374    // forward until we hit ourselves, only incrementing the index if it isn't a
375    // comma.
376    //
377    // Note: the subtlety around trailing commas (in getArgumentCount) does not apply
378    // here.  That's because we're only walking forward until we hit the node we're
379    // on.  In that case, even if we're after the trailing comma, we'll still see
380    // that trailing comma in the list, and we'll have generated the appropriate
381    // arg index.
382    let argumentIndex = 0;
383    for (const child of argumentsList.getChildren()) {
384        if (child === node) {
385            break;
386        }
387        if (child.kind !== SyntaxKind.CommaToken) {
388            argumentIndex++;
389        }
390    }
391
392    return argumentIndex;
393}
394
395function getArgumentCount(argumentsList: Node, ignoreTrailingComma: boolean) {
396    // The argument count for a list is normally the number of non-comma children it has.
397    // For example, if you have "Foo(a,b)" then there will be three children of the arg
398    // list 'a' '<comma>' 'b'.  So, in this case the arg count will be 2.  However, there
399    // is a small subtlety.  If you have "Foo(a,)", then the child list will just have
400    // 'a' '<comma>'.  So, in the case where the last child is a comma, we increase the
401    // arg count by one to compensate.
402    //
403    // Note: this subtlety only applies to the last comma.  If you had "Foo(a,," then
404    // we'll have: 'a' '<comma>' '<missing>'
405    // That will give us 2 non-commas.  We then add one for the last comma, giving us an
406    // arg count of 3.
407    const listChildren = argumentsList.getChildren();
408
409    let argumentCount = countWhere(listChildren, arg => arg.kind !== SyntaxKind.CommaToken);
410    if (!ignoreTrailingComma && listChildren.length > 0 && last(listChildren).kind === SyntaxKind.CommaToken) {
411        argumentCount++;
412    }
413    return argumentCount;
414}
415
416// spanIndex is either the index for a given template span.
417// This does not give appropriate results for a NoSubstitutionTemplateLiteral
418function getArgumentIndexForTemplatePiece(spanIndex: number, node: Node, position: number, sourceFile: SourceFile): number {
419    // Because the TemplateStringsArray is the first argument, we have to offset each substitution expression by 1.
420    // There are three cases we can encounter:
421    //      1. We are precisely in the template literal (argIndex = 0).
422    //      2. We are in or to the right of the substitution expression (argIndex = spanIndex + 1).
423    //      3. We are directly to the right of the template literal, but because we look for the token on the left,
424    //          not enough to put us in the substitution expression; we should consider ourselves part of
425    //          the *next* span's expression by offsetting the index (argIndex = (spanIndex + 1) + 1).
426    //
427    /* eslint-disable local/no-double-space */
428    // Example: f  `# abcd $#{#  1 + 1#  }# efghi ${ #"#hello"#  }  #  `
429    //              ^       ^ ^       ^   ^          ^ ^      ^     ^
430    // Case:        1       1 3       2   1          3 2      2     1
431    /* eslint-enable local/no-double-space */
432    Debug.assert(position >= node.getStart(), "Assumed 'position' could not occur before node.");
433    if (isTemplateLiteralToken(node)) {
434        if (isInsideTemplateLiteral(node, position, sourceFile)) {
435            return 0;
436        }
437        return spanIndex + 2;
438    }
439    return spanIndex + 1;
440}
441
442function getArgumentListInfoForTemplate(tagExpression: TaggedTemplateExpression, argumentIndex: number, sourceFile: SourceFile): ArgumentListInfo {
443    // argumentCount is either 1 or (numSpans + 1) to account for the template strings array argument.
444    const argumentCount = isNoSubstitutionTemplateLiteral(tagExpression.template) ? 1 : tagExpression.template.templateSpans.length + 1;
445    if (argumentIndex !== 0) {
446        Debug.assertLessThan(argumentIndex, argumentCount);
447    }
448    return {
449        isTypeParameterList: false,
450        invocation: { kind: InvocationKind.Call, node: tagExpression },
451        argumentsSpan: getApplicableSpanForTaggedTemplate(tagExpression, sourceFile),
452        argumentIndex,
453        argumentCount
454    };
455}
456
457function getApplicableSpanForArguments(argumentsList: Node, sourceFile: SourceFile): TextSpan {
458    // We use full start and skip trivia on the end because we want to include trivia on
459    // both sides. For example,
460    //
461    //    foo(   /*comment */     a, b, c      /*comment*/     )
462    //        |                                               |
463    //
464    // The applicable span is from the first bar to the second bar (inclusive,
465    // but not including parentheses)
466    const applicableSpanStart = argumentsList.getFullStart();
467    const applicableSpanEnd = skipTrivia(sourceFile.text, argumentsList.getEnd(), /*stopAfterLineBreak*/ false);
468    return createTextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart);
469}
470
471function getApplicableSpanForTaggedTemplate(taggedTemplate: TaggedTemplateExpression, sourceFile: SourceFile): TextSpan {
472    const template = taggedTemplate.template;
473    const applicableSpanStart = template.getStart();
474    let applicableSpanEnd = template.getEnd();
475
476    // We need to adjust the end position for the case where the template does not have a tail.
477    // Otherwise, we will not show signature help past the expression.
478    // For example,
479    //
480    //      ` ${ 1 + 1 foo(10)
481    //       |       |
482    // This is because a Missing node has no width. However, what we actually want is to include trivia
483    // leading up to the next token in case the user is about to type in a TemplateMiddle or TemplateTail.
484    if (template.kind === SyntaxKind.TemplateExpression) {
485        const lastSpan = last(template.templateSpans);
486        if (lastSpan.literal.getFullWidth() === 0) {
487            applicableSpanEnd = skipTrivia(sourceFile.text, applicableSpanEnd, /*stopAfterLineBreak*/ false);
488        }
489    }
490
491    return createTextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart);
492}
493
494function getContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile, checker: TypeChecker, isManuallyInvoked: boolean): ArgumentListInfo | undefined {
495    for (let n = node; !isSourceFile(n) && (isManuallyInvoked || !isBlock(n)); n = n.parent) {
496        // If the node is not a subspan of its parent, this is a big problem.
497        // There have been crashes that might be caused by this violation.
498        Debug.assert(rangeContainsRange(n.parent, n), "Not a subspan", () => `Child: ${Debug.formatSyntaxKind(n.kind)}, parent: ${Debug.formatSyntaxKind(n.parent.kind)}`);
499        const argumentInfo = getImmediatelyContainingArgumentOrContextualParameterInfo(n, position, sourceFile, checker);
500        if (argumentInfo) {
501            return argumentInfo;
502        }
503    }
504    return undefined;
505}
506
507function getChildListThatStartsWithOpenerToken(parent: Node, openerToken: Node, sourceFile: SourceFile): Node {
508    const children = parent.getChildren(sourceFile);
509    const indexOfOpenerToken = children.indexOf(openerToken);
510    Debug.assert(indexOfOpenerToken >= 0 && children.length > indexOfOpenerToken + 1);
511    return children[indexOfOpenerToken + 1];
512}
513
514function getExpressionFromInvocation(invocation: CallInvocation | TypeArgsInvocation): Expression {
515    return invocation.kind === InvocationKind.Call ? getInvokedExpression(invocation.node) : invocation.called;
516}
517
518function getEnclosingDeclarationFromInvocation(invocation: Invocation): Node {
519    return invocation.kind === InvocationKind.Call ? invocation.node : invocation.kind === InvocationKind.TypeArgs ? invocation.called : invocation.node;
520}
521
522const signatureHelpNodeBuilderFlags = NodeBuilderFlags.OmitParameterModifiers | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope;
523function createSignatureHelpItems(
524    candidates: readonly Signature[],
525    resolvedSignature: Signature,
526    { isTypeParameterList, argumentCount, argumentsSpan: applicableSpan, invocation, argumentIndex }: ArgumentListInfo,
527    sourceFile: SourceFile,
528    typeChecker: TypeChecker,
529    useFullPrefix?: boolean,
530): SignatureHelpItems {
531    const enclosingDeclaration = getEnclosingDeclarationFromInvocation(invocation);
532    const callTargetSymbol = invocation.kind === InvocationKind.Contextual ? invocation.symbol : (typeChecker.getSymbolAtLocation(getExpressionFromInvocation(invocation)) || useFullPrefix && resolvedSignature.declaration?.symbol);
533    const callTargetDisplayParts = callTargetSymbol ? symbolToDisplayParts(typeChecker, callTargetSymbol, useFullPrefix ? sourceFile : undefined, /*meaning*/ undefined) : emptyArray;
534    const items = map(candidates, candidateSignature => getSignatureHelpItem(candidateSignature, callTargetDisplayParts, isTypeParameterList, typeChecker, enclosingDeclaration, sourceFile));
535
536    if (argumentIndex !== 0) {
537        Debug.assertLessThan(argumentIndex, argumentCount);
538    }
539    let selectedItemIndex = 0;
540    let itemsSeen = 0;
541    for (let i = 0; i < items.length; i++) {
542        const item = items[i];
543        if (candidates[i] === resolvedSignature) {
544            selectedItemIndex = itemsSeen;
545            if (item.length > 1) {
546                // check to see if any items in the list better match than the first one, as the checker isn't filtering the nested lists
547                // (those come from tuple parameter expansion)
548                let count = 0;
549                for (const i of item) {
550                    if (i.isVariadic || i.parameters.length >= argumentCount) {
551                        selectedItemIndex = itemsSeen + count;
552                        break;
553                    }
554                    count++;
555                }
556            }
557        }
558        itemsSeen += item.length;
559    }
560
561    Debug.assert(selectedItemIndex !== -1); // If candidates is non-empty it should always include bestSignature. We check for an empty candidates before calling this function.
562    const help = { items: flatMapToMutable(items, identity), applicableSpan, selectedItemIndex, argumentIndex, argumentCount };
563    const selected = help.items[selectedItemIndex];
564    if (selected.isVariadic) {
565        const firstRest = findIndex(selected.parameters, p => !!p.isRest);
566        if (-1 < firstRest && firstRest < selected.parameters.length - 1) {
567            // We don't have any code to get this correct; instead, don't highlight a current parameter AT ALL
568            help.argumentIndex = selected.parameters.length;
569        }
570        else {
571            help.argumentIndex = Math.min(help.argumentIndex, selected.parameters.length - 1);
572        }
573    }
574    return help;
575}
576
577function createTypeHelpItems(
578    symbol: Symbol,
579    { argumentCount, argumentsSpan: applicableSpan, invocation, argumentIndex }: ArgumentListInfo,
580    sourceFile: SourceFile,
581    checker: TypeChecker
582): SignatureHelpItems | undefined {
583    const typeParameters = checker.getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol);
584    if (!typeParameters) return undefined;
585    const items = [getTypeHelpItem(symbol, typeParameters, checker, getEnclosingDeclarationFromInvocation(invocation), sourceFile)];
586    return { items, applicableSpan, selectedItemIndex: 0, argumentIndex, argumentCount };
587}
588
589function getTypeHelpItem(symbol: Symbol, typeParameters: readonly TypeParameter[], checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItem {
590    const typeSymbolDisplay = symbolToDisplayParts(checker, symbol);
591
592    const printer = createPrinterWithRemoveComments();
593    const parameters = typeParameters.map(t => createSignatureHelpParameterForTypeParameter(t, checker, enclosingDeclaration, sourceFile, printer));
594
595    const documentation = symbol.getDocumentationComment(checker);
596    const tags = symbol.getJsDocTags(checker);
597    const prefixDisplayParts = [...typeSymbolDisplay, punctuationPart(SyntaxKind.LessThanToken)];
598    return { isVariadic: false, prefixDisplayParts, suffixDisplayParts: [punctuationPart(SyntaxKind.GreaterThanToken)], separatorDisplayParts, parameters, documentation, tags };
599}
600
601const separatorDisplayParts: SymbolDisplayPart[] = [punctuationPart(SyntaxKind.CommaToken), spacePart()];
602
603function getSignatureHelpItem(candidateSignature: Signature, callTargetDisplayParts: readonly SymbolDisplayPart[], isTypeParameterList: boolean, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItem[] {
604    const infos = (isTypeParameterList ? itemInfoForTypeParameters : itemInfoForParameters)(candidateSignature, checker, enclosingDeclaration, sourceFile);
605    return map(infos, ({ isVariadic, parameters, prefix, suffix }) => {
606        const prefixDisplayParts = [...callTargetDisplayParts, ...prefix];
607        const suffixDisplayParts = [...suffix, ...returnTypeToDisplayParts(candidateSignature, enclosingDeclaration, checker)];
608        const documentation = candidateSignature.getDocumentationComment(checker);
609        const tags = candidateSignature.getJsDocTags();
610        return { isVariadic, prefixDisplayParts, suffixDisplayParts, separatorDisplayParts, parameters, documentation, tags };
611    });
612}
613
614function returnTypeToDisplayParts(candidateSignature: Signature, enclosingDeclaration: Node, checker: TypeChecker): readonly SymbolDisplayPart[] {
615    return mapToDisplayParts(writer => {
616        writer.writePunctuation(":");
617        writer.writeSpace(" ");
618        const predicate = checker.getTypePredicateOfSignature(candidateSignature);
619        if (predicate) {
620            checker.writeTypePredicate(predicate, enclosingDeclaration, /*flags*/ undefined, writer);
621        }
622        else {
623            checker.writeType(checker.getReturnTypeOfSignature(candidateSignature), enclosingDeclaration, /*flags*/ undefined, writer);
624        }
625    });
626}
627
628interface SignatureHelpItemInfo { readonly isVariadic: boolean; readonly parameters: SignatureHelpParameter[]; readonly prefix: readonly SymbolDisplayPart[]; readonly suffix: readonly SymbolDisplayPart[]; }
629
630function itemInfoForTypeParameters(candidateSignature: Signature, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItemInfo[] {
631    const typeParameters = (candidateSignature.target || candidateSignature).typeParameters;
632    const printer = createPrinterWithRemoveComments();
633    const parameters = (typeParameters || emptyArray).map(t => createSignatureHelpParameterForTypeParameter(t, checker, enclosingDeclaration, sourceFile, printer));
634    const thisParameter = candidateSignature.thisParameter ? [checker.symbolToParameterDeclaration(candidateSignature.thisParameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!] : [];
635
636    return checker.getExpandedParameters(candidateSignature).map(paramList => {
637        const params = factory.createNodeArray([...thisParameter, ...map(paramList, param => checker.symbolToParameterDeclaration(param, enclosingDeclaration, signatureHelpNodeBuilderFlags)!)]);
638        const parameterParts = mapToDisplayParts(writer => {
639            printer.writeList(ListFormat.CallExpressionArguments, params, sourceFile, writer);
640        });
641        return { isVariadic: false, parameters, prefix: [punctuationPart(SyntaxKind.LessThanToken)], suffix: [punctuationPart(SyntaxKind.GreaterThanToken), ...parameterParts] };
642    });
643}
644
645function itemInfoForParameters(candidateSignature: Signature, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItemInfo[] {
646    const printer = createPrinterWithRemoveComments();
647    const typeParameterParts = mapToDisplayParts(writer => {
648        if (candidateSignature.typeParameters && candidateSignature.typeParameters.length) {
649            const args = factory.createNodeArray(candidateSignature.typeParameters.map(p => checker.typeParameterToDeclaration(p, enclosingDeclaration, signatureHelpNodeBuilderFlags)!));
650            printer.writeList(ListFormat.TypeParameters, args, sourceFile, writer);
651        }
652    });
653    const lists = checker.getExpandedParameters(candidateSignature);
654    const isVariadic: (parameterList: readonly Symbol[]) => boolean =
655        !checker.hasEffectiveRestParameter(candidateSignature) ? _ => false
656        : lists.length === 1 ? _ => true
657        : pList => !!(pList.length && (pList[pList.length - 1] as TransientSymbol).checkFlags & CheckFlags.RestParameter);
658    return lists.map(parameterList => ({
659        isVariadic: isVariadic(parameterList),
660        parameters: parameterList.map(p => createSignatureHelpParameterForParameter(p, checker, enclosingDeclaration, sourceFile, printer)),
661        prefix: [...typeParameterParts, punctuationPart(SyntaxKind.OpenParenToken)],
662        suffix: [punctuationPart(SyntaxKind.CloseParenToken)]
663    }));
664}
665
666function createSignatureHelpParameterForParameter(parameter: Symbol, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile, printer: Printer): SignatureHelpParameter {
667    const displayParts = mapToDisplayParts(writer => {
668        const param = checker.symbolToParameterDeclaration(parameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!;
669        printer.writeNode(EmitHint.Unspecified, param, sourceFile, writer);
670    });
671    const isOptional = checker.isOptionalParameter(parameter.valueDeclaration as ParameterDeclaration);
672    const isRest = !!((parameter as TransientSymbol).checkFlags & CheckFlags.RestParameter);
673    return { name: parameter.name, documentation: parameter.getDocumentationComment(checker), displayParts, isOptional, isRest };
674}
675
676function createSignatureHelpParameterForTypeParameter(typeParameter: TypeParameter, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile, printer: Printer): SignatureHelpParameter {
677    const displayParts = mapToDisplayParts(writer => {
678        const param = checker.typeParameterToDeclaration(typeParameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!;
679        printer.writeNode(EmitHint.Unspecified, param, sourceFile, writer);
680    });
681    return { name: typeParameter.symbol.name, documentation: typeParameter.symbol.getDocumentationComment(checker), displayParts, isOptional: false, isRest: false };
682}
683