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