Skip to content
This repository was archived by the owner on Apr 14, 2022. It is now read-only.

Commit 25b4846

Browse files
author
Mikhail Arkhipov
authored
Special handling of variables representing class members (#1424)
* Initial * Update test * Tell between class members with/without self better * Move interface * Fix reference search
1 parent 47fabae commit 25b4846

File tree

17 files changed

+235
-33
lines changed

17 files changed

+235
-33
lines changed

src/Analysis/Ast/Impl/Analyzer/Definitions/LookupOptions.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public enum LookupOptions {
2323
Nonlocal = 2,
2424
Global = 4,
2525
Builtins = 8,
26-
Normal = Local | Nonlocal | Global | Builtins
26+
ClassMembers = 16,
27+
Normal = Local | Nonlocal | Global | Builtins,
28+
All = Normal | ClassMembers
2729
}
2830
}

src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.Scopes.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,14 @@ public void DeclareVariable(string name, IMember value, VariableSource source, L
6363

6464
public IMember LookupNameInScopes(string name, out IScope scope, out IVariable v, LookupOptions options) {
6565
scope = null;
66+
var classMembers = (options & LookupOptions.ClassMembers) == LookupOptions.ClassMembers;
6667

6768
switch (options) {
69+
case LookupOptions.All:
6870
case LookupOptions.Normal:
6971
// Regular lookup: all scopes and builtins.
7072
for (var s = CurrentScope; s != null; s = (Scope)s.OuterScope) {
71-
if (s.Variables.Contains(name)) {
73+
if (s.Variables.TryGetVariable(name, out var v1) && (!v1.IsClassMember || classMembers)) {
7274
scope = s;
7375
break;
7476
}

src/Analysis/Ast/Impl/Analyzer/Handlers/AssignmentHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public void HandleAnnotatedExpression(ExpressionWithAnnotation expr, IMember val
9393
private void TryHandleClassVariable(AssignmentStatement node, IMember value) {
9494
var mex = node.Left.OfType<MemberExpression>().FirstOrDefault();
9595
if (!string.IsNullOrEmpty(mex?.Name) && mex.Target is NameExpression nex && nex.Name.EqualsOrdinal("self")) {
96-
var m = Eval.LookupNameInScopes(nex.Name, out var scope, LookupOptions.Local);
96+
var m = Eval.LookupNameInScopes(nex.Name, out _, LookupOptions.Local);
9797
var cls = m.GetPythonType<IPythonClassType>();
9898
if (cls != null) {
9999
using (Eval.OpenScope(Eval.Module, cls.ClassDefinition, out _)) {

src/Analysis/Ast/Impl/Linting/UndefinedVariables/ExpressionWalker.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public override bool Walk(NameExpression node) {
7878
}
7979

8080
var analysis = _walker.Analysis;
81-
var m = analysis.ExpressionEvaluator.LookupNameInScopes(node.Name, out var variableDefinitionScope, out var v);
81+
var m = analysis.ExpressionEvaluator.LookupNameInScopes(node.Name, out var variableDefinitionScope, out var v, LookupOptions.All);
8282
if (m == null) {
8383
_walker.ReportUndefinedVariable(node);
8484
}

src/Analysis/Ast/Impl/Types/PythonClassType.Generics.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
1-
using System;
1+
// Copyright(c) Microsoft Corporation
2+
// All rights reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the License); you may not use
5+
// this file except in compliance with the License. You may obtain a copy of the
6+
// License at http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
9+
// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
10+
// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
11+
// MERCHANTABILITY OR NON-INFRINGEMENT.
12+
//
13+
// See the Apache Version 2.0 License for specific language governing
14+
// permissions and limitations under the License.
15+
16+
using System;
217
using System.Collections.Generic;
318
using System.Linq;
419
using Microsoft.Python.Analysis.Specializations.Typing;
@@ -7,7 +22,7 @@
722
using Microsoft.Python.Core;
823

924
namespace Microsoft.Python.Analysis.Types {
10-
internal partial class PythonClassType : PythonType, IPythonClassType, IGenericType, IEquatable<IPythonClassType> {
25+
internal partial class PythonClassType {
1126
private bool _isGeneric;
1227
private object _genericParameterLock = new object();
1328
private Dictionary<string, PythonClassType> _specificTypeCache;

src/Analysis/Ast/Impl/Types/PythonClassType.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ namespace Microsoft.Python.Analysis.Types {
3333
internal partial class PythonClassType : PythonType, IPythonClassType, IGenericType, IEquatable<IPythonClassType> {
3434
private static readonly string[] _classMethods = { "mro", "__dict__", @"__weakref__" };
3535

36-
private ReentrancyGuard<IPythonClassType> _memberGuard = new ReentrancyGuard<IPythonClassType>();
36+
private readonly ReentrancyGuard<IPythonClassType> _memberGuard = new ReentrancyGuard<IPythonClassType>();
3737
private string _genericName;
3838
private List<IPythonType> _bases;
3939
private IReadOnlyList<IPythonType> _mro;

src/Analysis/Ast/Impl/Types/PythonType.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,16 @@ internal bool TrySetTypeId(BuiltinTypeId typeId) {
115115
internal void AddMembers(IEnumerable<IVariable> variables, bool overwrite) {
116116
lock (_lock) {
117117
if (!_readonly) {
118-
foreach (var v in variables.Where(m => overwrite || !Members.ContainsKey(m.Name))) {
119-
// If variable holds function or a class, use value as member.
120-
// If it holds an instance, use the variable itself (i.e. it is a data member).
121-
WritableMembers[v.Name] = v.Value;
118+
foreach (var v in variables.OfType<Variable>()) {
119+
var hasMember = Members.ContainsKey(v.Name);
120+
if (overwrite || !hasMember) {
121+
// If variable holds function or a class, use value as member.
122+
// If it holds an instance, use the variable itself (i.e. it is a data member).
123+
WritableMembers[v.Name] = v.Value;
124+
}
125+
if (hasMember) {
126+
v.IsClassMember = true;
127+
}
122128
}
123129
}
124130
}

src/Analysis/Ast/Impl/Values/Definitions/IVariable.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ public interface IVariable: ILocatedMember {
3535
/// </summary>
3636
IMember Value { get; }
3737

38+
/// <summary>
39+
/// Variable represents class member.
40+
/// </summary>
41+
bool IsClassMember { get; }
42+
3843
/// <summary>
3944
/// Assigns value to the variable.
4045
/// </summary>

src/Analysis/Ast/Impl/Values/Variable.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public Variable(string name, IMember value, VariableSource source, Location loca
3232
public string Name { get; }
3333
public VariableSource Source { get; }
3434
public IMember Value { get; private set; }
35+
public bool IsClassMember { get; internal set; }
3536

3637
public void Assign(IMember value, Location location) {
3738
if (value is IVariable v) {

src/Analysis/Ast/Test/ClassesTests.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -632,7 +632,7 @@ def __init__(self):
632632
sw.Stop();
633633
// Desktop: product time is typically less few seconds second.
634634
// Test run time: typically ~ 20 sec.
635-
sw.ElapsedMilliseconds.Should().BeLessThan(60000);
635+
sw.ElapsedMilliseconds.Should().BeLessThan(60000);
636636
}
637637

638638
[TestMethod, Priority(0)]
@@ -651,6 +651,32 @@ def foo(*args, **kwargs):
651651
analysis.Should().HaveVariable("a").OfType(BuiltinTypeId.Int);
652652
}
653653

654+
[TestMethod, Priority(0)]
655+
public async Task MemberCtorAssignment() {
656+
const string code = @"
657+
class x:
658+
y: int
659+
z = y
660+
661+
a = x().z
662+
";
663+
var analysis = await GetAnalysisAsync(code);
664+
analysis.Should().HaveVariable("a").OfType(BuiltinTypeId.Int);
665+
}
666+
667+
[TestMethod, Priority(0)]
668+
public async Task AmbiguousMemberAssignment() {
669+
const string code = @"
670+
class x:
671+
x: int
672+
y = x
673+
674+
a = x().y
675+
";
676+
var analysis = await GetAnalysisAsync(code);
677+
analysis.Should().HaveVariable("a").OfType(BuiltinTypeId.Int);
678+
}
679+
654680
[TestMethod, Priority(0)]
655681
public async Task IOErrorBase() {
656682
const string code = @"

src/Analysis/Ast/Test/FunctionTests.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,5 +606,27 @@ def test(foo: Foo = func()):
606606
var analysis = await GetAnalysisAsync(code, PythonVersions.LatestAvailable3X);
607607
analysis.Should().HaveVariable("x").OfType("Foo");
608608
}
609+
610+
[TestMethod, Priority(0)]
611+
public async Task AmbiguousOptionalParameterType() {
612+
const string code = @"
613+
from typing import Optional
614+
class A: ...
615+
616+
class B:
617+
def __init__(self, A: Optional[A]):
618+
self.name = name
619+
620+
@property
621+
def A(self) -> int:
622+
";
623+
var analysis = await GetAnalysisAsync(code, PythonVersions.LatestAvailable3X);
624+
var a = analysis.Should().HaveClass("A").Which;
625+
analysis.Should().HaveClass("B")
626+
.Which.Should().HaveMethod("__init__")
627+
.Which.Should().HaveParameterAt(1)
628+
.Which.Should().HaveType("A")
629+
.Which.Should().BeOfType(a.GetType());
630+
}
609631
}
610632
}

src/Core/Impl/Text/IndexSpan.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,16 @@ namespace Microsoft.Python.Core.Text {
2323
/// It is closed on the left and open on the right: [Start .. End).
2424
/// </summary>
2525
public struct IndexSpan : IEquatable<IndexSpan> {
26-
private readonly int _start, _length;
27-
2826
public IndexSpan(int start, int length) {
29-
_start = start;
30-
_length = length;
27+
Start = start;
28+
Length = length;
3129
}
3230

33-
public int Start => _start;
31+
public int Start { get; }
3432

35-
public int End => _start + _length;
33+
public int End => Start + Length;
3634

37-
public int Length => _length;
35+
public int Length { get; }
3836

3937
public override int GetHashCode() => Length.GetHashCode() ^ Start.GetHashCode();
4038

@@ -49,7 +47,7 @@ public IndexSpan(int start, int length) {
4947
}
5048

5149
#region IEquatable<IndexSpan> Members
52-
public bool Equals(IndexSpan other) => _length == other._length && _start == other._start;
50+
public bool Equals(IndexSpan other) => Length == other.Length && Start == other.Start;
5351
#endregion
5452

5553
public static IndexSpan FromBounds(int start, int end) => new IndexSpan(start, end - start);

src/LanguageServer/Impl/Completion/TopLevelCompletion.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public static CompletionResult GetCompletions(Node statement, ScopeStatement sco
3939
IEnumerable<CompletionItem> items;
4040
using (eval.OpenScope(scope)) {
4141
// Get variables declared in the module.
42-
var variables = eval.CurrentScope.EnumerateTowardsGlobal.SelectMany(s => s.Variables).ToArray();
42+
var variables = eval.CurrentScope.EnumerateTowardsGlobal.SelectMany(s => s.Variables).Where(v => !v.IsClassMember).ToArray();
4343
items = variables.Select(v => context.ItemSource.CreateCompletionItem(v.Name, v)).ToArray();
4444
}
4545

src/LanguageServer/Impl/Sources/DefinitionSource.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ private Reference HandleImport(IDocumentAnalysis analysis, ImportStatement state
191191
private Reference TryFromVariable(string name, IDocumentAnalysis analysis, SourceLocation location, Node statement, out ILocatedMember definingMember) {
192192
definingMember = null;
193193

194-
var m = analysis.ExpressionEvaluator.LookupNameInScopes(name, out var scope);
194+
var m = analysis.ExpressionEvaluator.LookupNameInScopes(name, out var scope, LookupOptions.All);
195195
if (m == null || !(scope.Variables[name] is IVariable v)) {
196196
return null;
197197
}

src/LanguageServer/Impl/Sources/HoverSource.cs

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
// See the Apache Version 2.0 License for specific language governing
1414
// permissions and limitations under the License.
1515

16+
using System;
1617
using Microsoft.Python.Analysis;
1718
using Microsoft.Python.Analysis.Analyzer;
1819
using Microsoft.Python.Analysis.Analyzer.Expressions;
1920
using Microsoft.Python.Analysis.Types;
21+
using Microsoft.Python.Analysis.Values;
2022
using Microsoft.Python.Core;
2123
using Microsoft.Python.Core.Collections;
2224
using Microsoft.Python.Core.Text;
@@ -38,7 +40,7 @@ public Hover GetHover(IDocumentAnalysis analysis, SourceLocation location) {
3840
}
3941

4042
ExpressionLocator.FindExpression(analysis.Ast, location,
41-
FindExpressionOptions.Hover, out var node, out var statement, out var scope);
43+
FindExpressionOptions.Hover, out var node, out var statement, out var hoverScopeStatement);
4244

4345
if (!HasHover(node) || !(node is Expression expr)) {
4446
return null;
@@ -52,7 +54,7 @@ public Hover GetHover(IDocumentAnalysis analysis, SourceLocation location) {
5254
var eval = analysis.ExpressionEvaluator;
5355
switch (statement) {
5456
case FromImportStatement fi when node is NameExpression nex: {
55-
var contents = HandleFromImport(fi, location, scope, analysis);
57+
var contents = HandleFromImport(fi, location, hoverScopeStatement, analysis);
5658
if (contents != null) {
5759
return new Hover {
5860
contents = contents,
@@ -63,7 +65,7 @@ public Hover GetHover(IDocumentAnalysis analysis, SourceLocation location) {
6365
break;
6466
}
6567
case ImportStatement imp: {
66-
var contents = HandleImport(imp, location, scope, analysis);
68+
var contents = HandleImport(imp, location, hoverScopeStatement, analysis);
6769
if (contents != null) {
6870
return new Hover {
6971
contents = contents,
@@ -77,8 +79,30 @@ public Hover GetHover(IDocumentAnalysis analysis, SourceLocation location) {
7779

7880
IMember value;
7981
IPythonType type;
80-
using (eval.OpenScope(analysis.Document, scope)) {
81-
value = analysis.ExpressionEvaluator.GetValueFromExpression(expr);
82+
using (eval.OpenScope(analysis.Document, hoverScopeStatement)) {
83+
// Here we can be hovering over a class member. Class members are declared
84+
// as members as well as special variables in the class scope. If this is
85+
// a name expression (rather than a member expression) and it is a class
86+
// variable NOT in the immediate class scope, filter it out. Consider:
87+
// class A:
88+
// x = 1
89+
// y = x
90+
// hover over 'x' in 'y = x' should produce proper tooltip. However, in
91+
// class A:
92+
// x = 1
93+
// def func(self):
94+
// y = x
95+
// hover over 'x' in 'y = x' should not produce tooltip.
96+
97+
IVariable variable = null;
98+
if (expr is NameExpression nex) {
99+
analysis.ExpressionEvaluator.LookupNameInScopes(nex.Name, out _, out variable, LookupOptions.All);
100+
if (IsInvalidClassMember(variable, hoverScopeStatement, location.ToIndex(analysis.Ast))) {
101+
return null;
102+
}
103+
}
104+
105+
value = variable?.Value ?? analysis.ExpressionEvaluator.GetValueFromExpression(expr, LookupOptions.All);
82106
type = value?.GetPythonType();
83107
if (type == null) {
84108
return null;
@@ -122,19 +146,36 @@ public Hover GetHover(IDocumentAnalysis analysis, SourceLocation location) {
122146
};
123147
}
124148

125-
private bool HasHover(Node node) {
149+
private static bool HasHover(Node node) {
126150
switch (node) {
127151
// No hover for literals
128-
case ConstantExpression constExpr:
152+
case ConstantExpression _:
129153
// node is FString only if it didn't save an f-string subexpression
130-
case FString fStr:
131-
case NamedExpression namedExpr:
154+
case FString _:
155+
case NamedExpression _:
132156
return false;
133157
default:
134158
return true;
135159
}
136160
}
137161

162+
private bool IsInvalidClassMember(IVariable v, ScopeStatement scope, int hoverPosition) {
163+
if (v == null || !v.IsClassMember) {
164+
return false;
165+
}
166+
switch (v.Value) {
167+
case IPythonClassType cls when cls.ClassDefinition == scope:
168+
return hoverPosition > cls.ClassDefinition.HeaderIndex;
169+
case IPythonFunctionType ft when ft.FunctionDefinition == scope:
170+
return hoverPosition > ft.FunctionDefinition.HeaderIndex;
171+
case IPythonPropertyType prop when prop.FunctionDefinition == scope:
172+
return hoverPosition > prop.FunctionDefinition.HeaderIndex;
173+
case IPythonInstance _:
174+
return !(scope is ClassDefinition);
175+
}
176+
return true;
177+
}
178+
138179
private MarkupContent HandleImport(ImportStatement imp, SourceLocation location, ScopeStatement scope, IDocumentAnalysis analysis) {
139180
// 'import A.B, B.C, D.E as F, G, H'
140181
var eval = analysis.ExpressionEvaluator;

0 commit comments

Comments
 (0)