diff --git a/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.Annotations.cs b/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.Annotations.cs index 4bd4b0815..d0b3d5747 100644 --- a/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.Annotations.cs +++ b/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.Annotations.cs @@ -13,6 +13,7 @@ // See the Apache Version 2.0 License for specific language governing // permissions and limitations under the License. +using Microsoft.Python.Analysis.Specializations.Typing; using Microsoft.Python.Analysis.Types; using Microsoft.Python.Parsing.Ast; @@ -26,6 +27,14 @@ public IPythonType GetTypeFromAnnotation(Expression expr, out bool isGeneric, Lo switch (expr) { case null: return null; + case NameExpression nameExpr: + // x: T + var name = GetValueFromExpression(nameExpr); + if(name is IGenericTypeParameter gtp) { + isGeneric = true; + return gtp; + } + break; case CallExpression callExpr: // x: NamedTuple(...) return GetValueFromCallable(callExpr)?.GetPythonType() ?? UnknownType; diff --git a/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.Callables.cs b/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.Callables.cs index de3a6903c..b10ab0e68 100644 --- a/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.Callables.cs +++ b/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.Callables.cs @@ -325,14 +325,16 @@ public IReadOnlyList CreateFunctionParameters(IPythonClassType s if (self != null && function.HasClassFirstArgument()) { var p0 = fd.Parameters.FirstOrDefault(); if (p0 != null && !string.IsNullOrEmpty(p0.Name)) { + var annType = GetTypeFromAnnotation(p0.Annotation, out var isGeneric); // Actual parameter type will be determined when method is invoked. // The reason is that if method might be called on a derived class. // Declare self or cls in this scope. if (declareVariables) { DeclareVariable(p0.Name, new PythonInstance(self), VariableSource.Declaration, p0.NameExpression); } - // Set parameter info. - var pi = new ParameterInfo(Ast, p0, self, null, false); + // Set parameter info, declare type as annotation type for generic self + // e.g def test(self: T) + var pi = new ParameterInfo(Ast, p0, isGeneric ? annType : self, null, false); parameters.Add(pi); skip++; } diff --git a/src/Analysis/Ast/Impl/Extensions/MemberExtensions.cs b/src/Analysis/Ast/Impl/Extensions/MemberExtensions.cs index a8ff6d9e1..3ffcdb84c 100644 --- a/src/Analysis/Ast/Impl/Extensions/MemberExtensions.cs +++ b/src/Analysis/Ast/Impl/Extensions/MemberExtensions.cs @@ -35,6 +35,10 @@ public static bool IsUnknown(this IMember m) { public static bool IsOfType(this IMember m, BuiltinTypeId typeId) => m?.GetPythonType().TypeId == typeId; + public static string GetString(this IMember m) { + return (m as IPythonConstant)?.GetString(); + } + public static IPythonType GetPythonType(this IMember m) { switch (m) { case IPythonType pt: diff --git a/src/Analysis/Ast/Impl/Extensions/PythonClassExtensions.cs b/src/Analysis/Ast/Impl/Extensions/PythonClassExtensions.cs index 0e7408757..7ee3f4a85 100644 --- a/src/Analysis/Ast/Impl/Extensions/PythonClassExtensions.cs +++ b/src/Analysis/Ast/Impl/Extensions/PythonClassExtensions.cs @@ -14,6 +14,7 @@ // permissions and limitations under the License. using Microsoft.Python.Analysis.Analyzer; +using Microsoft.Python.Analysis.Specializations.Typing; using Microsoft.Python.Analysis.Types; using Microsoft.Python.Core; @@ -56,5 +57,17 @@ public static bool IsPrivateMember(this IPythonClassType cls, string memberName) var unmangledName = cls.UnmangleMemberName(memberName); return unmangledName.StartsWithOrdinal("__") && memberName.EqualsOrdinal($"_{cls.Name}{unmangledName}"); } + + /// + /// Gets specific type for the given generic type parameter, resolving bounds as well + /// + public static bool GetSpecificType(this IPythonClassType cls, IGenericTypeParameter param, out IPythonType specificType) { + cls.GenericParameters.TryGetValue(param, out specificType); + // If type has not been found, check if the type parameter has an upper bound and use that + if (specificType is IGenericTypeParameter gtp && gtp.Bound != null) { + specificType = gtp.Bound; + } + return specificType != null; + } } } diff --git a/src/Analysis/Ast/Impl/Specializations/Typing/Definitions/IGenericTypeParameter.cs b/src/Analysis/Ast/Impl/Specializations/Typing/Definitions/IGenericTypeParameter.cs index 9aca942a0..1376a6ed1 100644 --- a/src/Analysis/Ast/Impl/Specializations/Typing/Definitions/IGenericTypeParameter.cs +++ b/src/Analysis/Ast/Impl/Specializations/Typing/Definitions/IGenericTypeParameter.cs @@ -21,11 +21,16 @@ namespace Microsoft.Python.Analysis.Specializations.Typing { /// /// Represents generic type definition. Typically value returned by TypeVar. /// - public interface IGenericTypeParameter: IPythonType, IEquatable { + /// See 'https://docs.python.org/3/library/typing.html#typing.TypeVar' + public interface IGenericTypeParameter : IPythonType, IEquatable { /// /// List of constraints for the type. /// - /// See 'https://docs.python.org/3/library/typing.html#typing.TypeVar' IReadOnlyList Constraints { get; } + + /// + /// Bound type, i.e upper bound this type parameter can represent + /// + IPythonType Bound { get; } } } diff --git a/src/Analysis/Ast/Impl/Specializations/Typing/Types/GenericTypeParameter.cs b/src/Analysis/Ast/Impl/Specializations/Typing/Types/GenericTypeParameter.cs index 0cdc143fb..eb7a59412 100644 --- a/src/Analysis/Ast/Impl/Specializations/Typing/Types/GenericTypeParameter.cs +++ b/src/Analysis/Ast/Impl/Specializations/Typing/Types/GenericTypeParameter.cs @@ -25,11 +25,17 @@ namespace Microsoft.Python.Analysis.Specializations.Typing.Types { internal sealed class GenericTypeParameter : PythonType, IGenericTypeParameter { - public GenericTypeParameter(string name, IPythonModule declaringModule, IReadOnlyList constraints, string documentation, IndexSpan location) + public GenericTypeParameter(string name, IPythonModule declaringModule, IReadOnlyList constraints, + IPythonType bound, string documentation, IndexSpan location) : base(name, new Location(declaringModule), documentation) { Constraints = constraints ?? Array.Empty(); + Bound = bound; } + + #region IGenericTypeParameter public IReadOnlyList Constraints { get; } + public IPythonType Bound { get; } + #endregion public override BuiltinTypeId TypeId => BuiltinTypeId.Type; public override PythonMemberType MemberType => PythonMemberType.Generic; @@ -77,6 +83,22 @@ private static bool TypeVarArgumentsValid(IArgumentSet argSet) { return true; } + /// + /// Given arguments to TypeVar, finds the bound type + /// + private static IPythonType GetBoundType(IArgumentSet argSet) { + var eval = argSet.Eval; + var rawBound = argSet.GetArgumentValue("bound"); + switch (rawBound) { + case IPythonType t: + return t; + case IPythonConstant c when c.GetString() != null: + return eval.GetTypeFromString(c.GetString()); + default: + return rawBound.GetPythonType(); + } + } + public static IPythonType FromTypeVar(IArgumentSet argSet, IPythonModule declaringModule, IndexSpan location = default) { if (!TypeVarArgumentsValid(argSet)) { return declaringModule.Interpreter.UnknownType; @@ -85,15 +107,16 @@ public static IPythonType FromTypeVar(IArgumentSet argSet, IPythonModule declari var args = argSet.Arguments; var constraintArgs = argSet.ListArgument?.Values ?? Array.Empty(); - var name = (args[0].Value as IPythonConstant)?.GetString(); + var name = argSet.GetArgumentValue("name")?.GetString(); var constraints = constraintArgs.Select(a => { // Type constraints may be specified as type name strings. - var typeString = (a as IPythonConstant)?.GetString(); + var typeString = a.GetString(); return !string.IsNullOrEmpty(typeString) ? argSet.Eval.GetTypeFromString(typeString) : a.GetPythonType(); }).ToArray() ?? Array.Empty(); - + var bound = GetBoundType(argSet); var documentation = GetDocumentation(args, constraints); - return new GenericTypeParameter(name, declaringModule, constraints, documentation, location); + + return new GenericTypeParameter(name, declaringModule, constraints, bound, documentation, location); } private static string GetDocumentation(IReadOnlyList args, IReadOnlyList constraints) { diff --git a/src/Analysis/Ast/Impl/Specializations/Typing/TypingModule.cs b/src/Analysis/Ast/Impl/Specializations/Typing/TypingModule.cs index 0db5964bb..967409990 100644 --- a/src/Analysis/Ast/Impl/Specializations/Typing/TypingModule.cs +++ b/src/Analysis/Ast/Impl/Specializations/Typing/TypingModule.cs @@ -354,7 +354,7 @@ private IPythonType CreateAnyStr() { var docArgs = new[] { $"'{name}'" }.Concat(constraints.Select(c => c.Name)); var documentation = CodeFormatter.FormatSequence("TypeVar", '(', docArgs); - return new GenericTypeParameter(name, this, constraints, documentation, default); + return new GenericTypeParameter(name, this, constraints, default, documentation, default); } private IPythonType CreateGenericClassParameter(IReadOnlyList typeArgs) { diff --git a/src/Analysis/Ast/Impl/Types/PythonClassType.Generics.cs b/src/Analysis/Ast/Impl/Types/PythonClassType.Generics.cs index 88d4f7985..63a233bb8 100644 --- a/src/Analysis/Ast/Impl/Types/PythonClassType.Generics.cs +++ b/src/Analysis/Ast/Impl/Types/PythonClassType.Generics.cs @@ -251,6 +251,7 @@ private IReadOnlyDictionary GetSpecificTypes } } } + return genericTypeToSpecificType; } @@ -284,7 +285,7 @@ private void StoreGenericParameters(PythonClassType classType, IGenericTypeParam } } } - + /// /// Given generic type such as Generic[T1, T2, ...] attempts to extract specific types /// for its parameters from an argument value. Handles common cases such as dictionary, diff --git a/src/Analysis/Ast/Impl/Types/PythonFunctionOverload.cs b/src/Analysis/Ast/Impl/Types/PythonFunctionOverload.cs index bc81fa3de..4b340ec1d 100644 --- a/src/Analysis/Ast/Impl/Types/PythonFunctionOverload.cs +++ b/src/Analysis/Ast/Impl/Types/PythonFunctionOverload.cs @@ -140,7 +140,7 @@ private IMember GetSpecificReturnType(IPythonClassType selfClassType, IArgumentS break; case IGenericTypeParameter gtd1 when selfClassType != null: - return CreateSpecificReturnFromTypeVar(selfClassType, gtd1); // -> _T + return CreateSpecificReturnFromTypeVar(selfClassType, args, gtd1); // -> _T case IGenericTypeParameter gtd2 when args != null: // -> T on standalone function. return args.Arguments.FirstOrDefault(a => gtd2.Equals(a.Type))?.Value as IMember; @@ -172,8 +172,8 @@ private IMember CreateSpecificReturnFromClassType(IPythonClassType selfClassType return null; } - private IMember CreateSpecificReturnFromTypeVar(IPythonClassType selfClassType, IGenericTypeParameter returnType) { - if (selfClassType.GenericParameters.TryGetValue(returnType, out var specificType)) { + private IMember CreateSpecificReturnFromTypeVar(IPythonClassType selfClassType, IArgumentSet args, IGenericTypeParameter returnType) { + if (selfClassType.GetSpecificType(returnType, out var specificType)) { return new PythonInstance(specificType); } @@ -184,10 +184,21 @@ private IMember CreateSpecificReturnFromTypeVar(IPythonClassType selfClassType, .FirstOrDefault(b => b.GetMember(ClassMember.Name) != null && b.GenericParameters.ContainsKey(returnType)); // Try and infer return value from base class - if (baseType != null && baseType.GenericParameters.TryGetValue(returnType, out specificType)) { + if (baseType != null && baseType.GetSpecificType(returnType, out specificType)) { return new PythonInstance(specificType); } + // Try getting type from passed in arguments + var typeFromArgs = args?.Arguments.FirstOrDefault(a => returnType.Equals(a.Type))?.Value as IMember; + if (typeFromArgs != null) { + return typeFromArgs; + } + + // Try getting the type from the type parameter bound + if (returnType.Bound != null) { + return new PythonInstance(returnType.Bound); + } + // Try returning the constraint // TODO: improve this, the heuristic is pretty basic and tailored to simple func(_T) -> _T var name = StaticReturnValue.GetPythonType()?.Name; diff --git a/src/Analysis/Ast/Test/GenericsTests.cs b/src/Analysis/Ast/Test/GenericsTests.cs index 35a6146c5..aec8dba6c 100644 --- a/src/Analysis/Ast/Test/GenericsTests.cs +++ b/src/Analysis/Ast/Test/GenericsTests.cs @@ -1048,6 +1048,25 @@ def set_width(self, w: float) -> 'Square': analysis.Should().HaveVariable("square").Which.Should().HaveType("Square"); } + [TestMethod, Priority(0)] + public async Task GenericSelf() { + const string code = @" +from typing import TypeVar + +T = TypeVar('T') + +class C: + def test(self: T) -> T: + pass + +class D(C): ... + +x = D().test() +"; + var analysis = await GetAnalysisAsync(code, PythonVersions.LatestAvailable3X); + analysis.Should().HaveVariable("x").Which.Should().HaveType("D"); + } + [TestMethod, Priority(0)] public async Task GenericClassToDifferentTypes() { const string code = @" @@ -1122,5 +1141,40 @@ def log(self, message: str) -> None: var analysis = await GetAnalysisAsync(code); analysis.Should().HaveVariable("x").OfType(BuiltinTypeId.Int); } + + [TestMethod, Priority(0)] + public async Task GenericBound() { + const string code = @" +from typing import TypeVar, Generic +from logging import Logger, getLogger + +T = TypeVar('T', bound='A') + +class A: ... + +class Test(Generic[T]): + def get(self) -> T: ... + +x = Test().get() +"; + var analysis = await GetAnalysisAsync(code); + analysis.Should().HaveVariable("x").OfType("A"); + } + + [TestMethod, Priority(0)] + public async Task GenericPath() { + const string code = @" +import pathlib + +h = pathlib._PurePathBase +root = pathlib.Path('/some/directory') +subdir = root / 'subdir' +child = subdir / 'file.txt' +"; + var analysis = await GetAnalysisAsync(code, PythonVersions.Python37); + analysis.Should().HaveVariable("root").OfType("Path"); + analysis.Should().HaveVariable("subdir").OfType("PurePath"); + analysis.Should().HaveVariable("child").OfType("PurePath"); + } } }