From 9d258d7e881a3c61e0f540c987bb58401b614581 Mon Sep 17 00:00:00 2001 From: detachhead Date: Sun, 27 Oct 2024 22:46:26 +1000 Subject: [PATCH 1/6] add inlay hints for generics when instantiating classes --- .../src/analyzer/typeInlayHintsWalker.ts | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/pyright-internal/src/analyzer/typeInlayHintsWalker.ts b/packages/pyright-internal/src/analyzer/typeInlayHintsWalker.ts index 1ff3c40f0..4db1ec099 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 && @@ -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,29 @@ 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 where the type is not explicitly specified + if (this._settings.genericTypes && node.d.leftExpr.nodeType !== ParseNodeType.Index && isClass(callableType)) { + const returnType = evaluator.getType(node); + if ( + returnType && + isClass(returnType) && + returnType.priv.typeArgs?.length === returnType.shared.typeParams.length + ) { + this.featureItems.push({ + inlayHintType: 'generic', + position: this._endOfNode(node.d.leftExpr), + value: `[${returnType.priv.typeArgs.map((typeArg) => this._printType(typeArg)).join(', ')}]`, + }); + } + } + // 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 +305,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 }); } From 576665409d31811a6b14aacb4832104890e8ac57 Mon Sep 17 00:00:00 2001 From: detachhead Date: Mon, 28 Oct 2024 18:08:19 +1000 Subject: [PATCH 2/6] disable `genericTypes` by default since it would often display redundant information when `variableTypes` is enabled --- docs/configuration/language-server-settings.md | 2 +- packages/pyright-internal/src/languageServerBase.ts | 2 +- packages/pyright-internal/src/realLanguageServer.ts | 7 ++++++- packages/vscode-pyright/package.json | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) 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/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/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" } From fe1bf2835840dfffefcdb7c19bec16e7169596e7 Mon Sep 17 00:00:00 2001 From: detachhead Date: Mon, 28 Oct 2024 18:21:51 +1000 Subject: [PATCH 3/6] add tests for generic inlay hints --- .../src/tests/samples/inlay_hints/generics.py | 10 +++++++++- packages/pyright-internal/src/tests/testUtils.ts | 9 +++++++-- .../src/tests/typeInlayHintsWalker.test.ts | 12 +++++++++++- 3 files changed, 27 insertions(+), 4 deletions(-) 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..64996e1a7 100644 --- a/packages/pyright-internal/src/tests/samples/inlay_hints/generics.py +++ b/packages/pyright-internal/src/tests/samples/inlay_hints/generics.py @@ -7,4 +7,12 @@ 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("") \ 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..d0cbef38e 100644 --- a/packages/pyright-internal/src/tests/typeInlayHintsWalker.test.ts +++ b/packages/pyright-internal/src/tests/typeInlayHintsWalker.test.ts @@ -78,7 +78,7 @@ 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', @@ -90,6 +90,16 @@ if (process.platform !== 'win32' || !process.env['CI']) { position: 118, value: '[str]', }, + { + inlayHintType: 'generic', + position: 167, + value: '[int]', + }, + { + inlayHintType: 'generic', + position: 265, + value: '[bool]', + }, ]); }); } else { From e319c360460a37f70a5fdfa064c845a795f67214 Mon Sep 17 00:00:00 2001 From: detachhead Date: Mon, 28 Oct 2024 18:30:50 +1000 Subject: [PATCH 4/6] fix `callArgumentNames` inlay hints not being displayed when `genericTypes` is disabled --- .../pyright-internal/src/analyzer/typeInlayHintsWalker.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/pyright-internal/src/analyzer/typeInlayHintsWalker.ts b/packages/pyright-internal/src/analyzer/typeInlayHintsWalker.ts index 4db1ec099..8ccc05aad 100644 --- a/packages/pyright-internal/src/analyzer/typeInlayHintsWalker.ts +++ b/packages/pyright-internal/src/analyzer/typeInlayHintsWalker.ts @@ -171,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); @@ -249,6 +249,10 @@ export class TypeInlayHintsWalker extends ParseTreeWalker { } } + if (!this._settings.callArgumentNames) { + return; + } + // if it's an overload, figure out which one to use based on the arguments: const matchedFunctionType = limitOverloadBasedOnCall(evaluator, callableType, node.d.leftExpr); const matchedArgs = this._program.evaluator?.matchCallArgsToParams(node, matchedFunctionType); From 0d80931d329d54937085507e9494c10ddc1e2cbd Mon Sep 17 00:00:00 2001 From: detachhead Date: Mon, 28 Oct 2024 19:16:39 +1000 Subject: [PATCH 5/6] fix inlay hints for `tuple` generics and typevartuples --- .../src/analyzer/typeInlayHintsWalker.ts | 22 ++++++++++++++++--- .../src/tests/samples/inlay_hints/generics.py | 12 ++++++++-- .../src/tests/typeInlayHintsWalker.test.ts | 18 +++++++++++---- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/packages/pyright-internal/src/analyzer/typeInlayHintsWalker.ts b/packages/pyright-internal/src/analyzer/typeInlayHintsWalker.ts index 8ccc05aad..4c0edb3fb 100644 --- a/packages/pyright-internal/src/analyzer/typeInlayHintsWalker.ts +++ b/packages/pyright-internal/src/analyzer/typeInlayHintsWalker.ts @@ -233,18 +233,34 @@ export class TypeInlayHintsWalker extends ParseTreeWalker { return; } - // inlay hints for generics where the type is not explicitly specified - if (this._settings.genericTypes && node.d.leftExpr.nodeType !== ParseNodeType.Index && isClass(callableType)) { + // 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: `[${returnType.priv.typeArgs.map((typeArg) => this._printType(typeArg)).join(', ')}]`, + value: `[${printedTypeArgs.join(', ')}]`, }); } } 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 64996e1a7..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() @@ -15,4 +15,12 @@ class Foo[T]: def __init__(self, value: T) -> None: self.value = value -_ = Foo("") \ No newline at end of file +_ = 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/typeInlayHintsWalker.test.ts b/packages/pyright-internal/src/tests/typeInlayHintsWalker.test.ts index d0cbef38e..04fadb628 100644 --- a/packages/pyright-internal/src/tests/typeInlayHintsWalker.test.ts +++ b/packages/pyright-internal/src/tests/typeInlayHintsWalker.test.ts @@ -82,24 +82,34 @@ if (process.platform !== 'win32' || !process.env['CI']) { expect(result).toStrictEqual([ { inlayHintType: 'generic', - position: 47, + position: 55, value: '[int]', }, { inlayHintType: 'generic', - position: 118, + position: 126, value: '[str]', }, { inlayHintType: 'generic', - position: 167, + position: 175, value: '[int]', }, { inlayHintType: 'generic', - position: 265, + position: 273, value: '[bool]', }, + { + inlayHintType: 'generic', + position: 290, + value: '[Literal[1, 2, 3], ...]', + }, + { + inlayHintType: 'generic', + position: 399, + value: '[list[int], int, int, str]', + }, ]); }); } else { From e1c74323b2e22b4543cef11f3ad2bb21122172f9 Mon Sep 17 00:00:00 2001 From: detachhead Date: Mon, 28 Oct 2024 19:26:51 +1000 Subject: [PATCH 6/6] fix generics inlay hint test --- .../src/tests/typeInlayHintsWalker.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/pyright-internal/src/tests/typeInlayHintsWalker.test.ts b/packages/pyright-internal/src/tests/typeInlayHintsWalker.test.ts index 04fadb628..c9e0adcd0 100644 --- a/packages/pyright-internal/src/tests/typeInlayHintsWalker.test.ts +++ b/packages/pyright-internal/src/tests/typeInlayHintsWalker.test.ts @@ -100,6 +100,11 @@ if (process.platform !== 'win32' || !process.env['CI']) { position: 273, value: '[bool]', }, + { + inlayHintType: 'parameter', + position: 274, + value: 'value=', + }, { inlayHintType: 'generic', position: 290, @@ -110,6 +115,11 @@ if (process.platform !== 'win32' || !process.env['CI']) { position: 399, value: '[list[int], int, int, str]', }, + { + inlayHintType: 'parameter', + position: 400, + value: 'asdf=', + }, ]); }); } else {