• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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