diff --git a/docs/configuration/language-server-settings.md b/docs/configuration/language-server-settings.md index e7fa61c73..dffaaf04f 100644 --- a/docs/configuration/language-server-settings.md +++ b/docs/configuration/language-server-settings.md @@ -48,7 +48,7 @@ the following settings are exclusive to basedpyright ![](inlayHints.functionReturnTypes.png) -**basedpyright.analysis.inlayHints.genericTypes** [boolean]: Whether to show inlay hints on inferred generic types. (currently only works on `Final` and `ClassVar`): +**basedpyright.analysis.inlayHints.genericTypes** [boolean]: Whether to show inlay hints on inferred generic types. Defaults to `false`: ![](inlayHints.genericTypes.png) diff --git a/packages/pyright-internal/src/analyzer/typeInlayHintsWalker.ts b/packages/pyright-internal/src/analyzer/typeInlayHintsWalker.ts index 1ff3c40f0..4c0edb3fb 100644 --- a/packages/pyright-internal/src/analyzer/typeInlayHintsWalker.ts +++ b/packages/pyright-internal/src/analyzer/typeInlayHintsWalker.ts @@ -151,12 +151,11 @@ export class TypeInlayHintsWalker extends ParseTreeWalker { !isAny(type) && !(isClass(type) && isLiteralType(type)) && !isTypeVar(type) && - // !isFunction(type) && !isParamSpec(type) ) { this.featureItems.push({ inlayHintType: 'variable', - position: node.start + node.length, + position: this._endOfNode(node), value: `: ${ type.props?.typeAliasInfo && node.nodeType === ParseNodeType.Name && @@ -172,7 +171,7 @@ export class TypeInlayHintsWalker extends ParseTreeWalker { } override visitCall(node: CallNode): boolean { - if (this._settings.callArgumentNames && this._checkInRange(node)) { + if (this._checkInRange(node)) { this._generateHintsForCallNode(node); } return super.visitCall(node); @@ -211,7 +210,7 @@ export class TypeInlayHintsWalker extends ParseTreeWalker { if (valueType) { this.featureItems.push({ inlayHintType: 'generic', - position: node.start + node.length, + position: this._endOfNode(node), value: `[${this._printType(valueType)}]`, }); } @@ -229,12 +228,49 @@ export class TypeInlayHintsWalker extends ParseTreeWalker { if (!evaluator) { return; } - const functionType = evaluator.getType(node.d.leftExpr); - if (!functionType) { + const callableType = evaluator.getType(node.d.leftExpr); + if (!callableType) { return; } + + // inlay hints for generics + if ( + this._settings.genericTypes && + // where the type is not explicitly specified + node.d.leftExpr.nodeType !== ParseNodeType.Index && + // only show them on classes, because the index syntax to specify generics isn't valid on functions + isClass(callableType) + ) { + const returnType = evaluator.getType(node); + if ( + returnType && + isClass(returnType) && + returnType.priv.typeArgs?.length === returnType.shared.typeParams.length + ) { + const printedTypeArgs = returnType.priv.typeArgs.flatMap((typeArg) => + isClass(typeArg) && typeArg.priv.tupleTypeArgs + ? typeArg.priv.tupleTypeArgs.map((asdf) => this._printType(asdf.type)) + : this._printType(typeArg) + ); + if (returnType.priv.tupleTypeArgs) { + // for tuples, as far as i can tell there's no cases where it can infer non-variadic generics, so we just always + // add the ellipsis + printedTypeArgs.push('...'); + } + this.featureItems.push({ + inlayHintType: 'generic', + position: this._endOfNode(node.d.leftExpr), + value: `[${printedTypeArgs.join(', ')}]`, + }); + } + } + + if (!this._settings.callArgumentNames) { + return; + } + // if it's an overload, figure out which one to use based on the arguments: - const matchedFunctionType = limitOverloadBasedOnCall(evaluator, functionType, node.d.leftExpr); + const matchedFunctionType = limitOverloadBasedOnCall(evaluator, callableType, node.d.leftExpr); const matchedArgs = this._program.evaluator?.matchCallArgsToParams(node, matchedFunctionType); // if there was no match, or if there were multiple matches, we don't want to show any inlay hints because they'd likely be wrong: @@ -289,6 +325,8 @@ export class TypeInlayHintsWalker extends ParseTreeWalker { } } + private _endOfNode = (node: ParseNode) => node.start + node.length; + private _printType = (type: Type): string => this._program.evaluator!.printType(type, { enforcePythonSyntax: true }); } diff --git a/packages/pyright-internal/src/languageServerBase.ts b/packages/pyright-internal/src/languageServerBase.ts index 60f0d886b..0fec7a0ac 100644 --- a/packages/pyright-internal/src/languageServerBase.ts +++ b/packages/pyright-internal/src/languageServerBase.ts @@ -1011,7 +1011,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis callArgumentNames: inlayHintSettings?.callArgumentNames ?? true, functionReturnTypes: inlayHintSettings?.functionReturnTypes ?? true, variableTypes: inlayHintSettings?.variableTypes ?? true, - genericTypes: inlayHintSettings?.genericTypes ?? true, + genericTypes: inlayHintSettings?.genericTypes ?? false, }).onInlayHints(); }, token); } diff --git a/packages/pyright-internal/src/realLanguageServer.ts b/packages/pyright-internal/src/realLanguageServer.ts index 31da77560..e9f27e94a 100644 --- a/packages/pyright-internal/src/realLanguageServer.ts +++ b/packages/pyright-internal/src/realLanguageServer.ts @@ -102,7 +102,12 @@ export abstract class RealLanguageServer extends LanguageServerBase { logLevel: LogLevel.Info, autoImportCompletions: true, functionSignatureDisplay: SignatureDisplayType.formatted, - inlayHints: { callArgumentNames: true, functionReturnTypes: true, variableTypes: true, genericTypes: true }, + inlayHints: { + callArgumentNames: true, + functionReturnTypes: true, + variableTypes: true, + genericTypes: false, + }, }; try { diff --git a/packages/pyright-internal/src/tests/samples/inlay_hints/generics.py b/packages/pyright-internal/src/tests/samples/inlay_hints/generics.py index 4bb8b4554..5107d4f1f 100644 --- a/packages/pyright-internal/src/tests/samples/inlay_hints/generics.py +++ b/packages/pyright-internal/src/tests/samples/inlay_hints/generics.py @@ -1,4 +1,4 @@ -from typing import ClassVar, Final +from typing import ClassVar, Final, Unpack foo: Final = int() @@ -7,4 +7,20 @@ class Foo: a: ClassVar = "asdf" - b: ClassVar[str] = "asdf" \ No newline at end of file + b: ClassVar[str] = "asdf" + +_ = list([1]) + +class Foo[T]: + def __init__(self, value: T) -> None: + self.value = value + +_ = Foo(True) + +_ = tuple((1,2,3)) + +class Bar[U, *T]: + def __init__(self, asdf: U,*value: Unpack[T]) -> None: + pass + +_ = Bar([1], 1,2,"") \ No newline at end of file diff --git a/packages/pyright-internal/src/tests/testUtils.ts b/packages/pyright-internal/src/tests/testUtils.ts index da44d6456..f92a61f91 100644 --- a/packages/pyright-internal/src/tests/testUtils.ts +++ b/packages/pyright-internal/src/tests/testUtils.ts @@ -32,6 +32,7 @@ import { SemanticTokenItem, SemanticTokensWalker } from '../analyzer/semanticTok import { TypeInlayHintsItemType, TypeInlayHintsWalker } from '../analyzer/typeInlayHintsWalker'; import { Range } from 'vscode-languageserver-types'; import { ServiceProvider } from '../common/serviceProvider'; +import { InlayHintSettings } from '../common/languageServerInterface'; // This is a bit gross, but it's necessary to allow the fallback typeshed // directory to be located when running within the jest environment. This @@ -147,13 +148,17 @@ export const semanticTokenizeSampleFile = (fileName: string): SemanticTokenItem[ return walker.items; }; -export const inlayHintSampleFile = (fileName: string, range?: Range): TypeInlayHintsItemType[] => { +export const inlayHintSampleFile = ( + fileName: string, + range?: Range, + settings: Partial = {} +): TypeInlayHintsItemType[] => { const program = createProgram(); const fileUri = UriEx.file(resolveSampleFilePath(path.join('inlay_hints', fileName))); program.setTrackedFiles([fileUri]); const walker = new TypeInlayHintsWalker( program, - { callArgumentNames: true, functionReturnTypes: true, variableTypes: true, genericTypes: true }, + { callArgumentNames: true, functionReturnTypes: true, variableTypes: true, genericTypes: false, ...settings }, fileUri, range ); diff --git a/packages/pyright-internal/src/tests/typeInlayHintsWalker.test.ts b/packages/pyright-internal/src/tests/typeInlayHintsWalker.test.ts index 11d9da43a..c9e0adcd0 100644 --- a/packages/pyright-internal/src/tests/typeInlayHintsWalker.test.ts +++ b/packages/pyright-internal/src/tests/typeInlayHintsWalker.test.ts @@ -78,18 +78,48 @@ if (process.platform !== 'win32' || !process.env['CI']) { ]); }); test('generics', () => { - const result = inlayHintSampleFile('generics.py'); + const result = inlayHintSampleFile('generics.py', undefined, { genericTypes: true }); expect(result).toStrictEqual([ { inlayHintType: 'generic', - position: 47, + position: 55, value: '[int]', }, { inlayHintType: 'generic', - position: 118, + position: 126, value: '[str]', }, + { + inlayHintType: 'generic', + position: 175, + value: '[int]', + }, + { + inlayHintType: 'generic', + position: 273, + value: '[bool]', + }, + { + inlayHintType: 'parameter', + position: 274, + value: 'value=', + }, + { + inlayHintType: 'generic', + position: 290, + value: '[Literal[1, 2, 3], ...]', + }, + { + inlayHintType: 'generic', + position: 399, + value: '[list[int], int, int, str]', + }, + { + inlayHintType: 'parameter', + position: 400, + value: 'asdf=', + }, ]); }); } else { diff --git a/packages/vscode-pyright/package.json b/packages/vscode-pyright/package.json index 968797e21..2066f9fee 100644 --- a/packages/vscode-pyright/package.json +++ b/packages/vscode-pyright/package.json @@ -1764,7 +1764,7 @@ }, "basedpyright.analysis.inlayHints.genericTypes": { "type": "boolean", - "default": true, + "default": false, "description": "Whether to show inlay hints on inferred generic types.", "scope": "resource" }