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

Commit b17f08c

Browse files
author
Mikhail Arkhipov
committed
Allow navigation to stubs (#1960)
* Remove stale reference * Don't suppress LHS diagnostics on augmented assign * Revert "Don't suppress LHS diagnostics on augmented assign" This reverts commit 6109ac7. * Escape [ and ] * PR feedback * Allow navigation to stubs * Better handle stubs for compiled modules * Fix typeshed path * Partial undo * Partial undo * Undo accidental change
1 parent 7463f67 commit b17f08c

File tree

9 files changed

+105
-26
lines changed

9 files changed

+105
-26
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ public IDisposable OpenScope(IPythonModule module, ScopeStatement node, out Scop
179179
return new ScopeTracker(this);
180180
}
181181

182+
internal void ReplaceVariable(IVariable v) => CurrentScope.ReplaceVariable(v);
183+
182184
private class ScopeTracker : IDisposable {
183185
private readonly ExpressionEval _eval;
184186

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ public ExpressionEval(IServiceContainer services, IPythonModule module, PythonAs
6060
public LocationInfo GetLocationInfo(Node node) => node?.GetLocation(this) ?? LocationInfo.Empty;
6161

6262
public Location GetLocationOfName(Node node) {
63-
if (node == null || (Module.ModuleType != ModuleType.User && Module.ModuleType != ModuleType.Library)) {
63+
if (node == null ||
64+
Module.ModuleType == ModuleType.Specialized || Module.ModuleType == ModuleType.Compiled ||
65+
Module.ModuleType == ModuleType.CompiledBuiltin || Module.ModuleType == ModuleType.Builtins) {
6466
return DefaultLocation;
6567
}
6668

src/Analysis/Ast/Impl/Analyzer/StubMerger.cs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,16 @@ private void TryReplaceMember(IVariable v, IPythonType sourceType, IPythonType s
110110
}
111111
break;
112112

113-
case PythonClassType sourceClass:
114-
MergeClass(v, sourceClass, stubType, cancellationToken);
113+
case IPythonClassType sourceClass:
114+
MergeMembers(v, sourceClass, stubType, cancellationToken);
115+
break;
116+
117+
case PythonFunctionType sourceFunction:
118+
MergeMembers(v, sourceFunction, stubType, cancellationToken);
119+
break;
120+
121+
case PythonPropertyType sourceProperty:
122+
MergeMembers(v, sourceProperty, stubType, cancellationToken);
115123
break;
116124

117125
case IPythonModule _:
@@ -137,28 +145,38 @@ private void TryReplaceMember(IVariable v, IPythonType sourceType, IPythonType s
137145
}
138146
}
139147

140-
private void MergeClass(IVariable v, IPythonClassType sourceClass, IPythonType stubType, CancellationToken cancellationToken) {
148+
private void MergeMembers(IVariable v, IPythonType sourceType, IPythonType stubType, CancellationToken cancellationToken) {
141149
// Transfer documentation first so we get class documentation
142150
// that comes from the class definition win over one that may
143151
// come from __init__ during the member merge below.
144-
TransferDocumentationAndLocation(sourceClass, stubType);
152+
TransferDocumentationAndLocation(sourceType.GetPythonType(), stubType);
145153

146154
// Replace the class entirely since stub members may use generic types
147155
// and the class definition is important. We transfer missing members
148156
// from the original class to the stub.
149-
_eval.DeclareVariable(v.Name, v.Value, v.Source);
157+
//
158+
// In case module is compiled, it is already a stub and has no locations
159+
// for code navigation.. In this case we replace the entire variable by one
160+
// from the stub rather than just the value since stub variable has location
161+
// and its own root definition/reference chain.
162+
if (sourceType.DeclaringModule.ModuleType == ModuleType.Compiled ||
163+
sourceType.DeclaringModule.ModuleType == ModuleType.CompiledBuiltin) {
164+
_eval.ReplaceVariable(v);
165+
} else {
166+
_eval.DeclareVariable(v.Name, v.Value, v.Source);
167+
}
150168

151169
// First pass: go through source class members and pick those
152170
// that are not present in the stub class.
153-
foreach (var name in sourceClass.GetMemberNames().ToArray()) {
171+
foreach (var name in sourceType.GetMemberNames().ToArray()) {
154172
cancellationToken.ThrowIfCancellationRequested();
155173

156-
var sourceMember = sourceClass.GetMember(name);
174+
var sourceMember = sourceType.GetMember(name);
157175
if (sourceMember.IsUnknown()) {
158176
continue; // Do not add unknowns to the stub.
159177
}
160178
var sourceMemberType = sourceMember?.GetPythonType();
161-
if (sourceMemberType is IPythonClassMember cm && cm.DeclaringType != sourceClass) {
179+
if (sourceMemberType is IPythonClassMember cm && !cm.DeclaringModule.Equals(sourceType.DeclaringModule)) {
162180
continue; // Only take members from this class and not from bases.
163181
}
164182
if (!IsFromThisModuleOrSubmodules(sourceMemberType)) {
@@ -196,7 +214,7 @@ private void MergeClass(IVariable v, IPythonClassType sourceClass, IPythonType s
196214
continue; // Only take members from this class and not from bases.
197215
}
198216

199-
var sourceMember = sourceClass.GetMember(name);
217+
var sourceMember = sourceType.GetMember(name);
200218
if (sourceMember.IsUnknown()) {
201219
continue;
202220
}
@@ -305,7 +323,13 @@ private bool IsFromThisModuleOrSubmodules(IPythonType type) {
305323
var thisModule = _eval.Module;
306324
var typeModule = type.DeclaringModule;
307325
var typeMainModuleName = typeModule.Name.Split('.').FirstOrDefault();
308-
return typeModule.Equals(thisModule) || typeMainModuleName == thisModule.Name;
326+
if (typeModule.Equals(thisModule) || typeMainModuleName == thisModule.Name) {
327+
return true;
328+
}
329+
// Check if module is explicitly imported by the current one. For example, 'os'
330+
// imports 'nt' and os.pyi specifies functions from 'nt' such as mkdir and so on.
331+
var imported = thisModule.GlobalScope.Variables[typeModule.Name];
332+
return imported?.Value != null && imported.Source == VariableSource.Import;
309333
}
310334
}
311335
}

src/Analysis/Ast/Impl/Modules/Resolution/TypeshedResolution.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@ internal sealed class TypeshedResolution : ModuleResolutionBase, IModuleResoluti
3232

3333
public TypeshedResolution(string root, IServiceContainer services) : base(root, services) {
3434
// TODO: merge with user-provided stub paths
35-
var stubs = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Stubs");
36-
_typeStubPaths = GetTypeShedPaths(Root)
35+
var asmLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
36+
var stubs = Path.Combine(asmLocation, "Stubs");
37+
var typeshedRoot = Root ?? Path.Combine(asmLocation, "Typeshed");
38+
39+
_typeStubPaths = GetTypeShedPaths(typeshedRoot)
3740
.Concat(GetTypeShedPaths(stubs))
3841
.Where(services.GetService<IFileSystem>().DirectoryExists)
3942
.ToImmutableArray();

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ public void DeclareImported(string name, IMember value, Location location = defa
9393

9494
internal void AddChildScope(Scope s) => (_childScopes ?? (_childScopes = new List<Scope>())).Add(s);
9595

96+
internal void ReplaceVariable(IVariable v) {
97+
VariableCollection.RemoveVariable(v.Name);
98+
VariableCollection.DeclareVariable(v.Name, v.Value, v.Source, v.Location);
99+
}
100+
96101
private VariableCollection VariableCollection => _variables ?? (_variables = new VariableCollection());
97102

98103
private void DeclareBuiltinVariables() {

src/LanguageServer/Impl/Implementation/Server.Editor.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,23 @@ public async Task<Reference[]> GotoDefinition(TextDocumentPositionParams @params
8989
_log?.Log(TraceEventType.Verbose, $"Goto Definition in {uri} at {@params.position}");
9090

9191
var analysis = await Document.GetAnalysisAsync(uri, Services, CompletionAnalysisTimeout, cancellationToken);
92-
var reference = new DefinitionSource(Services).FindDefinition(analysis, @params.position, out _);
93-
return reference != null ? new[] { reference } : Array.Empty<Reference>();
92+
var ds = new DefinitionSource(Services);
93+
var reference = ds.FindDefinition(analysis, @params.position, out _);
94+
return reference != null && ds.CanNavigateToModule(reference.uri)
95+
? new[] { reference }
96+
: Array.Empty<Reference>();
9497
}
9598

9699
public async Task<Location> GotoDeclaration(TextDocumentPositionParams @params, CancellationToken cancellationToken) {
97100
var uri = @params.textDocument.uri;
98101
_log?.Log(TraceEventType.Verbose, $"Goto Declaration in {uri} at {@params.position}");
99102

100103
var analysis = await Document.GetAnalysisAsync(uri, Services, CompletionAnalysisTimeout, cancellationToken);
101-
var reference = new DeclarationSource(Services).FindDefinition(analysis, @params.position, out _);
102-
return reference != null ? new Location { uri = reference.uri, range = reference.range } : null;
104+
var ds = new DeclarationSource(Services);
105+
var reference = ds.FindDefinition(analysis, @params.position, out _);
106+
return reference != null && ds.CanNavigateToModule(reference.uri)
107+
? new Location { uri = reference.uri, range = reference.range }
108+
: null;
103109
}
104110

105111
public Task<Reference[]> FindReferences(ReferencesParams @params, CancellationToken cancellationToken) {
@@ -137,8 +143,8 @@ public async Task<CodeAction[]> CodeAction(CodeActionParams @params, Cancellatio
137143

138144
return codeActions.ToArray();
139145

140-
static bool AskedFor(CodeActionParams @params, string codeActionKind) {
141-
return @params.context.only == null || @params.context.only.Any(s => s.StartsWith(codeActionKind));
146+
bool AskedFor(CodeActionParams p, string codeActionKind) {
147+
return p.context.only == null || p.context.only.Any(s => s.StartsWith(codeActionKind));
142148
}
143149
}
144150
}

src/LanguageServer/Impl/Sources/DefinitionSource.cs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
// permissions and limitations under the License.
1515

1616
using System;
17+
using System.IO;
1718
using System.Linq;
1819
using Microsoft.Python.Analysis;
1920
using Microsoft.Python.Analysis.Analyzer;
@@ -360,22 +361,32 @@ private Reference FromMember(IMember m) {
360361
return null;
361362
}
362363

363-
private bool CanNavigateToModule(Uri uri) {
364+
public bool CanNavigateToModule(Uri uri) {
364365
if (uri == null) {
365366
return false;
366367
}
368+
369+
if (!CanNavigateToPath(uri.LocalPath)) {
370+
return false;
371+
}
367372
var rdt = _services.GetService<IRunningDocumentTable>();
368373
var doc = rdt.GetDocument(uri);
369374
// Allow navigation to modules not in RDT - most probably
370375
// it is a module that was restored from database.
371376
return doc == null || CanNavigateToModule(doc);
372377
}
373378

374-
private static bool CanNavigateToModule(IPythonModule m)
375-
=> m?.ModuleType == ModuleType.User ||
376-
m?.ModuleType == ModuleType.Stub ||
377-
m?.ModuleType == ModuleType.Package ||
378-
m?.ModuleType == ModuleType.Library ||
379-
m?.ModuleType == ModuleType.Specialized;
379+
private static bool CanNavigateToModule(IPythonModule m) {
380+
if(m == null || !CanNavigateToPath(m.FilePath)) {
381+
return false;
382+
}
383+
return m.ModuleType == ModuleType.User ||
384+
m.ModuleType == ModuleType.Stub ||
385+
m.ModuleType == ModuleType.Package ||
386+
m.ModuleType == ModuleType.Library ||
387+
m.ModuleType == ModuleType.Specialized;
388+
}
389+
390+
private static bool CanNavigateToPath(string path) => Path.GetExtension(path) != ".exe";
380391
}
381392
}

src/LanguageServer/Test/GoToDefinitionTests.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,5 +720,19 @@ def func(a, b):
720720
var reference = ds.FindDefinition(analysis, new SourceLocation(3, 12), out _);
721721
reference.range.Should().Be(1, 9, 1, 10);
722722
}
723+
724+
[TestMethod, Priority(0)]
725+
public async Task CompiledCode() {
726+
const string code = @"
727+
import os
728+
os.mkdir()
729+
";
730+
var analysis = await GetAnalysisAsync(code);
731+
var ds = new DefinitionSource(Services);
732+
var reference = ds.FindDefinition(analysis, new SourceLocation(3, 6), out _);
733+
reference.range.start.line.Should().NotBe(1);
734+
reference.range.end.line.Should().NotBe(1);
735+
reference.uri.AbsolutePath.Should().Contain(".pyi");
736+
}
723737
}
724738
}

src/LanguageServer/Test/HoverTests.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,18 @@ class A:
284284
AssertHover(hs, analysis, new SourceLocation(4, 7), @"x: int", new SourceSpan(4, 7, 4, 8));
285285
}
286286

287+
[TestMethod, Priority(0)]
288+
public async Task CompiledCode() {
289+
const string code = @"
290+
import os
291+
os.mkdir()
292+
";
293+
var analysis = await GetAnalysisAsync(code);
294+
var hs = new HoverSource(new PlainTextDocumentationSource());
295+
var hover = hs.GetHover(analysis, new SourceLocation(3, 6));
296+
hover.contents.value.Should().Contain("Create a directory");
297+
}
298+
287299
private static void AssertNoHover(HoverSource hs, IDocumentAnalysis analysis, SourceLocation position) {
288300
var hover = hs.GetHover(analysis, position);
289301
hover.Should().BeNull();

0 commit comments

Comments
 (0)