1/* @internal */ 2namespace ts.InlayHints { 3 4 const maxHintsLength = 30; 5 6 const leadingParameterNameCommentRegexFactory = (name: string) => { 7 return new RegExp(`^\\s?/\\*\\*?\\s?${name}\\s?\\*\\/\\s?$`); 8 }; 9 10 function shouldShowParameterNameHints(preferences: UserPreferences) { 11 return preferences.includeInlayParameterNameHints === "literals" || preferences.includeInlayParameterNameHints === "all"; 12 } 13 14 function shouldShowLiteralParameterNameHintsOnly(preferences: UserPreferences) { 15 return preferences.includeInlayParameterNameHints === "literals"; 16 } 17 18 export function provideInlayHints(context: InlayHintsContext): InlayHint[] { 19 const { file, program, span, cancellationToken, preferences } = context; 20 const sourceFileText = file.text; 21 const compilerOptions = program.getCompilerOptions(); 22 23 const checker = program.getTypeChecker(); 24 const result: InlayHint[] = []; 25 26 visitor(file); 27 return result; 28 29 function visitor(node: Node): true | undefined { 30 if (!node || node.getFullWidth() === 0) { 31 return; 32 } 33 34 switch (node.kind) { 35 case SyntaxKind.ModuleDeclaration: 36 case SyntaxKind.ClassDeclaration: 37 case SyntaxKind.InterfaceDeclaration: 38 case SyntaxKind.FunctionDeclaration: 39 case SyntaxKind.ClassExpression: 40 case SyntaxKind.FunctionExpression: 41 case SyntaxKind.MethodDeclaration: 42 case SyntaxKind.ArrowFunction: 43 cancellationToken.throwIfCancellationRequested(); 44 } 45 46 if (!textSpanIntersectsWith(span, node.pos, node.getFullWidth())) { 47 return; 48 } 49 50 if (isTypeNode(node) && !isExpressionWithTypeArguments(node)) { 51 return; 52 } 53 54 if (preferences.includeInlayVariableTypeHints && isVariableDeclaration(node)) { 55 visitVariableLikeDeclaration(node); 56 } 57 else if (preferences.includeInlayPropertyDeclarationTypeHints && isPropertyDeclaration(node)) { 58 visitVariableLikeDeclaration(node); 59 } 60 else if (preferences.includeInlayEnumMemberValueHints && isEnumMember(node)) { 61 visitEnumMember(node); 62 } 63 else if (shouldShowParameterNameHints(preferences) && (isCallExpression(node) || isNewExpression(node))) { 64 visitCallOrNewExpression(node); 65 } 66 else { 67 if (preferences.includeInlayFunctionParameterTypeHints && isFunctionLikeDeclaration(node) && hasContextSensitiveParameters(node)) { 68 visitFunctionLikeForParameterType(node); 69 } 70 if (preferences.includeInlayFunctionLikeReturnTypeHints && isSignatureSupportingReturnAnnotation(node)) { 71 visitFunctionDeclarationLikeForReturnType(node); 72 } 73 } 74 return forEachChild(node, visitor); 75 } 76 77 function isSignatureSupportingReturnAnnotation(node: Node): node is FunctionDeclaration | ArrowFunction | FunctionExpression | MethodDeclaration | GetAccessorDeclaration { 78 return isArrowFunction(node) || isFunctionExpression(node) || isFunctionDeclaration(node) || isMethodDeclaration(node) || isGetAccessorDeclaration(node); 79 } 80 81 function addParameterHints(text: string, position: number, isFirstVariadicArgument: boolean) { 82 result.push({ 83 text: `${isFirstVariadicArgument ? "..." : ""}${truncation(text, maxHintsLength)}:`, 84 position, 85 kind: InlayHintKind.Parameter, 86 whitespaceAfter: true, 87 }); 88 } 89 90 function addTypeHints(text: string, position: number) { 91 result.push({ 92 text: `: ${truncation(text, maxHintsLength)}`, 93 position, 94 kind: InlayHintKind.Type, 95 whitespaceBefore: true, 96 }); 97 } 98 99 function addEnumMemberValueHints(text: string, position: number) { 100 result.push({ 101 text: `= ${truncation(text, maxHintsLength)}`, 102 position, 103 kind: InlayHintKind.Enum, 104 whitespaceBefore: true, 105 }); 106 } 107 108 function visitEnumMember(member: EnumMember) { 109 if (member.initializer) { 110 return; 111 } 112 113 const enumValue = checker.getConstantValue(member); 114 if (enumValue !== undefined) { 115 addEnumMemberValueHints(enumValue.toString(), member.end); 116 } 117 } 118 119 function isModuleReferenceType(type: Type) { 120 return type.symbol && (type.symbol.flags & SymbolFlags.Module); 121 } 122 123 function visitVariableLikeDeclaration(decl: VariableDeclaration | PropertyDeclaration) { 124 if (!decl.initializer || isBindingPattern(decl.name) || isVariableDeclaration(decl) && !isHintableDeclaration(decl)) { 125 return; 126 } 127 128 const effectiveTypeAnnotation = getEffectiveTypeAnnotationNode(decl); 129 if (effectiveTypeAnnotation) { 130 return; 131 } 132 133 const declarationType = checker.getTypeAtLocation(decl); 134 if (isModuleReferenceType(declarationType)) { 135 return; 136 } 137 138 const typeDisplayString = printTypeInSingleLine(declarationType); 139 if (typeDisplayString) { 140 const isVariableNameMatchesType = preferences.includeInlayVariableTypeHintsWhenTypeMatchesName === false && equateStringsCaseInsensitive(decl.name.getText(), typeDisplayString); 141 if (isVariableNameMatchesType) { 142 return; 143 } 144 addTypeHints(typeDisplayString, decl.name.end); 145 } 146 } 147 148 function visitCallOrNewExpression(expr: CallExpression | NewExpression) { 149 const args = expr.arguments; 150 if (!args || !args.length) { 151 return; 152 } 153 154 const candidates: Signature[] = []; 155 const signature = checker.getResolvedSignatureForSignatureHelp(expr, candidates); 156 if (!signature || !candidates.length) { 157 return; 158 } 159 160 for (let i = 0; i < args.length; ++i) { 161 const originalArg = args[i]; 162 const arg = skipParentheses(originalArg); 163 if (shouldShowLiteralParameterNameHintsOnly(preferences) && !isHintableLiteral(arg)) { 164 continue; 165 } 166 167 const identifierNameInfo = checker.getParameterIdentifierNameAtPosition(signature, i); 168 if (identifierNameInfo) { 169 const [parameterName, isFirstVariadicArgument] = identifierNameInfo; 170 const isParameterNameNotSameAsArgument = preferences.includeInlayParameterNameHintsWhenArgumentMatchesName || !identifierOrAccessExpressionPostfixMatchesParameterName(arg, parameterName); 171 if (!isParameterNameNotSameAsArgument && !isFirstVariadicArgument) { 172 continue; 173 } 174 175 const name = unescapeLeadingUnderscores(parameterName); 176 if (leadingCommentsContainsParameterName(arg, name)) { 177 continue; 178 } 179 180 addParameterHints(name, originalArg.getStart(), isFirstVariadicArgument); 181 } 182 } 183 } 184 185 function identifierOrAccessExpressionPostfixMatchesParameterName(expr: Expression, parameterName: __String) { 186 if (isIdentifier(expr)) { 187 return expr.text === parameterName; 188 } 189 if (isPropertyAccessExpression(expr)) { 190 return expr.name.text === parameterName; 191 } 192 return false; 193 } 194 195 function leadingCommentsContainsParameterName(node: Node, name: string) { 196 if (!isIdentifierText(name, compilerOptions.target, getLanguageVariant(file.scriptKind))) { 197 return false; 198 } 199 200 const ranges = getLeadingCommentRanges(sourceFileText, node.pos); 201 if (!ranges?.length) { 202 return false; 203 } 204 205 const regex = leadingParameterNameCommentRegexFactory(name); 206 return some(ranges, range => regex.test(sourceFileText.substring(range.pos, range.end))); 207 } 208 209 function isHintableLiteral(node: Node) { 210 switch (node.kind) { 211 case SyntaxKind.PrefixUnaryExpression: { 212 const operand = (node as PrefixUnaryExpression).operand; 213 return isLiteralExpression(operand) || isIdentifier(operand) && isInfinityOrNaNString(operand.escapedText); 214 } 215 case SyntaxKind.TrueKeyword: 216 case SyntaxKind.FalseKeyword: 217 case SyntaxKind.NullKeyword: 218 case SyntaxKind.NoSubstitutionTemplateLiteral: 219 case SyntaxKind.TemplateExpression: 220 return true; 221 case SyntaxKind.Identifier: { 222 const name = (node as Identifier).escapedText; 223 return isUndefined(name) || isInfinityOrNaNString(name); 224 } 225 } 226 return isLiteralExpression(node); 227 } 228 229 function visitFunctionDeclarationLikeForReturnType(decl: FunctionDeclaration | ArrowFunction | FunctionExpression | MethodDeclaration | GetAccessorDeclaration) { 230 if (isArrowFunction(decl)) { 231 if (!findChildOfKind(decl, SyntaxKind.OpenParenToken, file)) { 232 return; 233 } 234 } 235 236 const effectiveTypeAnnotation = getEffectiveReturnTypeNode(decl); 237 if (effectiveTypeAnnotation || !decl.body) { 238 return; 239 } 240 241 const signature = checker.getSignatureFromDeclaration(decl); 242 if (!signature) { 243 return; 244 } 245 246 const returnType = checker.getReturnTypeOfSignature(signature); 247 if (isModuleReferenceType(returnType)) { 248 return; 249 } 250 251 const typeDisplayString = printTypeInSingleLine(returnType); 252 if (!typeDisplayString) { 253 return; 254 } 255 256 addTypeHints(typeDisplayString, getTypeAnnotationPosition(decl)); 257 } 258 259 function getTypeAnnotationPosition(decl: FunctionDeclaration | ArrowFunction | FunctionExpression | MethodDeclaration | GetAccessorDeclaration) { 260 const closeParenToken = findChildOfKind(decl, SyntaxKind.CloseParenToken, file); 261 if (closeParenToken) { 262 return closeParenToken.end; 263 } 264 return decl.parameters.end; 265 } 266 267 function visitFunctionLikeForParameterType(node: FunctionLikeDeclaration) { 268 const signature = checker.getSignatureFromDeclaration(node); 269 if (!signature) { 270 return; 271 } 272 273 for (let i = 0; i < node.parameters.length && i < signature.parameters.length; ++i) { 274 const param = node.parameters[i]; 275 if (!isHintableDeclaration(param)) { 276 continue; 277 } 278 279 const effectiveTypeAnnotation = getEffectiveTypeAnnotationNode(param); 280 if (effectiveTypeAnnotation) { 281 continue; 282 } 283 284 const typeDisplayString = getParameterDeclarationTypeDisplayString(signature.parameters[i]); 285 if (!typeDisplayString) { 286 continue; 287 } 288 289 addTypeHints(typeDisplayString, param.questionToken ? param.questionToken.end : param.name.end); 290 } 291 } 292 293 function getParameterDeclarationTypeDisplayString(symbol: Symbol) { 294 const valueDeclaration = symbol.valueDeclaration; 295 if (!valueDeclaration || !isParameter(valueDeclaration)) { 296 return undefined; 297 } 298 299 const signatureParamType = checker.getTypeOfSymbolAtLocation(symbol, valueDeclaration); 300 if (isModuleReferenceType(signatureParamType)) { 301 return undefined; 302 } 303 304 return printTypeInSingleLine(signatureParamType); 305 } 306 307 function truncation(text: string, maxLength: number) { 308 if (text.length > maxLength) { 309 return text.substr(0, maxLength - "...".length) + "..."; 310 } 311 return text; 312 } 313 314 function printTypeInSingleLine(type: Type) { 315 const flags = NodeBuilderFlags.IgnoreErrors | TypeFormatFlags.AllowUniqueESSymbolType | TypeFormatFlags.UseAliasDefinedOutsideCurrentScope; 316 const options: PrinterOptions = { removeComments: true }; 317 const printer = createPrinter(options); 318 319 return usingSingleLineStringWriter(writer => { 320 const typeNode = checker.typeToTypeNode(type, /*enclosingDeclaration*/ undefined, flags, writer); 321 Debug.assertIsDefined(typeNode, "should always get typenode"); 322 printer.writeNode(EmitHint.Unspecified, typeNode, /*sourceFile*/ file, writer); 323 }); 324 } 325 326 function isUndefined(name: __String) { 327 return name === "undefined"; 328 } 329 330 function isHintableDeclaration(node: VariableDeclaration | ParameterDeclaration) { 331 if ((isParameterDeclaration(node) || isVariableDeclaration(node) && isVarConst(node)) && node.initializer) { 332 const initializer = skipParentheses(node.initializer); 333 return !(isHintableLiteral(initializer) || isNewExpression(initializer) || isObjectLiteralExpression(initializer) || isAssertionExpression(initializer)); 334 } 335 return true; 336 } 337 } 338} 339