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